· 7 dakikalık okuma

DynamoDB için PartiQL ayrıştırıcısını neden elle yazdık

DynamoDB, 'in dar bir dilimini kabul eder ve geri kalan her şeyi istek anında reddeder. GROUP BY? ValidationException. İfade düzeyinde bir LIMIT? ValidationException. * operatörü, CAST, bir alt sorgu? Hepsi kafanızda sorunsuz ayrıştırılır, ağ üzerinden gider ve sunucuda ölür. Bu bilginin yaşadığı tek yer AWS dokümantasyonu ve hata mesajlarıydı; bu da DynamoDB için her editörün — bir süre boyunca bizimki de dahil — motorun kesinlikle reddedeceği bir ifadeyi yazmanıza seve seve izin vermesi anlamına geliyordu.

Reddin editörde, tuş vuruşunda, tam ilgili cümlenin altında kırmızı bir dalgalı çizgiyle ve bir yeniden yazım mevcut olduğunda tek tıklamalık bir düzeltmeyle gerçekleşmesini istedik. Bu editör ihtiyacı, DynamoDB'nin PartiQL lehçesi için elle yazılmış bir lexer'a ve CST ayrıştırıcısına dönüştü ve bu hafta onu açık kaynak yaptık: dynamodb-partiql-parser, saf TypeScript, sıfır bağımlılık, MIT; CodeMirror bağlantıları ise ayrıca codemirror-lang-partiql olarak yayımlandı. Bu yazı, neden elle yazıldığı, ilk linter'ın neyi yanlış yaptığı ve yalnızca birisi çöp yapıştırdığında ortaya çıkan iki hata hakkındadır.

Regex iyiydi, ta ki olmayana kadar

DynoTable'daki ilk PartiQL linter'ı yaklaşık 650 satırlık regex ve token taramasıydı ve gerçekten kullanışlıydı: on dokuz ayrı kontrol, yaygın tuzaklar için hızlı düzeltmeler (IN (...)'dan [...]'a, LIKE'dan contains()'a, IS NULL'dan attribute_not_exists()'a). Yayımlandı, gerçek hataları yakaladı, kullanıcılar kapsadığı durumlar için "sorgum neden başarısız oluyor" biletleri açmayı bıraktı.

Ama bir regex linter'ı desenleri bilir, yapıyı değil. SELECT price * quantity içindeki *'ın DynamoDB'nin reddettiği bir aritmetik olduğunu göremiyordu, çünkü * aynı zamanda "tüm sütunlar" anlamına gelir ve bu ikisini birbirinden ayırmak gerçekten ayrıştırmayı gerektirir. Tanı aralıkları yaklaşık değerlerdi — bir satırı işaret edecek kadar yakın, metni tam ofsetlerde birleştiren bir hızlı düzeltmeyi sürmek için fazla kaba. Ve her yeni kontrol yığını daha da kırılganlaştırıyordu, çünkü her regex diğer her regex'in varsayımlarına karşı kendini savunmak zorundaydı.

"Linter'ın yapıya ihtiyacı var"ın çözümü bir ayrıştırıcıdır. Soru, hangisi olduğuydu.

Kimse bir tane yazmamıştı

Workbench'in gerçek SQL tarafında bunu zaten yaşamıştık: bize yalan söyleyen hazır bir SQL ayrıştırıcısı, yerini her düğümde bir kaynak aralığı taşıyan ve tırnaklı-tırnaksız tanımlayıcıları koruyan sql-parser-cst'ye bıraktı. O deneyim, PartiQL tarafının neye ihtiyaç duyduğunun çıtasını belirledi — kayıplı bir AST değil, kayıpsız bir somut sözdizim ağacı.

Ama PartiQL, bir ayrıştırıcı açısından önemli olan yerlerde SQL değildir. DynamoDB'nin lehçesi IN listelerini köşeli parantezle yazar (WHERE OrderID IN [100, 300, 234]), bag literalleri (<<'a', 'b'>>), tırnaklı anahtarlı map literalleri ({'rating': 5}), bir MISSING literali, liste indeksli doküman yolları (Devices.FireStick.DateWatched[0]) ve RETURNING ALL OLD * içerir — bunların hiçbirini bir SQL dilbilgisi bilmez. Diğer yönde ise, bir SQL dilbilgisinin ısrar ettiği şeylerin yarısından yoksundur. O sırada npm'deki ayrıştırıcılar, genel PartiQL için AWS'nin Rust uygulamasının WebAssembly derlemeleriydi ve DynamoDB'nin özel olarak neyi reddettiğine dair hiçbir fikirleri yoktu.

