· 7 min de lectura

Por qué escribimos a mano un analizador PartiQL para DynamoDB

DynamoDB acepta una porción muy estrecha de y rechaza todo lo demás en tiempo de petición. ¿GROUP BY? ValidationException. ¿Un LIMIT a nivel de sentencia? ValidationException. ¿El operador *, CAST, una subconsulta? Todos se analizan sin problema en tu cabeza, viajan por la red y mueren en el servidor. El único sitio donde vivía ese conocimiento era la documentación de AWS y los mensajes de error, lo que significaba que cualquier editor para DynamoDB —incluido el nuestro, durante un tiempo— te dejaba componer tan tranquilo una sentencia que el motor tenía garantizado rechazar.

Queríamos que el rechazo ocurriera en el editor, en la pulsación de tecla, con un subrayado rojo sobre la cláusula exacta y una corrección de un clic allí donde existe una reescritura. Esa necesidad del editor acabó convertida en un lexer y un analizador CST escritos a mano para el dialecto PartiQL de DynamoDB, y esta semana lo hemos publicado en código abierto: dynamodb-partiql-parser, TypeScript puro, cero dependencias, MIT, con el cableado para CodeMirror publicado aparte como codemirror-lang-partiql. Esta publicación trata de por qué está escrito a mano, qué hacía mal el primer linter y los dos errores que solo aparecieron cuando alguien pegó basura.

Regex funcionaba, hasta que dejó de funcionar

El primer linter de PartiQL en DynoTable eran unas 650 líneas de expresiones regulares y escaneo de tokens, y resultaba genuinamente útil: diecinueve comprobaciones distintas, correcciones rápidas para las trampas habituales (IN (...) a [...], LIKE a contains(), IS NULL a attribute_not_exists()). Se publicó, cazó errores reales y los usuarios dejaron de abrir tickets de «por qué falla mi consulta» para los casos que cubría.

Pero un linter de expresiones regulares conoce patrones, no estructura. No podía ver que el * de SELECT price * quantity es aritmética que DynamoDB rechaza, porque * también significa «todas las columnas» y distinguir ambos casos exige analizar de verdad. Sus rangos de diagnóstico eran aproximaciones: lo bastante buenos para señalar una línea, demasiado gruesos para alimentar una corrección rápida que empalma texto en desplazamientos exactos. Y cada comprobación nueva volvía el montón más frágil, porque cada expresión regular tenía que defenderse de las suposiciones de todas las demás.

El remedio para «el linter necesita estructura» es un analizador. La pregunta era cuál.

Nadie había construido uno

Para el lado de SQL real del Workbench ya habíamos pasado por esto: un analizador SQL comercial que nos mentía, sustituido por sql-parser-cst, que lleva un rango de origen en cada nodo y preserva los identificadores entrecomillados frente a los que no lo están. Aquella experiencia fijó el listón de lo que necesitaba el lado PartiQL: un árbol de sintaxis concreto sin pérdidas, no un AST con pérdidas.

Pero PartiQL no es SQL justo en lo que le importa a un analizador. El dialecto de DynamoDB escribe las listas IN con corchetes (WHERE OrderID IN [100, 300, 234]), tiene literales de bag (<<'a', 'b'>>), literales de mapa con claves entrecomilladas ({'rating': 5}), un literal MISSING, rutas de documento con índices de lista (Devices.FireStick.DateWatched[0]) y RETURNING ALL OLD *; nada de eso lo conoce una gramática SQL. En sentido contrario, carece de la mitad de lo que una gramática SQL da por supuesto. Por entonces, los analizadores que había en npm eran compilaciones a WebAssembly de la implementación en Rust de AWS para PartiQL genérico, sin noción alguna de lo que DynamoDB rechaza en particular.

Así que escribimos uno: un lexer pequeño y un analizador de descenso recursivo, modelado sobre la forma que sql-parser-cst nos había enseñado a querer. Cada nodo lleva su rango de bytes. El conjunto tiene cero dependencias en tiempo de ejecución, una propiedad que ahora verifica la CI, porque es lo que hace al analizador empotrable en cualquier parte, incluido el navegador, incluido tu proyecto.

La gramática era la mitad fácil. El analizador de un linter se pasa la vida entera analizando código roto. A mitad de pulsación, media sentencia, una errata en la tercera cláusula. Detenerse en el primer error dejaría el editor inservible, así que el analizador es tolerante a errores: registra un diagnóstico, se resincroniza y sigue adelante, de modo que la cuarta cláusula se sigue analizando mientras la segunda está incompleta.

Cambiar el motor sin romper el avión

Para cuando el analizador estuvo listo, las cuatro funciones del linter de expresiones regulares sostenían medio editor, incluida la que decide si una sentencia es segura de autoejecutar. Cambiar ese comportamiento en silencio se manifiesta como «el editor no ejecuta mi consulta», que es de esa clase de errores que los usuarios no reportan tanto como abandonan.

Así que el cambio fue un estrangulamiento: el linter viejo se renombró, se congeló y se dejó en el árbol. El nuevo linter guiado por el analizador reexportaba exactamente las mismas cuatro funciones. Y un corpus de paridad pasaba cada fixture por ambos linters y fijaba las salidas de uno contra las del otro: todo diagnóstico que producía la versión de expresiones regulares tenía que producirlo también la versión del analizador antes de que se le permitiera producir más. El linter viejo sigue ahí hoy, congelado, como documentación ejecutable de lo que el cambio prometió.

