SPEC · El pilar dialecto — formato — parser
Especificación (2026-07-31). Fija el contrato de cómo la aplicación le habla al
Warehouse. Nace de auditar lo que tenemos y de investigar lo que documentan los
líderes (Iceberg, Databricks/Unity Catalog, Trino, Snowflake, sqlglot, DuckDB),
con las decisiones medidas contra nuestro propio stack, no supuestas.
▶ Plan de ejecución: carbon-sql-milestones.md — M0…M7,
con el inventario medido de lo que cada milestone BORRA.
Contexto: warehouse-sql-layer-sota.md ·
junction.md · warehouse-index.md.
Memoria: warehouse-index-pillar, warehouse-narrow-waist, router-junction.
0 · La tesis, en tres frases
El dialecto es un DATO, no una convención. Todo SQL que persistimos declara en qué lengua está escrito y contra qué coordenada resolvía — como hace el spec de vistas de Iceberg, y por el mismo motivo: sin eso, un SQL guardado es una cadena cuyo significado depende de quién la lea.
El parser es UNO, y es el del motor que ejecuta. Dos parsers que deben coincidir son un bug esperando fecha; nuestro caso ya tiene expediente (63 bypasses write-as-read encontrados por el red-team de F4.2). La única fuente sintáctica que no puede divergir del ejecutor es el ejecutor.
La resolución de nombres no es parsing: es un pase aparte, contra Index. Es lo que hacen Trino, Databricks y sqlglot, y lo que nosotros hacemos hoy con cinco heurísticas de expresiones regulares.
1 · Lo que tenemos (auditado, 2026-07-31)
✅ El formato está resuelto — y alineado con el estado del arte
Iceberg v2 + Parquet sobre R2, catálogo Iceberg REST (Lakekeeper), y Apache Arrow
IPC como formato de cable (duck-server lo devuelve como canónico; el JSON tipado
es sólo el envoltorio del editor). Mismo substrato que Databricks, Snowflake y Trino.
Aquí no hay deuda. Con una excepción: las vistas (§4.1).
❌ El lenguaje no está resuelto: son tres lenguas en un endpoint
| Superficie | Lengua | Quién la implementa |
|---|---|---|
| Catálogo / DDL | dialecto Databricks: SHOW …, DESCRIBE [EXTENDED|HISTORY], ALTER TABLE … RENAME, CREATE/DROP VIEW | regex a mano en la route |
| Motor | SQL de DuckDB, prácticamente verbatim (sólo sustitución de tokens + expansión de vistas a CTE) | duck-server |
information_schema.columns | tabla sintética servida desde PG (jsonb_to_recordset) — y enriquecida preguntándole al motor (duckColumns) | una tercera semántica |
Ninguna declara un contrato de dialecto. Escrituras: sólo DELETE y MERGE.
Funciones de tabla: sólo cuatro.
❌ Y no hay parser: hay dos clasificadores heurísticos que deben coincidir
lib/warehouse/query/sql-parse.ts (Node: extractTableNames, scanTableRefs,
extractCteNames, rewriteDottedFromRefs, extractDmlTarget) y
classify_statement + _clean_sql (Python, dentro de duck-server). En dos
lenguajes distintos, sobre la decisión más peligrosa del sistema: si algo es
lectura o escritura, y qué tabla escribe.
❌ Las vistas son una cadena sin contexto
datasets.view_definition guarda el SELECT verbatim: sin dialecto, sin catálogo
ni namespace por defecto, sin versión, y mutable (CREATE OR REPLACE machaca).
Si mañana el motor cambia, o si el usuario renombra un schema, el cuerpo de la vista
significa otra cosa —o deja de significar— y no hay forma de saberlo.
2 · Lo que documentan los líderes (evidencia)
| Fuente | Qué fija | Qué nos dice |
|---|---|---|
| Iceberg View Spec | representations[] con dialect requerido; default-catalog (opt) y default-namespace (requerido, es una LISTA de niveles); versiones inmutables; una representación por dialecto | El estándar ya resolvió «¿en qué lengua está este SQL y contra qué resolvía?». No hay que inventarlo |
| Unity Catalog | INFORMATION_SCHEMA lo sirve el catálogo, por catálogo, consciente de privilegios y auto-filtrado | Las preguntas de catálogo no las contesta el motor: no sabe quién pregunta |
| Trino | parser ANTLR en un módulo propio, separado del análisis y de la planificación | Parse ≠ análisis ≠ resolución. Son fases |
| sqlglot | qualify = qualify_tables(db, catalog) + qualify_columns(schema), con el schema como {catalog: {db: {table: {col: type}}}} | La resolución es un pase sobre el AST alimentado por el catálogo — y su forma es exactamente Warehouse›Catalog›Schema›Table |
| Federación de vistas Iceberg (2026) | buenas prácticas: referencias totalmente cualificadas, representaciones duales, test en cada motor; propuesta de dialecto "ANSI" | Portabilidad = cualificar + declarar, no = «escribir SQL estándar y confiar» |
| Substrait | IR de planes; DataFusion produce y consume, Velox consume, Gluten serializa el plan físico de Spark | El escalón siguiente, cuando el motor deje de ser uno |
2-bis · Cotejo con Cloudflare, Databricks, Snowflake y Google
Medido en documentación oficial el 2026-07-31. Siete ejes; en cada uno, qué hacen ellos, qué hacemos nosotros, y el veredicto.
| Eje | Cloudflare (R2 SQL) | Databricks | Snowflake | Google (BigQuery) | Carbon |
|---|---|---|---|---|---|
| Motor | DataFusion (comprado, no construido) | Spark + Photon (C++ vectorizado) | propio | propio | DuckDB (comprado) ✅ |
| Dialecto | el de DataFusion | ANSI ON por defecto (Spark 4 / DBR 17+) | Snowflake SQL | GoogleSQL, ANSI; Legacy SQL se retira | sin declarar ❌ |
| Nº de gramáticas | 1 | 1 (ANTLR) | 1 | 1 | 2 que deben coincidir ❌ |
| Superficie | deliberadamente estrecha y documentada | completa | completa | completa | estrecha, sin documentar ⚠️ |
| Formato de cable | Arrow IPC sobre gRPC | Arrow | Arrow | Arrow | Arrow IPC ✅ |
| Quién contesta la metadata | el catálogo | Unity Catalog (por catálogo, filtrado por privilegios) | el catálogo | BigLake Metastore | Index… salvo un hilo ⚠️ |
| Escritura sobre catálogo AJENO | — | foreign Iceberg = read-only | external-catalog Iceberg = read-only | — | catálogo propio ⇒ escribimos ✅ |
Dónde coincidimos (y no por casualidad)
El motor se compra. Cloudflare —que tiene toda la ingeniería del mundo para
escribir el suyo— construyó R2 SQL sobre Apache DataFusion. Nosotros sobre DuckDB.
Lo que ellos construyeron es lo que de verdad les distingue: el planificador que
poda con las estadísticas de Iceberg, y el reparto del trabajo. La lección no es
«usa DataFusion», es dónde ponen su ingeniería propia: en el plano de control y
la planificación, no en el ejecutor. Es exactamente donde está Junction.
Arrow es el cable. R2 SQL devuelve resultados por gRPC serializados en Arrow
IPC. Nosotros ya servimos Arrow IPC como formato canónico de duck-server. Coincidencia
plena, y sin habérnoslo propuesto.
La gobernanza vive en el catálogo, no en el motor. Unity Catalog sirve el
INFORMATION_SCHEMA por catálogo y filtrado por privilegios. Es nuestro diseño
(Index contesta, el motor queda ciego) — con un hilo suelto que la spec cierra en F4:
buildInfoSchemaRows todavía le pregunta al motor longitudes y precisiones.
Quien no es dueño del catálogo, no escribe. Databricks marca las foreign Iceberg
tables como read-only; Snowflake, las de catálogo externo, igual. Es la misma
disciplina que ya aplicamos con el perímetro de escritura (warehouse-writer como
único escritor). Nosotros somos dueños de nuestro Lakekeeper, así que escribimos —
pero la regla de que la capacidad depende de quién posee el catálogo es la que
sostiene el perímetro, y conviene tenerla escrita.
Dónde nos apartamos — y sólo una de las tres es defendible
① Nadie tiene dos gramáticas. Nosotros sí. Cloudflare hereda la de DataFusion, Trino y Spark tienen una ANTLR, Snowflake y Google la suya. Todos: exactamente una. Nosotros tenemos dos heurísticas de regex, en dos lenguajes, decidiendo si algo es lectura o escritura. Esto no es una elección de diseño distinta: es la única divergencia sin precedente en el sector, y es la que resuelve C3.
② Todos nombran su dialecto; nosotros no. «GoogleSQL». «Snowflake SQL». «ANSI por defecto». Y Google no sólo nombra el suyo: está retirando el otro — Legacy SQL deja de estar disponible tras el 1 de junio de 2026 para quien no lo use. Un dialecto es algo que se declara, se versiona y se retira. Nosotros decimos «paridad Databricks», que es una aspiración de superficie, no un contrato. Lo resuelve C2.
③ Nuestra superficie estrecha es correcta — pero por accidente. Aquí está el matiz más útil del cotejo, y es a favor de Cloudflare. R2 SQL también tiene una superficie deliberadamente incompleta: ordena sólo por columnas de la clave de partición, va bien en filtros y las agregaciones complejas aún no. Y lo publican. Estrechar la superficie no es una vergüenza que ocultar: es una forma legítima de enviar antes, siempre que esté declarada.
Lo nuestro no está declarado, y es peor que eso. El SQL Editor admite sólo DELETE y
MERGE, y el propio código dice por qué ya no debería:
«el MOTIVO de esta restricción ha desaparecido — se rechazaban por preservar una
identidad de fila sintética que ya no existe […] La matriz de verbos está PENDIENTE
DE RE-MEDIR» —lib/compute/junction.ts:1295
La de Cloudflare es una decisión; la nuestra es un resto. Esa es la diferencia real, y no se arregla ampliando la superficie a lo loco: se arregla midiendo la matriz de verbos y publicando el resultado, como hacen ellos. Va a F5.
Un eje donde la industria valida algo que ya elegimos
Databricks activó ANSI por defecto y con ello cambió el comportamiento ante
entradas inválidas: lanza excepción en vez de devolver null. Es la misma tesis
que ya gobierna nuestra puerta —fail-loud sobre fail-silent— aplicada al dialecto.
Cuando C2 fije duckdb como dialecto de registro, la pregunta hermana es qué hacemos
ante desbordamiento, división por cero y casts imposibles. Que el motor sea estricto
es una propiedad del contrato, no un detalle de configuración, y conviene fijarla
en la misma decisión.
3 · La medición que decide el diseño
Los candidatos de parser para Node, comprobados:
| Opción | Estado real | Veredicto |
|---|---|---|
sqlglot-ts | v0.1.5, 5 releases, npm 2026-03 | La forma correcta, DuckDB cubierto — pero demasiado joven para ser el eje |
node-sql-parser | v5.4.0, 180 releases, maduro | No tiene dialecto DuckDB (PostgreSQL es aproximación, no equivalencia) |
parser del propio DuckDB (json_serialize_sql) | medido abajo | Divergencia CERO con el ejecutor, por construcción |
Medido en DuckDB 1.5.4 (C:\tmp\parser-probe.py), la cobertura real:
SELECT · CTE+JOIN · FQN citado 3-partes · SHOW ALL TABLES · DESCRIBE → PARSEA (SELECT_NODE)
EXPLAIN · DELETE · MERGE · INSERT · UPDATE · CTAS · CREATE VIEW
ALTER RENAME · WITH…INSERT · ATTACH · COPY → "Only SELECT statements…"
Dos consecuencias, y son las que arman la spec:
① Es un oráculo de lectura/escritura infalsificable. Si json_serialize_sql
acepta, es un SELECT — lo dice la gramática del motor que va a ejecutarlo. Si
rechaza, no lo es. WITH … INSERT y ATTACH/COPY caen del lado correcto sin
una sola expresión regular. Eso cierra por construcción la clase de bypass
write-as-read que el red-team encontró 63 veces.
② Permite reescribir por AST, no por sustitución de texto. Verificado el bucle completo:
SELECT a, SUM(b) FROM ventas WHERE a > 1 GROUP BY a
→ json_serialize_sql → AST → reescribir nodos BASE_TABLE → json_deserialize_sql →
SELECT a, sum(b) FROM lake."main.default".ds_38b26211… WHERE (a > 1) GROUP BY a
El motor descubre las tablas (no las adivina un escáner) y cita la identidad
física él mismo. rewriteDottedFromRefs, scanTableRefs, extractCteNames y
replaceTableToken dejan de tener trabajo.
4 · Los tres contratos
C1 · FORMATO — adoptar la forma del Iceberg View Spec para las vistas
Iceberg v2 + Parquet + Arrow se quedan como están. Lo que cambia es cómo se persiste un SQL. Toda vista (y todo artefacto SQL guardado: la definición de un paso de pipeline, una expectativa, una consulta programada) lleva, en Index:
| Campo | Origen | Por qué |
|---|---|---|
sql | el cuerpo, verbatim | lo único que hay hoy |
dialect | requerido — "duckdb" | sin esto, cambiar de motor invalida silenciosamente el corpus |
default_catalog | opcional; si falta, el catálogo de la vista | resuelve las referencias sin catálogo |
default_namespace | requerido, lista de niveles (["main","default"]) | resuelve las referencias de un solo identificador — y es multi-nivel de nacimiento, como nuestro main.default |
schema_id | el esquema con el que se creó | detectar deriva de esquema |
version_id | entero, inmutable | un CREATE OR REPLACE crea versión nueva; no machaca |
No inventamos un esquema: adoptamos el del spec, campo por campo. Cuando las vistas migren a objetos-vista de Iceberg de verdad, la migración será un movimiento de sitio, no una traducción. Y la puerta queda abierta a lo que el spec ya permite: varias representaciones de la misma vista, una por dialecto, el día que haya un segundo motor.
Regla de escritura de vistas: el cuerpo se persiste cualificado, no bare. Es la primera recomendación de la guía de federación de 2026, y elimina la dependencia del contexto de sesión.
C2 · DIALECTO — declarado, y uno
Nuestro dialecto de registro es duckdb. No «SQL estándar» (no lo es, y decir
que lo es es la mentira que hace perder tardes), no «paridad Databricks» (eso es una
aspiración de superficie, no un contrato).
Se separa explícitamente en dos gramáticas, porque tienen dueños distintos:
| Gramática de catálogo | Gramática de query | |
|---|---|---|
| Qué | SHOW …, DESCRIBE …, ALTER TABLE … RENAME, CREATE/DROP VIEW | todo lo demás |
| Tamaño | cerrada, ~6 comandos | abierta, la de DuckDB |
| Dueño | nuestro | del motor |
| Quién la reconoce | un reconocedor explícito, versionado y testeado — no un dialecto | el parser del motor |
| Quién la contesta | Index | el motor |
Que la de catálogo sea nuestra es una decisión, no una deuda: es pequeña, cerrada y de forma Databricks a propósito (es lo que el usuario espera teclear). Lo que no vale es que se confunda con la otra ni que crezca.
C3 · PARSER — una autoridad, un verificador que falla cerrado
SQL del usuario
│
├─ ¿casa la gramática de CATÁLOGO (cerrada)? ──sí──▶ INDEX (nunca llega al motor)
│
└─ no
│
├─ pelar EXPLAIN (caso cerrado y conocido)
├─ json_serialize_sql ──acepta──▶ ES LECTURA
│ └─ AST → PASE DE RESOLUCIÓN contra Index
│ · descubre tablas (nodos BASE_TABLE)
│ · autoriza cada una (tenencia, purpose, RLS)
│ · reescribe a identidad física
│ · expande vistas (con SU dialecto y SU default_namespace)
│ → json_deserialize_sql → SQL cualificado
│
└─ rechaza ──▶ NO ES LECTURA
└─ gramática de ESCRITURA (cerrada: DELETE | MERGE)
· destino DECLARADO fuera de banda (ya es así hoy)
· autorizado contra Index
Y el cambio de rol que resuelve el problema de los dos clasificadores:
Node deja de mandar SQL a secas: manda SQL + INTENCIÓN DECLARADA (
read|
write+ destino). duck-server deja de ser una segunda autoridad semántica y
pasa a ser un verificador que falla cerrado: comprueba con su propia heurística
gruesa que lo recibido concuerda con la intención declarada y, ante cualquier
discrepancia, rechaza.
Eso convierte «dos parsers que deben coincidir» —una carrera que se pierde tarde o
temprano— en «una autoridad + un guardián que se cierra cuando no entiende». La
defensa en profundidad de F4.2 (write_target impuesto server-side) se mantiene
intacta: sigue siendo el cerrojo que no depende de que Node acierte.
5 · La sinergia con Index (el tercer pilar)
Todo lo anterior converge en que Index es el analizador, y es lo que ya sostiene el resto de la arquitectura:
- Index alimenta el pase de resolución. El mapa que necesita —
{catalog: {schema: {table: {col: type}}}}, la forma exacta que consume elqualifyde sqlglot — es literalmente lo que Index ya guarda (coordenada + schema SSOT). No hay que construir un catálogo nuevo: hay que exponer el que existe con esa forma. - Index contesta
information_schemaySHOW/DESCRIBE, como Unity Catalog: por catálogo y filtrado por privilegios. Esto cierra el hilo suelto que queda hoy —buildInfoSchemaRowsle pregunta al motor (duckColumns) longitudes y precisiones que el catálogo conoce. - Index posee el dialecto y la procedencia de cada SQL guardado (C1). Es donde ya viven el tier, el linaje y la gobernanza: el dialecto es metadato del mismo tipo — lo que el catálogo sabe y el motor no puede saber.
- El motor sigue ciego. Recibe SQL cualificado e intención declarada. Nunca un nombre de usuario, nunca una coordenada lógica, nunca una decisión de política.
6 · Fases
- F0 · Oráculo de lectura (inerte, medible). Cablear
json_serialize_sqlcomo clasificador en sombra: se ejecuta junto aclassifyForRouting, se LOGuea el diff y no decide nada. Cierra la pregunta «¿cuántas veces discrepan hoy?» con datos antes de tocar el camino real. Prerrequisito: exponer el serializador por duck-server (oduckdben proceso en Node) — decisión de §7. - F1 · El oráculo pasa a decidir.
classifyForRoutingse retira. Node declara la intención; duck-server verifica y falla cerrado ante discrepancia. - F2 · Resolución por AST. El pase de resolución sustituye a
rewriteDottedFromRefsscanTableRefs+extractCteNames+replaceTableToken. Gate: paridad diferencial contra el plan actual sobre el corpus de queries reales — el mismo patrón de harness que ya usamos (f3-equivalence,f1-differential-parity).
- F3 · Contrato de vistas (C1). Columnas nuevas en Index; escritura cualificada;
versiones inmutables. Backfill: las vistas existentes se re-cualifican con el
default_namespaceque tuvieran al crearse — y las que no resuelvan se marcan, no se adivinan. - F4 ·
information_schemaySHOW/DESCRIBE100% desde Index. Se cortaduckColumns. - F5 · Re-medir la matriz de capacidades contra DuckDB (hoy marcada como no
verificada en
docs/warehouse-sql/), y completar los docs con la estructura real — que es lo que este pilar produce. - Diferido · Substrait. Cuando haya un segundo motor, el plan viaja como IR y el dialecto deja de ser el contrato de portabilidad. No antes: hoy sería complejidad sin consumidor.
7 · Decisiones del owner (antes de implementar)
-
✅ RATIFICADA (2026-07-31) · Dónde vive el parser: en duck-server, y en la MISMA llamada que ejecuta.
Las tres opciones que se barajaban cayeron por medición. El suelo de un salto Node→duck-server es 221 ms de mediana (7 muestras), y una lectura real de
main.testson 236 ms: el salto ES el coste; el trabajo de la query es ruido. Luego un endpoint/parseseparado —la recomendación previa— son dos saltos: ~450 ms donde hoy hay ~236. Duplicar la latencia del editor para ganar corrección no es un intercambio aceptable.duckdben proceso en Node mantiene dos binarios en sincronía;sqlglot-ts(v0.1.5) no es el parser del ejecutor.La forma adoptada — parsear, resolver y ejecutar en UNA llamada. Node manda: SQL lógico · la rebanada de catálogo ya autorizada (nombre → identidad física, que
buildWarehouseCatalogya construye entera hoy) · la intención declarada. duck-server parsea con su propio parser, resuelve estructuralmente contra esa rebanada y ejecuta.Propiedad Cómo queda Latencia un salto, igual que hoy Divergencia parser↔ejecutor inexistente: hay un solo DuckDB Tablas no autorizadas rechazo duro por construcción — hoy un escáner de regex puede no ver una tabla y dejarla pasar Tamaño de la rebanada ~143 tablas del mayor workspace ≈ 11 KB La objeción, y por qué se acepta: contradice literalmente el principio «el motor recibe identidades ya resueltas». No lo contradice en lo que importa — el motor no decide: aplica una lista blanca que Node ya decidió. La política se toma aguas arriba; lo que baja es su resultado. El cerrojo de tenencia en escritura (
write_target, F4.2) queda intacto.Consecuencia en el plan: M2 y M3 se funden en un solo milestone.
-
SHOW/DESCRIBEparsean como SELECT en DuckDB (medido). ¿Los sigue interceptando Index antes del oráculo —recomendado, son preguntas de catálogo— o se deja que el motor conteste algunas? Afecta a qué ve el usuario en un warehouse multi-tenant. -
Alcance del contrato de dialecto (C1). ¿Sólo vistas, o todo SQL persistido (pasos de pipeline, expectativas, consultas guardadas)? Cuanto más tarde se extienda, mayor el backfill.
-
La gramática de catálogo, ¿se congela? Proponemos cerrarla en los ~6 comandos actuales y que crecer requiera decisión explícita. Sin eso vuelve a ser un dialecto por acumulación.
-
Pineado de DuckDB y extensiones. Toda la propiedad «un solo parser» descansa en que el parser y el ejecutor sean la misma versión. Hoy las extensiones se rebajan con
install_mode=REPOSITORYen cada arranque. Es prerrequisito, no higiene. -
✅ RATIFICADA (2026-07-31) · Estrictez: los defaults de DuckDB SON el contrato — pero se declaran explícitamente.
Medido sobre DuckDB 1.5.4, no supuesto:
Caso Comportamiento por defecto ¿ANSI? Desbordamiento de entero lanza OutOfRangeException✅ Cast 'abc'::INTEGERlanza ConversionException✅ Cast estrechante 300::TINYINTlanza ✅ Fecha inválida '2026-02-30'lanza ✅ Casting implícito desde VARCHAR ya desactivado ( old_implicit_casting=false)✅ División por cero inf(float) ·NULL(entera)❌ DuckDB ya es ANSI-estricto exactamente donde Spark 4 rompió compatibilidad. No hay nada que activar. Y para el único hueco no existe palanca: con
ieee_floating_point_ops=false,1/0pasa deinfaNULLpero sigue sin lanzar. Hacerlo ANSI exigiría reescribir el SQL del usuario — justo la clase de cosa que este pilar existe para no hacer.Lo adoptado:
- Los defaults de DuckDB son la estrictez de Carbon SQL. No se pelea con el motor.
- La divergencia se PUBLICA (división por cero →
NULL/inf, no error). Principio Cloudflare: una limitación declarada es una decisión; una callada es un resto. - ⭐ Se fijan EXPLÍCITAMENTE en
build_engineaunque coincidan con el default. Sin declararlas, nuestra estrictez es lo que DuckDB decida y puede moverse entre versiones sin que nadie toque el repo. Es exactamente el fallo deSUPPORT_NESTED_NAMESPACES, que costó una tarde: un default ajeno gobernando nuestro contrato. errors_as_json=true. No es estrictez: es otra rueda reinventada. Hoyparse_duck_errorextrae el número de línea con una regex sobre el texto; el motor entregaexception_type,error_subtype,positionycandidates(sugerencias «¿quisiste decir?») estructurados.
-
¿Publicamos la superficie? (sale del cotejo, §2-bis). Cloudflare documenta los límites de R2 SQL. Recomendado: publicar los nuestros —verbos admitidos, funciones de tabla, lo no soportado— en
docs/warehouse-sql/. Es el entregable natural de F5.
Fuentes
- Iceberg View Spec ·
format/view-spec.md - The 2026 Guide to Iceberg View Federation
- Information schema | Databricks
- sqlglot · optimizer/qualify · qualify_columns
- SQL to/from JSON – DuckDB · discusión #22528 (DDL no soportado)
- trino-parser (ANTLR4)
- sqlglot-ts · node-sql-parser
- Powered by Substrait
- Cotejo de industria (§2-bis): R2 SQL: deep dive into our new distributed query engine · Cloudflare Data Platform · ANSI compliance in Databricks Runtime · Introducing Apache Spark 4.0 · Work with foreign tables | Databricks · Apache Iceberg tables | Snowflake · Legacy SQL feature availability | BigQuery · BigLake tables for Apache Iceberg in BigQuery
- Mediciones propias (DuckDB 1.5.4):
C:\tmp\parser-probe.py— cobertura del serializador y bucle completo AST → reescritura → SQL.