Bu yüzden bir tane yazdık: küçük bir lexer ve sql-parser-cst'nin bize istemeyi öğrettiği biçime göre modellenmiş, özyinelemeli inişli (recursive-descent) bir ayrıştırıcı. Her düğüm kendi bayt aralığını taşır. Bütünün sıfır çalışma zamanı bağımlılığı var — CI'ın artık doğruladığı bir özellik, çünkü ayrıştırıcıyı tarayıcı dahil, sizin projeniz dahil her yere gömülebilir kılan şey bu.

Dilbilgisi kolay yarıydı. Bir linter'ın ayrıştırıcısı tüm hayatını bozuk kod ayrıştırarak geçirir. Tuş vuruşunun ortası, yarım bir ifade, üçüncü cümlede bir yazım hatası. İlk hatada durmak editörü işe yaramaz hale getirirdi, bu yüzden ayrıştırıcı hataya toleranslıdır: bir tanı kaydeder, yeniden senkronize olur ve devam eder; böylece ikinci cümle eksikken dördüncü cümle yine de lint'lenir.

Uçağı düşürmeden motoru değiştirmek

Ayrıştırıcı hazır olduğunda, regex linter'ının dört fonksiyonu editörün her yerinde yük taşıyordu — bir ifadenin otomatik çalıştırılmasının güvenli olup olmadığına karar veren de dahil. Bu davranışı sessizce değiştirmek "editör sorgumu çalıştırmıyor" olarak ortaya çıkar; bu, kullanıcıların bildirmekten çok yüzünden çekip gittiği türden bir hatadır.

Bu yüzden değişim bir strangler oldu: eski linter yeniden adlandırıldı, donduruldu ve ağaçta tutuldu. Yeni, ayrıştırıcı tabanlı linter tam olarak aynı dört fonksiyonu yeniden dışa aktardı. Ve bir eşitlik (parity) korpusu her fixture'ı her iki linter'dan geçirip çıktıları birbirine sabitledi — regex sürümünün ürettiği her tanıyı, ayrıştırıcı sürümü de üretmek zorundaydı; fazlasını üretmesine ancak ondan sonra izin verildi. Eski linter bugün hâlâ orada, donmuş halde, değişimin neyi vaat ettiğinin çalıştırılabilir dokümantasyonu olarak.

Yalnızca çöpün bulduğu hatalar

İki arıza hiçbir gerçek sorguda ortaya çıkmadı ve her ikisi de editörü çökertecekti.