Los errores que solo encuentra la basura

Dos fallos que nunca aparecieron en ninguna consulta real y que ambos habrían tumbado el editor.

Un linter de CodeMirror se ejecuta de forma síncrona sobre el documento, en cada cambio, sin ningún sumidero de errores por encima. Una excepción no capturada no hace fallar un análisis: deja el editor en blanco. Y un analizador de descenso recursivo trae de serie una excepción no capturada natural: la pila de llamadas. Pega [[[[[[… con unos cuantos miles de corchetes de profundidad, o una cadena NOT NOT NOT …, y cada nivel de anidamiento es un marco de pila; V8 acaba lanzando RangeError: Maximum call stack size exceeded directamente a través del linter.

Las correcciones son aburridas a propósito. La recursión de expresiones tiene un techo de profundidad estricto —quinientos niveles, mucho más allá de lo que escribe cualquier humano y muy por debajo del presupuesto de pila— pasado el cual el analizador emite un único diagnóstico en lugar de lanzar. Y las construcciones donde los pegados encadenan de forma realista, como A UNION B UNION C … con miles de ramas, se reescribieron de recursión a listas planas: un solo marco de parseSelect y un array de operaciones de conjunto, en lugar de un marco por rama. La suite de estrés pega ahora 100 KB de basura y cadenas de operadores de 30 000 de profundidad en cada compilación, y el paquete público envuelve toda la tubería en un punto de entrada lint() que nunca lanza, porque el próximo editor que empotre esto tendrá el mismo problema de ausencia de sumidero de errores que tuvimos nosotros.

Una suite de pruebas que puedes auditar contra la documentación de AWS

Las reglas del dialecto —qué acepta DynamoDB, qué rechaza, qué reescritura arregla qué— salen todas de la referencia de PartiQL de AWS. El comportamiento derivado de documentación tiene un modo de fallo muy concreto: el documento se mueve, el código no y nadie se entera.

Así que el corpus está estructurado en función de eso. Doscientos ocho fixtures, y cada uno abre con la URL de la página de documentación de AWS de la que sale la regla. Una tabla de cobertura mapea cada regla documentada con su fixture, y la suite falla si una regla pierde su fixture. Cuando AWS cambia el dialecto, el diff es un diff de fixtures con una cita encima.

Esa disciplina se pagó sola la semana en que publicamos el código. El aviso del linter sobre listas IN citaba dos límites: 50 valores en una columna de clave de partición, 100 en una columna que no es clave. Al reverificar cada número antes de la publicación, pudimos confirmar el 100 en la documentación actual de AWS, y no encontramos el 50 por ninguna parte en un documento vigente. Sobrevive por todos lados en publicaciones de blog y respuestas viejas de foros, pero la fuente primaria ha seguido adelante. El linter lo tenía bien por accidente (solo avisa a partir de 100, ya que sin tu esquema no puede saber qué caso aplica), y el comentario dice ahora exactamente qué mitad de la afirmación está documentada y cuál es folclore.

Qué se transfiere si estás construyendo uno

  • Un analizador de descenso recursivo escrito a mano para un dialecto pequeño son días de trabajo, no meses, y controlas cada mensaje de error. La versión que asusta de «escribe un analizador» presupone una gramática grande.
  • Construye un CST, no un AST. Los rangos de bytes en cada nodo son lo que convierte los diagnósticos en correcciones rápidas; un árbol con pérdidas no puede empalmar texto.
  • Si el analizador alimenta un linter, la tolerancia a errores es la funcionalidad. Recupérate y continúa; un analizador que se detiene en el primer error no analiza nada de ahí en adelante.
  • Cambia motores detrás de una interfaz congelada, con un corpus de paridad que fije lo viejo contra lo nuevo. La implementación vieja es la especificación que ya aceptaste.
  • Allí donde la entrada pueda anidarse, alguien pegará algo que anida de forma absurda. Pon un tope de profundidad a la recursión y aplana las cadenas; prueba con basura, no solo con consultas.
  • Cita tus fuentes en las pruebas. Un fixture que nombra la página de documentación que codifica es una prueba auditable cuando el documento cambie, y va a cambiar.

El analizador está en GitHub y en npm (npm install dynamodb-partiql-parser), con la integración del editor en codemirror-lang-partiql. Si lo que quieres es el dialecto en sí y no el analizador, PartiQL frente a SQL cubre lo que el subconjunto de DynamoDB puede y no puede hacer, y ejemplos de PartiQL es el recorrido práctico; el editor para el que se construyó todo esto está en DynoTable, y puedes probarlo gratis.

Trabaja con DynamoDB sin la Consola

Un cliente de escritorio rápido para DynamoDB que ejecuta el SQL real que DynamoDB no puede — JOINs, GROUP BY, agregaciones — con edición visual y un agente de IA con tus propias claves de Bedrock.

Prueba gratuita de 30 días, sin tarjeta — después, el plan Free sin límite de tiempo.