Bir CodeMirror linter'ı, dokümanın üzerinde, her değişiklikte, üstünde hiçbir hata toplayıcısı olmadan senkron çalışır. Yakalanmayan tek bir istisna bir lint'i başarısız kılmaz — editörü beyaz ekrana düşürür. Ve özyinelemeli inişli bir ayrıştırıcının içinde doğal bir yakalanmamış istisna gömülüdür: çağrı yığını. Birkaç bin parantez derinliğinde [[[[[[… ya da bir NOT NOT NOT … zinciri yapıştırın; her iç içe geçme düzeyi bir yığın çerçevesidir ve V8 sonunda RangeError: Maximum call stack size exceeded hatasını doğrudan linter'ın içinden fırlatır.

Düzeltmeler kasıtlı olarak sıkıcı. İfade özyinelemesinin katı bir derinlik tavanı var — beş yüz düzey, bir insanın yazacağı her şeyin çok ötesinde, yığın bütçesinin epey altında — bunun ötesinde ayrıştırıcı istisna fırlatmak yerine tek bir tanı yayar. Ve yapıştırmaların gerçekçi biçimde zincirlendiği yapılar — binlerce kol uzunluğunda A UNION B UNION C … gibi — özyinelemeden düz listelere dönüştürüldü: kol başına bir çerçeve yerine tek bir parseSelect çerçevesi ve bir küme işlemleri dizisi. Stres paketi artık her derlemede 100 KB çöp ve 30.000 derinliğinde operatör zincirleri yapıştırıyor ve genel paket tüm boru hattını asla istisna fırlatmayan bir lint() giriş noktasına sarıyor; çünkü bunu gömecek bir sonraki editör de bizimkiyle aynı hata-toplayıcısı-yok sorununu yaşayacak.

AWS'nin dokümanlarına karşı denetleyebileceğiniz bir test paketi

Lehçe kuralları — DynamoDB'nin neyi kabul ettiği, neyi reddettiği, hangi yeniden yazımın neyi düzelttiği — hepsi AWS'nin PartiQL referansından geliyor. Dokümantasyondan türetilmiş davranışın kendine özgü bir arıza biçimi vardır: doküman değişir, kod değişmez ve kimse fark etmez.

Bu yüzden korpus ona göre yapılandırıldı. İki yüz sekiz fixture ve her biri, kuralın geldiği AWS dokümantasyon sayfasının URL'siyle başlıyor. Bir kapsam tablosu, belgelenmiş her kuralı kendi fixture'ına eşler ve bir kural fixture'ını kaybederse paket başarısız olur. AWS lehçeyi değiştirdiğinde, fark, üzerinde bir kaynak göstermesi bulunan bir fixture farkıdır.

Bu disiplin, açık kaynak yaptığımız hafta kendini amorti etti. Linter'ın IN listesi uyarısı iki üst sınır gösteriyordu: partition anahtarı sütununda 50 değer, anahtar olmayan sütunda 100. Yayından önce her sayıyı yeniden doğrularken, 100'ü AWS'nin güncel dokümantasyonunda teyit edebildik — ama 50'yi yürürlükteki hiçbir dokümanda bulamadık. Blog yazılarının ve eski forum cevaplarının her yerinde yaşamayı sürdürüyor, ama birincil kaynak yoluna devam etmiş. Linter tesadüfen doğruyu yapıyordu (yalnızca 100'ün ötesinde uyarıyor, çünkü şemanız olmadan hangi durumun geçerli olduğunu ayırt edemez) ve yorum satırı artık iddianın hangi yarısının belgelenmiş, hangisinin halk hikâyesi olduğunu tam olarak söylüyor.

Bir tane kuruyorsanız ne aktarılır

  • Küçük bir lehçe için elle yazılmış, özyinelemeli inişli bir ayrıştırıcı ayların değil, günlerin işidir ve her hata mesajı sizindir. "Bir ayrıştırıcı yaz"ın korkutucu sürümü büyük bir dilbilgisi varsayar.
  • AST değil, CST kurun. Tanıları hızlı düzeltmelere çeviren şey, her düğümdeki bayt aralıklarıdır; kayıplı bir ağaç metni birleştiremez.
  • Ayrıştırıcı bir linter'ı besliyorsa, asıl özellik hata toleransıdır. Toparlanın ve devam edin; ilk hatada duran bir ayrıştırıcı, ondan sonrasında hiçbir şeyi lint'lemez.
  • Motorları, eskiyi yeniye sabitleyen bir eşitlik korpusuyla, donmuş bir arayüzün arkasında değiştirin. Eski uygulama, üzerinde zaten anlaştığınız spesifikasyondur.
  • Girdinin iç içe geçebildiği her yerde, birileri saçma derecede iç içe geçen bir şey yapıştıracaktır. Özyinelemeye derinlik sınırı koyun ve zincirleri düzleştirin; yalnızca sorgularla değil, çöple test edin.
  • Testlerinizde kaynaklarınızı gösterin. Kodladığı doküman sayfasını adlandıran bir fixture, doküman değiştiğinde denetlenebilen bir testtir — ve değişecektir.

Ayrıştırıcı GitHub'da ve npm'de (npm install dynamodb-partiql-parser), editör entegrasyonu ise codemirror-lang-partiql içinde. Ayrıştırıcı yerine lehçenin kendisini istiyorsanız, PartiQL ve SQL DynamoDB'nin alt kümesinin neler yapıp yapamayacağını kapsar ve PartiQL örnekleri pratik gezintidir; tüm bunların uğruna kurulduğu editör DynoTable içindedir ve onu ücretsiz deneyebilirsiniz.

Console olmadan DynamoDB ile çalış

DynamoDB’nin çalıştıramadığı gerçek SQL’i çalıştıran hızlı bir DynamoDB masaüstü istemcisi — JOINs, GROUP BY, toplamalar — görsel düzenleme ve kendi Bedrock anahtarların üzerinde bir yapay zekâ aracısıyla.

30 günlük ücretsiz deneme, kredi kartı yok — ardından süre sınırı olmayan Ücretsiz plan.