Esquema de cada comando soportado con railroad diagram (mermaid), gramática EBNF, ejemplos válidos y errores típicos. Equivalente al syntax diagrams de SQLite o al SQL command reference de PostgreSQL — pero acotado al subset que
gabysqlya entrega.Para el inventario exhaustivo de lo que NO está implementado todavía (comandos faltantes, prioridades, bloques de implementación sugeridos), ver MISSING_COMMANDS.md.
Para el detalle del formato en disco que respalda esta gramática, ver TECHNICAL_SPECS.md. Para el AST en código, src/sql.rs.
📊 ¿No ves los railroad diagrams? Este documento usa 17 bloques mermaid + 1 en ARCHITECTURE.md. Si los ves como código en lugar de gráfico, tu renderer no soporta mermaid. Soluciones:
- GitHub web: los renderiza nativamente desde 2022. Si no se ven, refrescá hard (Ctrl+F5).
- VS Code: instalá la extensión
bierner.markdown-mermaid— sin ella el Markdown Preview no los pinta.- Obsidian / Typora / Joplin: lo soportan nativamente.
- GitHub mobile: NO los renderiza — abrir en navegador desktop.
- PDF export: requiere
mermaid-cli(npm i -g @mermaid-js/mermaid-cli) o usar Pandoc + filtro.Los 17 bloques de este archivo + el de ARCHITECTURE.md fueron validados sintácticamente el 2026-05-29 — si alguno no renderiza es porque el renderer cortó por límite de nodos (raro), no porque el código mermaid esté roto.
🧭 Índice de comandos
| Comando | Categoría | Estado |
|---|---|---|
CREATE DATABASE |
DDL · server multi-DB | 🟢 |
DROP DATABASE |
DDL · server multi-DB | 🟢 |
SHOW DATABASES |
DDL · server multi-DB | 🟢 |
INTEGRITY CHECK |
Operacional | 🟢 |
CREATE TABLE |
DDL | 🟢 |
DROP TABLE |
DDL | 🟢 |
ALTER TABLE ADD COLUMN |
DDL | 🟢 |
CREATE TABLE AS SELECT / RENAME TABLE / ALTER TABLE DROP/RENAME COLUMN |
DDL (K1) | 🟢 |
CREATE INDEX |
DDL | 🟢 |
DROP INDEX |
DDL | 🟢 |
INSERT |
DML | 🟢 |
SELECT |
DML | 🟢 |
UPDATE (WHERE completo desde bloque E3 — multi-fila, indexado, subquery) |
DML | 🟢 |
DELETE (WHERE completo desde bloque E3 — multi-fila, indexado, subquery) |
DML | 🟢 |
WHERE col IN (SELECT …) (no-correlacionada, single-column) |
DML | 🟢 |
WHERE col = (SELECT …) (subquery escalar no-correlacionada) |
DML | 🟢 |
WHERE [NOT] EXISTS (SELECT …) (no-correlacionada y correlacionada single-eq) |
DML | 🟢 |
WHERE con AND/OR/NOT + paréntesis y 3VL para NULL (bloque E1) |
DML | 🟢 |
WHERE con <, >, <=, >=, <>/!=, [NOT] LIKE (con %/_), IS [NOT] NULL, [NOT] IN (lista) (bloque E2) |
DML | 🟢 |
Agregaciones: COUNT(*), COUNT(col), COUNT(DISTINCT col), SUM, AVG, MIN, MAX, GROUP BY, HAVING, DISTINCT (bloque F) |
DML | 🟢 (sin JOINs aún) |
Transacciones explícitas: BEGIN/START TRANSACTION, COMMIT/END, ROLLBACK (bloque T) |
TCL | 🟢 (batch-local + SAVEPOINT/ROLLBACK TO/RELEASE desde M12, ADR-0089) |
Cross-request transactions HTTP via X-Gabysql-Session header + endpoints /tx/{begin,commit,rollback} (M13) |
TCL | 🟢 (single-slot global; ver API.md) |
Multi-row INSERT VALUES (...), (...), INSERT INTO t SELECT ..., TRUNCATE [TABLE] (bloque J) |
DML | 🟢 |
UPSERT (INSERT ... ON CONFLICT DO NOTHING / DO UPDATE SET ...), REPLACE INTO, RETURNING (bloque J2) |
DML | 🟢 (sin EXCLUDED.col) |
INNER JOIN ... ON l = r, CROSS JOIN, comma-syntax, aliases (AS), multi-tabla chain, self-join |
DML | 🟢 |
LEFT [OUTER] JOIN, RIGHT [OUTER] JOIN, FULL [OUTER] JOIN con NULL-fill |
DML | 🟢 |
JOIN ... USING (col), NATURAL JOIN con SELECT * dedup |
DML | 🟢 |
| Index-loop join optimization (transparente: aplica auto cuando hay índice/PK) | DML | 🟢 |
PK compuesta (PRIMARY KEY (a, b, ...)) — all-INT NOT NULL |
DDL | 🟢 (K2, VERSION 8) |
Índices compuestos (CREATE [UNIQUE] INDEX idx ON t (a, b, ...)) — all-INT, equality-only |
DDL | 🟢 (K2, VERSION 8) |
FK referential actions completas (ON DELETE / ON UPDATE con RESTRICT / CASCADE / SET NULL / SET DEFAULT / NO ACTION) |
DDL+DML | 🟢 (L1/VERSION 9 + residual #4) |
CHECK (expr) column-level y table-level, ALTER TABLE ADD [CONSTRAINT n] CHECK (expr) |
DDL | 🟢 (L2/VERSION 10 + L3) |
Nombres de constraint (CONSTRAINT <name> en PK/UNIQUE/FK/CHECK) + ALTER TABLE DROP CONSTRAINT [IF EXISTS] <name> |
DDL | 🟢 (residual #2/VERSION 11) |
FK multi-col (FOREIGN KEY (a, b) REFERENCES p (x, y)) |
DDL | 🟢 (residual #3/VERSION 12) |
UPDATE de PK con re-encode + cascade ON UPDATE (regular UPDATE; UPSERT DO UPDATE sigue restringido) |
DML | 🟢 (residual #4) |
CREATE VIEW [IF NOT EXISTS] v [(col_aliases)] AS SELECT ... / DROP VIEW [IF EXISTS] v, SELECT ... FROM v |
DDL+DML | 🟢 (bloque V/VERSION 13) |
WITH name AS (SELECT ...) (CTEs no-recursivas) — encadenables, en JOIN/subquery/set-ops, shadowing ANSI |
DML | 🟢 (bloque W1) |
WITH RECURSIVE name AS (anchor UNION [ALL] step) <body> — fixpoint base+step, delta semantics, guards de runaway |
DML | 🟢 (bloque W2) |
Window functions — ROW_NUMBER/RANK/DENSE_RANK/NTILE/LAG/LEAD/FIRST_VALUE/LAST_VALUE/SUM/COUNT/AVG/MIN/MAX con OVER (PARTITION BY ... ORDER BY ...) |
DML | 🟢 (bloque W3) |
CREATE TRIGGER name {BEFORE\|AFTER} {INSERT\|UPDATE\|DELETE} ON t FOR EACH ROW <body> + DROP TRIGGER — body single-stmt o BEGIN stmt; stmt; END |
DDL | 🟢 (bloques X1+X2 / VERSION 14) |
CREATE PROCEDURE name(p1 TYPE, ...) AS <body> + DROP PROCEDURE + CALL name(args) |
DDL+DML | 🟢 (bloque X3 / VERSION 15) |
CREATE FUNCTION name(p1 TYPE, ...) RETURNS TYPE AS <expr> + DROP FUNCTION, invocable como name(args) en cualquier expresión |
DDL+DML | 🟢 (bloque X3b / VERSION 16) |
IF expr THEN ... [ELSIF ...]* [ELSE ...] END IF — statement top-level + dentro de bodies; anidado |
TCL | 🟢 (bloque X4) |
DECLARE + SET + WHILE LOOP + EXIT [WHEN] — variables locales con scope plano; vars visibles en Expr (no en INSERT VALUES) |
TCL | 🟢 (bloque X4b) |
RAISE [EXCEPTION\|NOTICE] 'msg' + FOR i IN start TO end LOOP ... END LOOP — aborto explícito + range loop con auto-decl |
TCL | 🟢 (bloque X4c) |
BEGIN <body> [EXCEPTION WHEN OTHERS THEN <handler>] END + LOOP <body> END LOOP standalone — try/catch catch-all + loop infinite hasta EXIT |
TCL | 🟢 (bloque X4d) |
CASE WHEN cond THEN ... [ELSE ...] END CASE + EXCEPTION WHEN <code> THEN ... — CASE statement-level + filtros por código en EXCEPTION + OTHERS fallback |
TCL | 🟢 (bloque X4e) |
CREATE FUNCTION ... AS BEGIN ... RETURN expr; END — function bodies multi-statement con RETURN como sentinel (compat single-expr body de X3b) |
DDL | 🟢 (bloque X4f) |
NEW mutable en BEFORE, EXCEPTION WHEN <name> simbólico, FOR row IN SELECT, RETURN expr, RETURNS TABLE, body de function como SELECT, partial indexes, frame specs explícitas, WINDOW w AS (...), múltiples CTEs RECURSIVE |
— | 🔴 (ver MISSING_COMMANDS) |
🔤 Identificadores
Tablas, columnas e índices comparten una sola regla. Definida en catalog::validate_identifier.
- Forma léxica:
[A-Za-z_][A-Za-z0-9_]* - Longitud máxima: 64 caracteres (
MAX_IDENT_LEN) - Case-insensitive en comparación, case-preserving en almacenamiento
- No puede ser una palabra reservada del parser
Palabras reservadas (case-insensitive): add, alter, and, between, bool, column, create, database, databases, date, datetime, default, delete, drop, exists, false, float, from, if, index, insert, int, into, json, key, limit, not, null, offset, on, primary, select, set, show, table, text, true, unique, update, values, where.
Errores típicos:
| Mensaje | Causa |
|---|---|
nombre de tabla 'X' es palabra reservada |
el nombre coincide con una keyword del parser |
nombre de columna 'X' inválido: debe empezar con letra o '_' |
empieza con dígito o símbolo |
nombre de índice 'X' inválido: solo se admiten [A-Za-z0-9_] |
tiene caracteres no permitidos (guion, espacio, etc.) |
nombre de tabla 'X' excede el máximo de 64 caracteres |
identificador demasiado largo |
🧱 Tipos de dato soportados
flowchart LR
INT["INT<br/>i64 little-endian"]
TEXT["TEXT<br/>UTF-8 bytes"]
BOOL["BOOL<br/>0 ó 1"]
FLOAT["FLOAT<br/>f64 little-endian"]
DATE["DATE<br/>texto ISO-8601"]
DATETIME["DATETIME<br/>texto ISO-8601"]
TIME["TIME<br/>texto HH:MM:SS[.fff]"]
UUID["UUID<br/>texto 8-4-4-4-12 hex"]
JSON["JSON<br/>texto, no indexable"]
NULL["NULL<br/>solo en columnas no PK"]
| Tipo canónico | Almacenamiento | Indexable | Aliases sintácticos (bloque Y) | Notas |
|---|---|---|---|---|
INT |
8 bytes LE | ✅ | INTEGER, INT2, INT4, INT8, BIGINT, SMALLINT, TINYINT, MEDIUMINT |
Único tipo válido como PK. Sin enforcement de rango para SMALLINT/TINYINT (alias puro). |
TEXT |
bytes UTF-8 | ✅ | VARCHAR[(n)], CHAR[(n)], CHARACTER[(n)], CHARACTER VARYING[(n)], NVARCHAR[(n)], NCHAR[(n)], STRING, CLOB |
Hasta 65 535 bytes globalmente. Si la columna se declara con (n), el n se enforce en bytes UTF-8 desde el bloque Y2 — [GBY-4119] si se excede. |
BOOL |
1 byte | ✅ | BOOLEAN |
TRUE / FALSE |
FLOAT |
8 bytes LE (f64) |
✅ | REAL, DOUBLE, DOUBLE PRECISION, NUMERIC[(p,s)], DECIMAL[(p,s)], DEC[(p,s)] |
DECIMAL/NUMERIC son aliases — no son decimal exacto. |
DATE |
texto | ✅ | — | YYYY-MM-DD, validación lexical (no semántica). |
DATETIME |
texto | ✅ | TIMESTAMP |
YYYY-MM-DD HH:MM:SS, validación lexical. |
TIME (bloque Y) |
texto | ✅ | — | HH:MM:SS o HH:MM:SS.fff. Validación lexical en CAST. No timezone. |
UUID (bloque Y) |
texto | ✅ | — | Canónico 8-4-4-4-12 hex. CAST AS UUID normaliza a lowercase. |
JSON |
texto | ❌ | — | Sin semántica de igualdad canónica |
NULL |
tag de presencia | n/a | — | No admitido en columnas PK |
No soportado todavía:
BLOB/BYTEA/BINARY(binario crudo),DECIMALexacto,ARRAY[T],ENUM(...),INTERVAL,TIME WITH TIME ZONE,TIMESTAMP WITH TIME ZONE. Diferidos a Y2.
CREATE DATABASE
Solo en modo server multi-DB (
-dir) o CLI con un directorio. Crea un archivo.dbaplicando el formatoVERSION = 33(bump P4 por column stats persistidas, ver ADR-0068). En modo single-DB (-db) responde405.
🛤️ Railroad
flowchart LR
S([▶]) --> C[CREATE] --> D[DATABASE]
D --> IFNE{IF NOT EXISTS?}
IFNE -- "no" --> N[/db_name/]
IFNE -- "sí" --> IF[IF] --> NOT[NOT] --> EX[EXISTS] --> N
N --> SEMI[";"] --> E([■])
📜 EBNF
create_database ::= "CREATE" "DATABASE" ("IF" "NOT" "EXISTS")? identifier
✅ Ejemplos
CREATE DATABASE shop;
CREATE DATABASE IF NOT EXISTS analytics;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
base de datos 'X' ya existe |
falta IF NOT EXISTS |
CREATE/DROP/SHOW DATABASE requieren modo -dir |
el server fue arrancado con -db |
nombre de DB inválido: solo [A-Za-z0-9_-] |
identificador con caracteres prohibidos |
DROP DATABASE
Elimina el archivo
.dby su.walsi quedó. Acción irreversible, sin respaldo.
🛤️ Railroad
flowchart LR
S([▶]) --> D[DROP] --> DB[DATABASE]
DB --> IFE{IF EXISTS?}
IFE -- "no" --> N[/db_name/]
IFE -- "sí" --> IF[IF] --> EX[EXISTS] --> N
N --> SEMI[";"] --> E([■])
📜 EBNF
drop_database ::= "DROP" "DATABASE" ("IF" "EXISTS")? identifier
✅ Ejemplos
DROP DATABASE analytics;
DROP DATABASE IF EXISTS legacy;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
base de datos 'X' no existe |
falta IF EXISTS y la DB no estaba |
SHOW DATABASES
🛤️ Railroad
flowchart LR
S([▶]) --> SH[SHOW] --> DBS[DATABASES] --> SEMI[";"] --> E([■])
📜 EBNF
show_databases ::= "SHOW" "DATABASES"
✅ Resultado
Devuelve una ResultSet con una columna database, un row por DB ordenado alfabéticamente:
{ "columns": ["database"], "rows": [["analytics"], ["shop"]], "message": null }
CREATE TABLE
🛤️ Railroad diagram
flowchart LR
Start([▶]) --> CREATE[CREATE]
CREATE --> TABLE[TABLE]
TABLE --> Name[/identifier/]
Name --> POPEN["("]
POPEN --> Col[ColumnDef]
Col --> COMMA{","}
COMMA -- "sí" --> Col
COMMA -- "no" --> PCLOSE[")"]
PCLOSE --> SEMI[";"]
SEMI --> End([■])
flowchart LR
S([ColumnDef]) --> N[/identifier/]
N --> T[/type/]
T --> CST{constraint?}
CST -- "no" --> E([fin])
CST -- "PRIMARY KEY" --> CST
CST -- "NOT NULL" --> CST
CST -- "UNIQUE" --> CST
CST -- "DEFAULT lit" --> CST
📜 EBNF
create_table ::= "CREATE" "TABLE" identifier "(" table_item ("," table_item)* ")"
table_item ::= column_def | table_constraint
column_def ::= identifier type column_constraint*
column_constraint ::= [ "CONSTRAINT" identifier ]
( "PRIMARY" "KEY"
| "NOT" "NULL"
| "UNIQUE"
| "DEFAULT" default_value
| "CHECK" "(" expr ")"
| "REFERENCES" identifier "(" identifier ")" fk_action* )
table_constraint ::= [ "CONSTRAINT" identifier ]
( "PRIMARY" "KEY" "(" identifier { "," identifier } ")"
| "UNIQUE" "(" identifier { "," identifier } ")"
| "CHECK" "(" expr ")"
| "FOREIGN" "KEY" "(" identifier { "," identifier } ")"
"REFERENCES" identifier "(" identifier { "," identifier } ")" fk_action* )
fk_action ::= ("ON" "DELETE" | "ON" "UPDATE")
("RESTRICT" | "CASCADE" | "SET" "NULL" | "SET" "DEFAULT" | "NO" "ACTION")
type ::= "INT" | "TEXT" | "BOOL" | "FLOAT" | "DATE" | "DATETIME" | "JSON"
literal ::= integer | float | string | "TRUE" | "FALSE" | "NULL"
default_value ::= literal | default_fn
default_fn ::= ("gen_random_uuid" | "uuid_v4" | "uuid_generate_v4" | "random_uuid"
| "uuid_v7" | "uuid_generate_v7" | "gen_uuid_v7"
| "current_timestamp" | "now") "(" ")"
identifier ::= [A-Za-z_][A-Za-z0-9_]*
Notas:
PRIMARY KEYimplicaNOT NULL. Sigue habiendo una sola PK por tabla (puede ser compuestaPRIMARY KEY (a, b, ...)con K2/VERSION 8, todas INT NOT NULL).UNIQUEinline auto-genera un índice unique con nombreuq_<tabla>_<col>(verCREATE UNIQUE INDEX).UNIQUE (a, b, ...)table-level reusa el encoder K2.DEFAULT NULLes válido pero incompatible conNOT NULLen la misma columna.DEFAULTno se admite sobre la PK.- El literal de
DEFAULTdebe coincidir con el tipo de la columna;name TEXT DEFAULT 1se rechaza enCREATE TABLE. DEFAULT <fn>()(N5, 2026-05-30): además de literales,DEFAULTacepta una llamada sin argumentos a una función pura del whitelist:gen_random_uuid(alias:uuid_v4,uuid_generate_v4,random_uuid),uuid_v7(alias:uuid_generate_v7,gen_uuid_v7),current_timestamp(alias:now). La función se re-evalúa por fila en cada INSERT. Funciones fuera del whitelist se rechazan enCREATE TABLE. Ver ADR-0066 Gap 6.CHECK (expr)(L2/VERSION 10) admite cualquier expresión Expr (booleana 3VL ANSI). Se evalúa en INSERT/UPDATE/UPSERT/cascade. Subqueries dentro deCHECKse rechazan con[GBY-4069]. Persistencia como texto canónico víaformat_expr+ reparse en cada open.[GBY-3008] CHECK_VIOLATEDcuando falla.REFERENCES <tabla>(<col>): el target column debe ser la PK del parent (no se admiten FKs contraUNIQUEno-PK). El tipo de la FK debe coincidir con el de la PK referenciada. Self-references válidas. Acciones referenciales completas enON DELETEyON UPDATE(L1/V9 + residual #4):RESTRICT(default),CASCADE,SET NULL,SET DEFAULT,NO ACTION. FK multi-col (residual #3/V12) via constraint table-levelFOREIGN KEY (a, b) REFERENCES p (x, y)— target debe ser la PK compuesta del parent, lookup O(log n) por fingerprint K2.CONSTRAINT <name>(residual #2/V11): nombre opcional para PK/UNIQUE/FK/CHECK; útil conALTER TABLE DROP CONSTRAINT <name>después.
✅ Ejemplos válidos
CREATE TABLE users (
id INT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
name TEXT,
active BOOL DEFAULT TRUE,
score FLOAT,
status TEXT NOT NULL DEFAULT 'pending',
born DATE,
meta JSON
);
CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT REFERENCES users(id) ON DELETE CASCADE,
total FLOAT,
tries INT DEFAULT 0
);
-- Self-reference: cada empleado puede tener un manager (que también es empleado).
CREATE TABLE employee (
id INT PRIMARY KEY,
name TEXT NOT NULL,
manager_id INT REFERENCES employee(id)
);
❌ Errores típicos
| Mensaje | Causa |
|---|---|
PRIMARY KEY 'pk' debe ser INT (...) |
la columna marcada como PK no es INT |
PRIMARY KEY requerida (...) |
no se declaró ninguna columna como PK |
PRIMARY KEY 'pk' no admite DEFAULT en esta versión |
DEFAULT aplicado sobre la PK |
columna 'X': NOT NULL incompatible con DEFAULT NULL |
combinación contradictoria de constraints |
columna 'X': DEFAULT incompatible con tipo TEXT |
el literal de DEFAULT no coincide con el tipo declarado |
FOREIGN KEY 'X.col' referencia tabla inexistente 'Y' |
el target table no existe (ni es self-ref) |
FOREIGN KEY 'X.col' debe referenciar la PK de 'Y' (es 'pk_real'); esta versión no admite REFERENCES contra columnas no-PK |
target column no es la PK del parent |
FOREIGN KEY 'X.col' debe ser INT para coincidir con la PK de 'Y' |
tipo del FK no matchea el tipo de la PK referenciada |
nombre de columna duplicado |
dos columnas con el mismo nombre |
tabla X ya existe |
hay otra tabla con ese nombre en el catálogo |
tipo no soportado: BIGINT |
tipos fuera de la lista anterior |
CREATE INDEX
🛤️ Railroad
flowchart LR
S([▶]) --> C[CREATE] --> I[INDEX] --> N[/index_name/]
N --> ON[ON] --> T[/table_name/]
T --> POPEN["("] --> COL[/column_name/] --> PCLOSE[")"] --> SEMI[";"] --> E([■])
📜 EBNF
create_index ::= "CREATE" "UNIQUE"? "INDEX" identifier "ON" identifier "(" identifier ")"
✅ Ejemplos
-- Crear índice (backfill automático sobre las filas ya existentes)
CREATE INDEX idx_users_name ON users (name);
CREATE INDEX idx_orders_status ON orders (status);
-- Índice único: el backfill aborta si existen duplicados; en caliente
-- INSERT/UPDATE conflictivos se rechazan antes de tocar disco.
CREATE UNIQUE INDEX uq_users_email ON users (email);
❌ Errores típicos
| Mensaje | Causa |
|---|---|
ya existe un índice llamado 'X' en la tabla 'Y' |
el nombre del índice se repite (debe ser único en toda la DB) |
la columna 'X' ya tiene un índice secundario |
esta versión soporta solo un índice por columna |
no se admiten índices sobre columnas JSON en esta versión |
JSON no es indexable (ver tabla de tipos) |
columna no existe: X |
la columna no aparece en el CREATE TABLE |
CREATE UNIQUE INDEX rechazado: columna 'X' tiene valores duplicados existentes |
la tabla ya tenía duplicados al pedir el índice unique |
violación de UNIQUE en índice 'uq_t_c' (PK existente: N) |
INSERT/UPDATE intenta colocar un valor ya presente en otra fila |
Reglas: una sola columna por índice, solo equality (
=).UNIQUEpermite múltiplesNULL. Ver ADR-0005.
DROP INDEX
🛤️ Railroad
flowchart LR
S([▶]) --> D[DROP] --> I[INDEX] --> N[/index_name/] --> SEMI[";"] --> E([■])
📜 EBNF
drop_index ::= "DROP" "INDEX" identifier
✅ Ejemplos
DROP INDEX idx_users_name;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
índice no existe: X |
no hay un índice con ese nombre en ninguna tabla |
DROP INDEXno libera las páginas del B+Tree del índice. La reclamación queda para una futura herramientavacuum(ver STATUS.md).
DROP TABLE
Borra la entrada de la tabla en el catálogo. Las páginas backing (data + índices secundarios de esa tabla) no se liberan; el espacio se reclama con un futuro
vacuum.
🛤️ Railroad
flowchart LR
S([▶]) --> D[DROP] --> T[TABLE]
T --> IFE{IF EXISTS?}
IFE -- "no" --> N[/table_name/]
IFE -- "sí" --> IF[IF] --> EX[EXISTS] --> N
N --> SEMI[";"] --> E([■])
📜 EBNF
drop_table ::= "DROP" "TABLE" ("IF" "EXISTS")? identifier
✅ Ejemplos
DROP TABLE users;
DROP TABLE IF EXISTS scratch;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
tabla no existe: X |
no hay tabla con ese nombre y no se usó IF EXISTS |
ALTER TABLE ADD COLUMN
Agrega una columna al final del esquema. Las filas previas se decodifican con su
DEFAULT(oNULL) sin reescritura — el rewrite ocurre naturalmente cuando unUPDATEtoca esa fila.
🛤️ Railroad
flowchart LR
S([▶]) --> A[ALTER] --> T[TABLE] --> N[/table_name/]
N --> ADD[ADD] --> COL{COLUMN?} --> CD[ColumnDef]
CD --> SEMI[";"] --> E([■])
📜 EBNF
alter_table_add ::= "ALTER" "TABLE" identifier "ADD" "COLUMN"? column_def
column_def ::= identifier type column_constraint*
✅ Ejemplos
ALTER TABLE users ADD COLUMN nick TEXT;
ALTER TABLE users ADD COLUMN status TEXT NOT NULL DEFAULT 'pending';
ALTER TABLE users ADD COLUMN email TEXT UNIQUE;
ALTER TABLE users ADD score FLOAT DEFAULT 0; -- COLUMN es opcional
❌ Errores típicos
| Mensaje | Causa |
|---|---|
tabla no existe: X |
la tabla a alterar no está en el catálogo |
columna 'X' ya existe en la tabla 'Y' |
nombre repetido |
ALTER TABLE ADD COLUMN no admite PRIMARY KEY (la PK ya existe) |
esta versión no permite swap ni multi-PK |
ALTER TABLE ADD COLUMN 'X' NOT NULL requiere un DEFAULT no nulo (...) |
sin DEFAULT, las filas previas violarían la constraint |
ALTER TABLE ADD COLUMN 'X' UNIQUE con DEFAULT no nulo produciría duplicados en N filas existentes |
el backfill insertaría el mismo valor en todas las filas |
columna 'X': DEFAULT incompatible con tipo TEXT |
mismo validador de tipos que CREATE TABLE |
Restricciones: para
DROP COLUMN,RENAME COLUMNyRENAME TABLEver la sección DDL extendido (K1). PK compuesta + índices compuestos cerrados en K2 (VERSION 8, ver ADR-0019).ALTER ... TYPEy ALTER PK siguen pendientes.
DDL extendido (K1)
Bloque K1 (2026-05-26). Cuatro sentencias DDL adicionales que no cambian el formato en disco (VERSION sigue en 7).
CREATE TABLE [IF NOT EXISTS] [(col_aliases)] AS SELECT
Materializa el resultado de una SelectQuery (SELECT, set ops o VALUES) como una tabla nueva. La primera columna del result-set debe ser INT no-NULL y se promueve a PRIMARY KEY.
ctas ::= "CREATE" "TABLE" ("IF" "NOT" "EXISTS")? identifier
("(" identifier ("," identifier)* ")")?
"AS" select_query ";"
CREATE TABLE activos AS SELECT id, nombre FROM usuarios WHERE id > 0;
CREATE TABLE IF NOT EXISTS dst (pk, label) AS SELECT id, nombre FROM src;
CREATE TABLE lit (id, label) AS VALUES (1, 'a'), (2, 'b');
CREATE TABLE merged AS SELECT id, nombre FROM a UNION SELECT id, nombre FROM b;
Errores típicos: [GBY-4058] primera columna no INT no-NULL · [GBY-4063] arity de aliases ≠ arity del SELECT · [GBY-2004] el destino ya existe (sin IF NOT EXISTS).
RENAME TABLE / ALTER TABLE ... RENAME TO
rename_table ::= "RENAME" "TABLE" identifier "TO" identifier ";"
| "ALTER" "TABLE" identifier "RENAME" "TO" identifier ";"
RENAME TABLE old TO new;
ALTER TABLE old RENAME TO new;
Las FKs entrantes (otras tablas que referencien old) se reescriben automáticamente al nuevo nombre. Errores: [GBY-4062] destino tomado · [GBY-2001] origen no existe.
ALTER TABLE ... DROP COLUMN [IF EXISTS]
alter_drop ::= "ALTER" "TABLE" identifier "DROP" "COLUMN"
("IF" "EXISTS")? identifier ";"
ALTER TABLE users DROP COLUMN nick;
ALTER TABLE users DROP COLUMN IF EXISTS deprecated_flag;
Bloqueado sobre la PK ([GBY-4059]), columnas indexadas ([GBY-4060], sugiere DROP INDEX <name> primero) y columnas con FK saliente o entrante ([GBY-4061]). Implementación: rewrite in place de cada fila (decode + remove + encode + upsert).
ALTER TABLE ... RENAME COLUMN
alter_rename_col ::= "ALTER" "TABLE" identifier "RENAME" "COLUMN"
identifier "TO" identifier ";"
ALTER TABLE users RENAME COLUMN nick TO handle;
ALTER TABLE pedidos RENAME COLUMN id TO pedido_id; -- arrastra PK + FKs entrantes
No reescribe filas (el on-disk row es posicional). Si la columna es la PK, TableMeta.primary_key se actualiza; si está indexada, IndexMeta.column se actualiza; las FKs entrantes que referencien la columna se reescriben. Errores: [GBY-4062] destino tomado · [GBY-2002] origen no existe.
ALTER TABLE ADD CHECK (L3) / DROP CONSTRAINT (residual #2)
L3 (2026-05-27): agrega un
CHECKdespués delCREATE TABLE. Hace full-scan de la tabla y revalida todas las filas existentes contra la expresión — si alguna viola, la sentencia se aborta sin persistir el constraint. Costo O(n) en filas, una sola vez.Residual #2 (2026-05-27): elimina por nombre cualquier constraint nombrado (CHECK / UNIQUE / FK). PK siempre rechazada (
[GBY-4072]).
📜 EBNF
alter_add_check ::= "ALTER" "TABLE" identifier "ADD"
[ "CONSTRAINT" identifier ] "CHECK" "(" expr ")" ";"
alter_drop_constraint ::= "ALTER" "TABLE" identifier "DROP" "CONSTRAINT"
[ "IF" "EXISTS" ] identifier ";"
✅ Ejemplos
-- Agregar CHECK con re-validación O(n)
ALTER TABLE products ADD CHECK (price >= 0);
ALTER TABLE products ADD CONSTRAINT chk_stock_positivo CHECK (stock >= 0);
-- Quitar constraints nombradas
ALTER TABLE products DROP CONSTRAINT chk_stock_positivo;
ALTER TABLE orders DROP CONSTRAINT IF EXISTS fk_orders_user;
ALTER TABLE users DROP CONSTRAINT uq_users_email;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-3008] CHECK_VIOLATED (en ALTER TABLE ADD CHECK) |
alguna fila existente viola el predicado — sentencia abortada |
[GBY-4069] subquery dentro de CHECK |
el AST de CHECK no admite subqueries en esta versión |
[GBY-4071] CONSTRAINT_NOT_FOUND |
nombre no existe (omite con IF EXISTS) |
[GBY-4072] CANNOT_DROP_PRIMARY_KEY_CONSTRAINT |
intento de drop sobre la PK |
CREATE VIEW / DROP VIEW (bloque V)
Bloque V (2026-05-27, VERSION 13). Vistas lógicas read-only sobre el SELECT que las definió. La vista guarda únicamente el
source_sql+ alias de columnas; en cada query se expande dentro del FROM como una derived table. Tablas y vistas comparten namespace.
📜 EBNF
create_view ::= "CREATE" "VIEW" [ "IF" "NOT" "EXISTS" ] identifier
[ "(" identifier ("," identifier)* ")" ]
"AS" select_stmt ";"
drop_view ::= "DROP" "VIEW" [ "IF" "EXISTS" ] identifier ";"
✅ Ejemplos
CREATE VIEW activos AS
SELECT id, nombre, email FROM users WHERE active = TRUE;
SELECT * FROM activos ORDER BY nombre;
CREATE VIEW IF NOT EXISTS resumen (cliente, total)
AS SELECT customer_id, SUM(total) FROM orders GROUP BY customer_id;
-- DML sobre vistas no se permite
-- INSERT INTO activos VALUES (...); -- [GBY-4075] VIEWS_ARE_READONLY
-- Vistas anidadas (la fuente puede referenciar otra vista; cycle guard depth=32)
CREATE VIEW vips AS SELECT * FROM activos WHERE id IN (SELECT user_id FROM gold_members);
DROP VIEW IF EXISTS resumen;
⚠️ Reglas
- Read-only:
INSERT/UPDATE/DELETE/TRUNCATEsobre una vista devuelven[GBY-4075]. - SELECT simple: la fuente debe ser un
SELECTplano. Set ops (UNION/INTERSECT/EXCEPT) oVALUEScomo fuente devuelven[GBY-4078]. - Cycle guard: expansión limitada a
MAX_VIEW_DEPTH = 32. Ciclos o cadenas muy profundas devuelven[GBY-4076] VIEW_DEPTH_EXCEEDED. - Namespace compartido: una vista con el nombre de una tabla existente (o viceversa) devuelve
[GBY-4077] NAME_ALREADY_EXISTS. - Los
column_aliases(si se declaran) deben tener la misma arity que el SELECT.
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4075] VIEWS_ARE_READONLY |
DML sobre vista |
[GBY-4076] VIEW_DEPTH_EXCEEDED |
recursión / ciclo entre vistas más allá de 32 niveles |
[GBY-4077] NAME_ALREADY_EXISTS |
colisión de nombre tabla ↔ vista |
[GBY-4078] VIEW_SOURCE_NOT_SIMPLE_SELECT |
la fuente no es un SELECT simple |
INSERT
🛤️ Railroad
flowchart LR
S([▶]) --> I["INSERT (o REPLACE)"] --> INTO[INTO] --> T[/table/]
T --> COLS["( col_list )"]
COLS --> SRC{insert_source}
SRC -- "VALUES" --> ROWS["(vals), (vals), ..."]
SRC -- "SELECT" --> SEL[SELECT body]
ROWS --> OC{ON CONFLICT?}
SEL --> OC
OC -- "DO NOTHING" --> RET
OC -- "DO UPDATE SET ..." --> RET
OC -- "(REPLACE)" --> RET
OC --> RET{RETURNING?}
RET -- "* / cols" --> SEMI[";"]
RET --> SEMI
SEMI --> E([■])
📜 EBNF
insert ::= ("INSERT" | "REPLACE") "INTO" identifier "(" col_list ")"
insert_source
on_conflict_clause?
returning_clause?
insert_source ::= "VALUES" "(" value_list ")" ("," "(" value_list ")")*
| "SELECT" select_body
on_conflict_clause
::= "ON" "CONFLICT" ( "(" identifier ")" )? "DO" conflict_action
conflict_action
::= "NOTHING"
| "UPDATE" "SET" assignment ("," assignment)*
assignment ::= identifier "=" value
returning_clause
::= "RETURNING" ( "*" | identifier ("," identifier)* )
col_list ::= identifier ("," identifier)*
value_list ::= value ("," value)*
value ::= integer | float | string | "TRUE" | "FALSE" | "NULL"
string ::= "'" ([^'] | "''")* "'"
REPLACE INTO ... VALUES (...) se desugara internamente a
INSERT ... ON CONFLICT DO REPLACE — la cláusula ON CONFLICT explícita
no se acepta si la sentencia empezó con REPLACE. El target opcional
(col) solo se admite si col es PK o tiene índice UNIQUE ([GBY-4032]).
En DO UPDATE SET, los valores de la derecha son literales — EXCLUDED.col
no se soporta en este release.
Desde el bloque J (2026-05-25) el INSERT admite tres formas:
- Single-row:
INSERT INTO t (cols) VALUES (a, b, c); - Multi-row:
INSERT INTO t (cols) VALUES (a, b), (c, d), (e, f); - Por subquery:
INSERT INTO t (cols) SELECT ... FROM ...;— elSELECTpuede usar cualquier feature del SELECT (WHERE/JOIN/GROUP BY/ORDER BY). Se materializa primero, después se insertan filas en orden.
El message del response trae la cuenta: "OK (3 filas insertadas)".
✅ Ejemplos
-- Single-row (compat pre-J)
INSERT INTO users (id, name, active, score) VALUES (1, 'Ana', TRUE, 9.5);
INSERT INTO products (id, name, price) VALUES (10, 'Café o''rgánico', 4500.50);
-- Multi-row (bloque J)
INSERT INTO users (id, name, active) VALUES
(2, 'Beto', FALSE),
(3, 'Carla', TRUE),
(4, 'Dario', TRUE);
-- INSERT...SELECT (bloque J)
INSERT INTO users_backup (id, name, active)
SELECT id, name, active FROM users WHERE active = TRUE;
-- Con agregados del bloque F
INSERT INTO sales_summary (region, total)
SELECT region, SUM(monto) FROM ventas GROUP BY region;
-- UPSERT (bloque J2): ON CONFLICT DO NOTHING
INSERT INTO users (id, email) VALUES (1, 'a@x')
ON CONFLICT DO NOTHING;
-- UPSERT: ON CONFLICT DO UPDATE (sin EXCLUDED.col por ahora — RHS literal)
INSERT INTO users (id, name) VALUES (1, 'Ana M')
ON CONFLICT (id) DO UPDATE SET name = 'Ana M';
-- REPLACE INTO (SQLite-style): borra la fila conflictiva + inserta
REPLACE INTO users (id, name) VALUES (1, 'Anna');
-- RETURNING (bloque J2): la respuesta trae las filas afectadas
INSERT INTO orders (id, total) VALUES (10, 199.50) RETURNING id;
UPDATE products SET on_sale = TRUE WHERE stock > 0 AND price < 50 RETURNING id, price;
DELETE FROM sessions WHERE last_seen < '2024-01-01' RETURNING user_id;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
cantidad columnas != valores |
la lista de columnas y la de valores tienen distinto largo |
columna duplicada en INSERT |
se nombra dos veces la misma columna |
columna no existe: X |
la columna no está en el schema |
duplicate primary key: N |
la PK ya está usada — usa otra o haz UPDATE |
PRIMARY KEY no puede ser NULL |
se intentó pasar NULL para la PK |
<col> debe ser INT (o FLOAT, etc.) |
tipo del valor no encaja con el de la columna |
Mantenimiento: tras el insert, todos los índices secundarios de la tabla se actualizan automáticamente.
SELECT
🛤️ Railroad
flowchart LR
S([▶]) --> SEL[SELECT]
SEL --> DIST{DISTINCT?}
DIST --> COLS{select_list}
COLS -- "*" --> FROM
COLS -- "items" --> ITEMS[col, agg AS alias, ...] --> FROM[FROM]
FROM --> T[/table/]
T --> JOIN{JOINs?}
JOIN --> WH{WHERE?}
WH --> GB{GROUP BY?}
GB --> HAV{HAVING?}
HAV --> OB{ORDER BY?}
OB --> LIM{LIMIT?}
LIM --> OFF{OFFSET?}
OFF --> SEMI[";"] --> E([■])
flowchart LR
A([where_clause]) --> OR{OR}
OR --> AND{AND}
AND --> NOT{NOT?}
NOT -- "NOT" --> NOT
NOT --> PRIM{primary}
PRIM -- "(...)" --> A
PRIM -- "EXISTS (SELECT)" --> EX([EXISTS])
PRIM --> ATOM[atom]
ATOM --> COL[/column/]
COL --> OP{operador}
OP -- "=" --> VEQ["value, (SELECT), o ref outer"]
OP -- "< > <= >= <> !=" --> VCMP["value"]
OP -- "BETWEEN" --> VBT["int AND int"]
OP -- "IS [NOT] NULL" --> VNULL([NULL])
OP -- "[NOT] LIKE" --> VLIKE["'patron' (con % _ y \\)"]
OP -- "[NOT] IN" --> VIN["lista o (SELECT)"]
📜 EBNF
select ::= "SELECT" ["DISTINCT"] select_list "FROM" identifier
("WHERE" where_clause)?
("GROUP" "BY" identifier ("," identifier)*)?
("HAVING" where_clause)?
("ORDER" "BY" identifier ("ASC" | "DESC")?)?
("LIMIT" integer)?
("OFFSET" integer)?
select_list ::= "*" | select_item ("," select_item)*
select_item ::= identifier
| agg_func "(" agg_arg ")" ["AS" identifier | identifier]
agg_func ::= "COUNT" | "SUM" | "AVG" | "MIN" | "MAX"
agg_arg ::= "*" | "DISTINCT" identifier | identifier
where_clause ::= where_or
where_or ::= where_and ( "OR" where_and )*
where_and ::= where_not ( "AND" where_not )*
where_not ::= "NOT" where_not
| where_primary
where_primary ::= "(" where_or ")"
| "EXISTS" "(" select ")"
| "NOT" "EXISTS" "(" select ")"
| where_atom
where_atom ::= identifier "=" ( value | "(" select ")" | qualified_ident )
| identifier compare_op value
| identifier "BETWEEN" integer "AND" integer
| identifier "IS" ["NOT"] "NULL"
| identifier ["NOT"] "LIKE" string
| identifier ["NOT"] "IN" "(" value_list ")"
| identifier "IN" "(" select ")"
compare_op ::= "<" | "<=" | ">" | ">=" | "<>" | "!="
value_list ::= value ("," value)*
qualified_ident ::= identifier ( "." identifier )?
Precedencia (de más baja a más alta): OR < AND < NOT < paréntesis / átomo.
Es la convención estándar SQL: a OR b AND c se interpreta como a OR (b AND c).
Los paréntesis fuerzan agrupaciones distintas.
Lógica trivaluada (3VL) — el WHERE evalúa con la tabla de verdad de SQL estándar para NULL:
NULL AND false→false;NULL AND true→NULL;NULL AND NULL→NULLNULL OR true→true;NULL OR false→NULL;NULL OR NULL→NULLNOT NULL→NULL- Una fila sobrevive el filtro solo si la expresión evalúa a
true;falseyNULL(unknown) la descartan.
Limitación E1: EXISTS correlacionado y col = otra.col (column-ref del
outer) solo se permiten como único átomo del WHERE. Combinarlos con
AND/OR/NOT devuelve [GBY-4024] — soporte completo queda para un
bloque posterior. Subqueries no-correlacionadas (IN (SELECT), = (SELECT),
EXISTS no-correlacionado) sí se pueden combinar libremente.
🔗 FROM con JOINs (bloque A del roadmap)
from_clause ::= table_ref join_clause*
table_ref ::= identifier [ ["AS"] identifier ]
join_clause ::= ( "," | "CROSS" "JOIN" ) table_ref
| ( "INNER" "JOIN" | "JOIN" ) table_ref "ON" qualified_ident "=" qualified_ident
| ( "LEFT" ["OUTER"] "JOIN" ) table_ref "ON" qualified_ident "=" qualified_ident
| ( "RIGHT" ["OUTER"] "JOIN" ) table_ref "ON" qualified_ident "=" qualified_ident
| ( "FULL" ["OUTER"] "JOIN" ) table_ref "ON" qualified_ident "=" qualified_ident
| ( "INNER" | "LEFT" ["OUTER"] | "RIGHT" ["OUTER"] | "FULL" ["OUTER"] | ε ) "JOIN" table_ref "USING" "(" identifier ")"
| "NATURAL" ( "INNER" | "LEFT" ["OUTER"] | "RIGHT" ["OUTER"] | "FULL" ["OUTER"] | ε ) "JOIN" table_ref
Reglas:
INNER JOIN(oJOINsolo, equivalente ANSI) requiereON l = rcon un único equi-predicado.AND/ORy operadores no-equi (<,>,BETWEEN, etc.) en elONsiguen pendientes — workaround: filtrarlos en elWHEREpost-JOIN.CROSS JOIN(y la comma-syntaxFROM a, b) NO admiteON. Producto cartesiano completo.LEFT [OUTER] JOIN: preserva todas las filas del lado izquierdo. Cuando no hay match, las columnas del lado derecho aparecen comoNULL.RIGHT [OUTER] JOIN: simétrico — preserva todas las del derecho, NULL-fill en el izquierdo.FULL [OUTER] JOIN: combina ambos comportamientos (toda fila de cualquier lado aparece, con NULL en el otro lado si no hay match).- El
OUTERes opcional (estándar SQL):LEFT JOINyLEFT OUTER JOINson sinónimos. JOIN ... USING (col)es sugar paraJOIN ... ON l.col = r.col. La columnacolaparece una sola vez enSELECT *(ANSI). En este release soporta exactamente UNA columna en la lista (multi-col en backlog).NATURAL JOINderiva automáticamente unUSINGcon la columna que ambas tablas comparten por nombre. Si las tablas comparten 0 o >1 columnas comunes en este release →[GBY-4023].- Las tablas se pueden aliasar con
[AS] alias. El alias oculta el nombre real (estándar SQL): si declarásFROM alumnos a, después tenés que usara.nombre, noalumnos.nombre. - En SELECT/WHERE/ORDER BY, una columna que existe en >1 tabla debe ir cualificada (
tabla.col); si no,[GBY-4018]. SELECT *en JOIN expande a TODAS las columnas de TODAS las tablas, cada una prefijada con su qualifier para evitar colisiones.
Complejidad:
- Nested-loop puro (fallback):
O(N1 × N2 × … × Nk). Se usa cuando elONno apunta contra PK ni índice del right, o cuando el JOIN es CROSS/RIGHT/FULL. - Index-loop (optimización transparente):
O(N1 × log N2)por JOIN. Se activa automáticamente cuando se cumplen las 3 condiciones: (a) elON(o el USING/NATURAL derivado) referencia la PK o una columna indexada del right; (b) el tipo de JOIN esINNERoLEFT; (c) hay un predicate (no aplica aCROSS). El engine elige el path por sí mismo — no hace falta cambiar el SQL.
Sobre
qualified_identen el RHS del=: solo es válido dentro de una subquery correlacionada dentro deEXISTS (...). Permite expresarWHERE inner_col = outer_table.outer_col, dondeouter_tablees la tabla del SELECT padre. Usarlo fuera de ese contexto devuelve[GBY-4016].Sobre
EXISTS: la subquery se ejecuta una sola vez si no referencia columnas del outer; cuando sí lo hace, se re-ejecuta una vez por cada fila del outer (post-filter). Esta variante correlacionada es O(N × costo_subquery), sin optimizer; tiene sentido cuando la subquery se reduce vía PK/índice con la outer-ref.
=funciona sobre la PK o sobre cualquier columna que tenga índice secundario.BETWEENfunciona sobre la PK y sobre cualquier columnaINTcon índice secundario (índiceOrderedInt, default automático al crear índice sobreINT; ver ADR-0017); paraTEXT/FLOAT/BOOL/DATE/DATETIMEindexados,BETWEENqueda en el Camino A.IN (SELECT …)acepta subqueries no-correlacionadas (la subquery no referencia columnas del outer); la subquery debe devolver exactamente una columna, se ejecuta una sola vez y el resultado se materializa como set para filtrar el outer — la columna del outer debe ser la PK o tener índice secundario, igual que=.ORDER BYfunciona sobre cualquier columna del schema (no requiere índice); el sort es en memoria post-scan, así que para tablas grandes conLIMITchico conviene tener unWHEREque reduzca el conjunto antes del sort.
✅ Ejemplos
SELECT * FROM users;
SELECT id, name FROM users LIMIT 10;
SELECT id, name FROM users LIMIT 10 OFFSET 20;
-- Por PK
SELECT * FROM users WHERE id = 1;
SELECT id, name FROM users WHERE id BETWEEN 1 AND 100;
-- Por columna indexada (requiere CREATE INDEX previo)
SELECT * FROM users WHERE name = 'Ana';
SELECT id FROM orders WHERE status = 'pending' LIMIT 50;
-- BETWEEN sobre columna INT indexada (índice OrderedInt, ADR-0017)
-- CREATE INDEX idx_users_score ON users (score); -- score INT
SELECT id, name FROM users WHERE score BETWEEN 80 AND 100 LIMIT 25;
-- Operadores E2: <, >, <=, >=, <>, !=, LIKE, IS NULL, IN literal
SELECT id FROM users WHERE score < 50;
SELECT id FROM users WHERE score >= 80 AND score <= 100;
SELECT id FROM users WHERE name <> 'Ana';
SELECT id FROM users WHERE name LIKE 'A%'; -- empieza con 'A'
SELECT id FROM users WHERE name LIKE '_eto'; -- 4 chars, termina en 'eto'
SELECT id FROM users WHERE description NOT LIKE '%spam%';
SELECT id FROM users WHERE deleted_at IS NULL;
SELECT id FROM users WHERE id IN (1, 2, 3);
SELECT id FROM users WHERE country NOT IN ('AR', 'BR');
SELECT id FROM products WHERE code LIKE '50\%%'; -- LIKE literal '%' con escape
-- Agregaciones (bloque F): COUNT/SUM/AVG/MIN/MAX, GROUP BY, HAVING, DISTINCT
SELECT COUNT(*) FROM users;
SELECT COUNT(*) AS total FROM users WHERE active = TRUE;
SELECT COUNT(monto), SUM(monto), AVG(monto) FROM ventas;
SELECT region, SUM(monto) AS total
FROM ventas
GROUP BY region
ORDER BY total DESC;
SELECT region, producto, COUNT(*) AS n
FROM ventas
GROUP BY region, producto
HAVING COUNT(*) > 1;
SELECT DISTINCT category FROM products;
SELECT COUNT(DISTINCT user_id) FROM sessions;
-- Reglas ANSI estrictas:
-- - Toda columna no-agregada en el SELECT debe figurar en GROUP BY ([GBY-4027]).
-- - Las funciones agregadas solo se permiten en SELECT y HAVING, no en WHERE ([GBY-4025]).
-- - Sin GROUP BY pero con agregados → UNA fila global (incluso sobre input vacío: COUNT=0, resto=NULL).
-- - Agregados sobre SELECT con JOIN funcionan desde F2 (2026-05-30, ADR-0066 Gap 1+7).
-- - COUNT(DISTINCT col) sobre JOIN funciona desde R9 (2026-06-15, ADR-0079).
-- AND / OR / NOT + paréntesis (bloque E1)
SELECT id FROM users WHERE active = TRUE AND score BETWEEN 80 AND 100;
SELECT id FROM users WHERE city = 'BA' OR city = 'MDQ';
SELECT id FROM users WHERE NOT status = 'banned';
SELECT id FROM users WHERE (city = 'BA' OR city = 'MDQ') AND active = TRUE;
-- Precedencia estándar: AND ata más fuerte que OR.
SELECT id FROM users WHERE city = 'BA' OR city = 'MDQ' AND active = TRUE;
-- Equivale a: city = 'BA' OR (city = 'MDQ' AND active = TRUE)
-- ORDER BY (cualquier columna; ASC default; NULLs primero)
SELECT id, name FROM users ORDER BY name ASC;
SELECT id, name FROM users ORDER BY score DESC LIMIT 10;
SELECT id FROM orders WHERE status = 'pending' ORDER BY total DESC LIMIT 5 OFFSET 10;
-- IN (SELECT …) — subquery no-correlacionada
-- Requiere: outer.curso_id indexado (o ser PK) e inner.nivel indexado (o ser PK).
SELECT nombre FROM alumnos
WHERE curso_id IN (SELECT id FROM cursos WHERE nivel = '3 Medio');
-- IN sobre PK directa (no requiere índice en el outer):
SELECT id, label FROM t WHERE id IN (SELECT ref_id FROM picks);
-- = (SELECT …) — subquery escalar (1 columna × ≤1 fila).
-- Si la subquery devuelve 0 filas o NULL, el match es vacío (semántica ANSI).
-- Si devuelve >1 fila, error [GBY-4014] — usar IN (...) en su lugar.
SELECT nombre FROM alumnos
WHERE curso_id = (SELECT id FROM cursos WHERE nombre = 'matematica');
-- EXISTS no-correlacionada: la subquery se ejecuta UNA vez.
SELECT id FROM padre WHERE EXISTS (SELECT id FROM auditoria WHERE id = 1);
-- EXISTS correlacionada: re-ejecuta por cada fila del outer.
-- Padres que tienen al menos un hijo:
SELECT id, nombre FROM padre
WHERE EXISTS (SELECT id FROM hijo WHERE parent_id = padre.id);
-- NOT EXISTS correlacionado: padres sin hijos.
SELECT id, nombre FROM padre
WHERE NOT EXISTS (SELECT id FROM hijo WHERE parent_id = padre.id);
-- INNER JOIN clásico con aliases y columnas cualificadas
SELECT a.nombre, c.nombre FROM alumnos a
INNER JOIN cursos c ON a.curso_id = c.id
ORDER BY a.nombre ASC;
-- JOIN de 3 tablas en cadena (left-deep)
SELECT persona.nombre, ciudad.nombre, pais.nombre
FROM persona
JOIN ciudad ON persona.ciudad_id = ciudad.id
JOIN pais ON ciudad.pais_id = pais.id;
-- CROSS JOIN explícito (cartesian product)
SELECT a.v, b.w FROM a CROSS JOIN b;
-- Comma-syntax = CROSS JOIN
SELECT a.v, b.w FROM a, b;
-- Self-join vía aliases distintos
SELECT e.nombre, j.nombre FROM empleado e
INNER JOIN empleado j ON e.jefe_id = j.id;
-- WHERE sobre columna cualificada de cualquier tabla
SELECT alumnos.nombre FROM alumnos
JOIN cursos ON alumnos.curso_id = cursos.id
WHERE cursos.nivel = '3M';
-- LEFT JOIN: padres sin hijos aparecen con etiqueta NULL
SELECT padre.nombre, hijo.etiqueta FROM padre
LEFT JOIN hijo ON padre.id = hijo.parent_id
ORDER BY padre.id ASC;
-- RIGHT JOIN: filas del derecho sin match aparecen con columnas del izq en NULL
SELECT a.v, b.w FROM a
RIGHT JOIN b ON a.id = b.a_id;
-- FULL OUTER JOIN: combina LEFT + RIGHT en un solo paso
SELECT a.v, b.w FROM a
FULL OUTER JOIN b ON a.id = b.a_id;
-- USING (col) — sugar para ON l.col = r.col; SELECT * dedup la columna
SELECT ciudad.nombre, pais.nombre_pais
FROM ciudad JOIN pais USING (pais_id);
-- NATURAL JOIN — auto-detecta la columna común por nombre
SELECT ciudad.nombre, pais.nombre_pais
FROM ciudad NATURAL JOIN pais;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
tabla no existe: X |
la tabla no está creada en la DB |
ORDER BY: columna 'X' no existe en 'Y' |
la columna del ORDER BY no está en el schema de la tabla |
WHERE solo soporta PK (X) o columnas con índice secundario; 'Y' no está indexada [GBY-4001] |
aplica al fast-path indexado de SELECT (= o BETWEEN sobre columna sin índice). El WHERE compuesto con AND/OR/NOT/</>/LIKE/IS NULL/IN literal cae a FullScan y no exige índice. |
WHERE: no se reconoció el operador después de la columna 'X' [GBY-4001] |
operador fuera de la gramática actual del WHERE. Lista soportada: =, <, >, <=, >=, <>/!=, BETWEEN, IS [NOT] NULL, [NOT] LIKE, [NOT] IN (lista | SELECT), EXISTS. |
subquery en IN debe devolver exactamente 1 columna; devolvió N |
la subquery proyectó más de una columna — reescribila con una sola |
subquery escalar debe devolver exactamente 1 columna; devolvió N |
igual que el anterior pero en = (SELECT ...) |
subquery escalar en WHERE devolvió N filas; debe devolver a lo sumo 1 |
la subquery escalar matcheó más de una fila — agregar WHERE/LIMIT 1 o usar IN (SELECT ...) |
WHERE IN solo soporta PK (X) o columnas con índice secundario; 'Y' no está indexada [GBY-4013] |
aplica solo cuando el WHERE es col IN (SELECT) como único átomo (fast-path). El WHERE compuesto (col IN (SELECT) AND ...) cae a FullScan + 3VL y no exige índice. |
EXISTS requiere '(SELECT ...)' a continuación [GBY-4015] |
EXISTS no seguido por un paréntesis abriendo un SELECT |
outer column 'X.Y' fuera de alcance [GBY-4016] |
col = outer.col usado fuera de un EXISTS (...) correlacionado, o la tabla outer no coincide con la del outer-stack |
PRIMARY KEY 'X' es INT; valor incompatible en WHERE |
pasaste un string a una PK INT |
UPDATE
🛤️ Railroad
flowchart LR
S([▶]) --> U[UPDATE] --> T[/table/]
T --> SET[SET] --> A[col = value]
A --> COMMA{","}
COMMA -- "sí" --> A
COMMA -- "no" --> WH[WHERE]
WH --> WC[where_clause] --> RET{RETURNING?}
RET -- "* / cols" --> SEMI[";"]
RET --> SEMI
SEMI --> E([■])
📜 EBNF
update ::= "UPDATE" identifier "SET" assignment ("," assignment)*
"WHERE" where_clause
returning_clause?
assignment ::= identifier "=" value
where_clause y returning_clause son los mismos definidos en SELECT
y INSERT respectivamente — ver esas secciones.
Desde el bloque E3 el WHERE de UPDATE acepta exactamente la misma
gramática que SELECT: combinadores AND/OR/NOT, paréntesis, todos los
operadores E1+E2 (=, <, >, <=, >=, <>/!=, LIKE, IS NULL,
IN literal, BETWEEN) y subqueries (IN (SELECT), = (SELECT),
EXISTS). El UPDATE se aplica a todas las filas que el WHERE
matchee; el message del response trae la cuenta.
✅ Ejemplos
-- Por PK directa
UPDATE users SET name = 'Ana M' WHERE id = 1;
-- Multi-asignación
UPDATE orders
SET status = 'paid', total = 199.50
WHERE id = 42;
-- Por columna indexada (afecta a todas las filas matcheadas)
UPDATE users SET active = FALSE WHERE city = 'BA';
-- Por predicado compuesto (E1+E2)
UPDATE products SET on_sale = TRUE
WHERE price < 100 AND stock > 0 AND name LIKE '%demo%';
-- Por subquery
UPDATE users SET status = 'banned'
WHERE id IN (SELECT uid FROM blacklist);
❌ Errores típicos
| Mensaje | Causa |
|---|---|
fila no existe: PK=N |
el WHERE era pk = N y N no está en la tabla. Solo aplica al fast-path de PK literal; un WHERE compuesto con 0 matches devuelve OK con cuenta 0. |
[GBY-4008] UPDATE_PK_NOT_ALLOWED (solo UPSERT DO UPDATE) |
SET pk = ... dentro de ON CONFLICT DO UPDATE. En el UPDATE regular está permitido desde residual #4 (2026-05-27): se hace re-encode + move físico de la fila y cascade ON UPDATE a las hijas. Errores típicos del nuevo path: [GBY-4073] FK_RESTRICT_BLOCKS_UPDATE (hijos con RESTRICT) y [GBY-4074] FK_UPDATE_CASCADE_AFFECTS_CHILD_PK. |
columna duplicada en SET |
dos asignaciones a la misma columna |
Solo los índices cuya columna está en el
SETse tocan; los demás no pagan costo. La resolución del WHERE en E3 hace FullScan + filtro 3VL salvo cuando el WHERE es exactamentepk = N(fast-path por PK). La optimización para= col_indexadayIN (SELECT)queda en backlog (correctitud primero). Ver src/sql.rs:exec_update.
DELETE
🛤️ Railroad
flowchart LR
S([▶]) --> D[DELETE] --> F[FROM] --> T[/table/]
T --> WH[WHERE] --> WC[where_clause]
WC --> RET{RETURNING?}
RET -- "* / cols" --> SEMI[";"]
RET --> SEMI
SEMI --> E([■])
📜 EBNF
delete ::= "DELETE" "FROM" identifier "WHERE" where_clause
returning_clause?
where_clause y returning_clause son los mismos definidos en SELECT
e INSERT respectivamente.
Mismo where_clause que SELECT y UPDATE (bloque E3). Borra todas las
filas matcheadas en orden de PK ascendente; las FK con ON DELETE CASCADE
se aplican fila por fila. El message del response trae la cuenta.
✅ Ejemplos
-- Por PK
DELETE FROM users WHERE id = 5;
-- Por columna indexada (multi-fila)
DELETE FROM logs WHERE level = 'debug';
-- Por subquery
DELETE FROM sessions WHERE user_id IN (SELECT id FROM users WHERE banned = TRUE);
-- Por predicado compuesto
DELETE FROM tickets WHERE status = 'closed' AND updated_at < '2024-01-01';
❌ Errores típicos
| Mensaje | Causa |
|---|---|
fila no existe: PK=N |
el WHERE era pk = N y N no está. Con WHERE compuesto, 0 matches es OK. |
violación de FK: 'X.col' referencia 'Y' (ON DELETE RESTRICT, N fila(s) afectadas) |
hay filas hijas y la FK fue declarada ON DELETE RESTRICT (default) |
Antes de borrar la fila, el engine la lee para evictar la entrada correspondiente de cada índice secundario. Si la tabla tiene FKs entrantes, el motor resuelve cascade/restrict iterativamente con un worklist y cycle protection (visited set sobre
(tabla, pk)). Para tablas grandes con FKs entrantes, se recomienda crear un índice secundario sobre la columna FK del hijo — el engine lo usa automáticamente para que el lookup de hijos sea O(log n) en vez de full scan.
TRUNCATE
Bloque J (2026-05-25). Borra todas las filas de la tabla manteniendo el schema (columnas, índices, FKs). Implementación naive: scan de todas las PKs +
delete_with_cascadepor fila. Respeta las declaracionesON DELETE CASCADE/RESTRICTde FKs entrantes — no es un O(1) hack como en Postgres/MySQL.
📜 EBNF
truncate ::= "TRUNCATE" ["TABLE"] identifier
✅ Ejemplos
TRUNCATE TABLE logs; -- borra todo `logs`
TRUNCATE staging; -- la palabra TABLE es opcional
❌ Errores típicos
| Mensaje | Causa |
|---|---|
tabla no existe: X |
la tabla no está en el catálogo |
violación de FK: ... ON DELETE RESTRICT, N fila(s) afectadas |
hay filas hijas con FK ON DELETE RESTRICT apuntando a la tabla — no se puede vaciar sin borrar primero los hijos |
Transacciones explícitas (BEGIN / COMMIT / ROLLBACK)
Bloque T (2026-05-25). Por defecto cada batch enviado a
/exec(HTTP) o cada invocación degabysql exec(CLI) es una transacción atómica implícita: o se commitean todas las sentencias del batch o ninguna.BEGIN/COMMIT/ROLLBACKpermiten ademas abortar el batch a mitad de camino y obtener feedback explícito por sentencia.
📜 EBNF
tcl ::= "BEGIN" ["TRANSACTION" | "WORK"]
| "START" "TRANSACTION"
| "COMMIT" ["TRANSACTION" | "WORK"]
| "END" ["TRANSACTION" | "WORK"]
| "ROLLBACK" ["TRANSACTION" | "WORK"]
| "ROLLBACK" "TO" ["SAVEPOINT"] ident -- M12
| "SAVEPOINT" ident -- M12
| "RELEASE" ["SAVEPOINT"] ident -- M12
Sinónimos ANSI aceptados: BEGIN = START TRANSACTION. COMMIT = END.
Las palabras TRANSACTION y WORK después de BEGIN/COMMIT/END/ROLLBACK son opcionales.
Las palabras SAVEPOINT después de ROLLBACK TO o RELEASE son opcionales (compat con PostgreSQL).
✅ Ejemplos
BEGIN;
INSERT INTO ledger (id, amount) VALUES (1, 100);
INSERT INTO ledger (id, amount) VALUES (2, -100);
COMMIT;
-- Aborto a mitad de batch:
BEGIN;
UPDATE inventory SET stock = stock - 1 WHERE sku = 'ABC';
-- ... validación adicional falla ...
ROLLBACK;
-- M12: recuperación parcial con SAVEPOINT (ANSI SQL:2003).
BEGIN;
INSERT INTO usuarios (id, nombre) VALUES (1, 'Ana');
SAVEPOINT antes_de_imports;
INSERT INTO usuarios SELECT * FROM csv_dump; -- falla con PK dup
ROLLBACK TO SAVEPOINT antes_de_imports; -- vuelve al checkpoint
-- Ana sigue intacta. Sigo con otro intento.
INSERT INTO usuarios (id, nombre) VALUES (2, 'Beto');
RELEASE SAVEPOINT antes_de_imports; -- libera el slot (no revierte)
COMMIT;
🧠 Semántica SAVEPOINT (M12)
SAVEPOINT namemarca un punto-de-retorno dentro de la tx. Cero efecto sobre la tx en sí; solo bookkeeping (snapshot del cache del Pager).ROLLBACK TO [SAVEPOINT] namedescarta cambios desde el savepoint. El savepoint sigue en la stack — hay queRELEASEpara liberarlo. Cualquier savepoint declarado DESPUÉS denamequeda invalidado.RELEASE [SAVEPOINT] namelibera el slot del savepoint (y los posteriores). NO revierte cambios — los inserts entre savepoint y release permanecen.COMMITyROLLBACKfull limpian toda la stack automáticamente.- Re-
SAVEPOINTcon el mismo nombre marca un nuevo punto sin borrar el anterior (semántica ANSI). [GBY-4140]si SAVEPOINT/ROLLBACK TO/RELEASE se emite sinBEGINactivo.[GBY-4141]si el nombre no existe en la tx actual.
Ver ADR-0089.
⚠️ Limitaciones
ROLLBACKdescarta TODO el cache del Pager: en un batch que mezcla sentencias antes y después deBEGIN, las anteriores también se pierden.BEGIN/ROLLBACKfunciona limpio cuandoBEGINes la primera sentencia del batch. Para revertir parcialmente dentro de la tx, usarSAVEPOINT(M12).No hay transacciones cross-request en el server HTTP✅ entregado por M13 (2026-06-15, ADR-0090). El cliente abre sesión conPOST /tx/begin, envía elsession_iden cada/exec(headerX-Gabysql-Sessiono?session=<id>), y cierra conPOST /tx/commitoPOST /tx/rollback. Ver API.md.✅ implementados desde M12 (2026-06-15, ADR-0089). Sintaxis:SAVEPOINT/ROLLBACK TO SAVEPOINTno implementados (P1).SAVEPOINT name,ROLLBACK TO [SAVEPOINT] name,RELEASE [SAVEPOINT] name. ANSI SQL:2003 completo.SET TRANSACTION ISOLATION LEVEL ...yBEGIN READ ONLYno implementados (P2). gabysql es single-writer estricto — el isolation siempre es serializable de facto; no hay nivel para configurar.
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4029] BEGIN: ya hay una transacción explícita abierta |
dos BEGIN consecutivos sin COMMIT/ROLLBACK intermedio |
[GBY-4030] COMMIT/ROLLBACK: no hay transacción explícita activa |
COMMIT o ROLLBACK sin BEGIN previo |
Funciones escalares (bloques G1 + G2 + G3)
Desde el bloque G1 (2026-05-26), el
SELECTlist acepta expresiones escalares además de columnas crudas y agregadas: funciones built-in (LENGTH,UPPER,CONCAT, …),CAST(x AS TYPE),CASE … END, literales, y los conditionalsCOALESCE/NULLIF/IFNULL/IF. Cada expresión puede recibirAS aliaspara nombrar la columna delResultSet.El bloque G2 (2026-05-26) extiende esas mismas expresiones a las superficies de filtrado y mutación:
WHERE(incluye el WHERE deSELECT,UPDATE,DELETE): cualquierExprque evalúe a BOOL/NULL puede ser un átomo del WHERE. Forma típica:WHERE LENGTH(nombre) > 3,WHERE UPPER(nombre) = 'ANA',WHERE COALESCE(activo, false) = true,WHERE 5 < LENGTH(nombre)(LHS literal, RHS función).HAVING: igual que WHERE, con la libertad ya existente de referir agregados. Ej:HAVING UPPER(grupo) = 'X'.UPDATE SET col = <expr>yON CONFLICT DO UPDATE SET col = <expr>: la RHS puede ser cualquierExpr. Se evalúa contra la fila pre-update, así queSET a = b, b = adeja ambos con los valores intercambiados (las dos RHS ven el snapshot original).El bloque G3 (2026-05-26) cierra la familia:
- Operadores aritméticos binarios
+,-,*,/,%sobre INT/FLOAT con promoción implícita (INT+FLOAT → FLOAT). Overflow →[GBY-4042]; división/módulo por cero →[GBY-4043]; tipos inválidos →[GBY-4044].- Operador
||(concat) con misma precedencia que+/-(regla PostgreSQL). Cualquier tipo se reduce a TEXT; NULL propaga (ANSI estricta).- Postfix predicates sobre
Expr:LENGTH(x) IS NULL,UPPER(x) LIKE 'A%',LENGTH(x) IN (3,4,5),LENGTH(x) BETWEEN 3 AND 10(más sus formasNOT ...).- Funciones escalares P2/P3:
TRIM/LTRIM/RTRIM,REPLACE,SPLIT_PART,CEIL/FLOOR,MOD,POWER/SQRT,DATE_ADD/DATE_SUB,DATEDIFF,EXTRACT,STRFTIME.Pendientes residuales menores:
EXCLUDED.coldentro deON CONFLICT DO UPDATE SETy unary-prefix sobre expresión (se puede escribir0 - LENGTH(x)).NULL propagation: por defecto cualquier argumento
NULLhace que la función devuelvaNULL. Las excepciones sonCOALESCE/NULLIF/IFNULL/IF/Now/CurrentDate/CurrentTimestamp(la primera tiene su propio short-circuit, las últimas no tienen args).
📜 EBNF mínima
select_item = expression [ "AS" ident | ident ] ;
expression = arith [ cmp_op arith | postfix ] ;
postfix = "IS" [ "NOT" ] "NULL"
| [ "NOT" ] "LIKE" string_literal
| [ "NOT" ] "IN" "(" value { "," value } ")"
| [ "NOT" ] "BETWEEN" arith "AND" arith ;
arith = arith_term { ( "+" | "-" | "||" ) arith_term } ;
arith_term = arith_factor { ( "*" | "/" | "%" ) arith_factor } ;
arith_factor = primary ;
primary = literal
| qualified_ident
| func_call
| "CAST" "(" expression "AS" type_name ")"
| "CASE" [ expression ] ( "WHEN" expression "THEN" expression )+
[ "ELSE" expression ] "END"
| "(" expression ")" ;
func_call = ident "(" [ expression { "," expression } ] ")"
| "EXTRACT" "(" extract_field "FROM" expression ")"
| "CURRENT_DATE" | "CURRENT_TIMESTAMP" ;
extract_field = "YEAR" | "MONTH" | "DAY" | "HOUR" | "MINUTE" | "SECOND" ;
cmp_op = "=" | "<>" | "!=" | "<" | "<=" | ">" | ">=" ;
type_name = "INT" | "FLOAT" | "TEXT" | "BOOL" | "DATE" | "DATETIME" | "JSON" ;
🧮 Operadores aritméticos (bloque G3)
| Operador | Precedencia | Notas |
|---|---|---|
* / % |
Alta | Multiplicación, división, módulo. INT×INT con checked_* → overflow [GBY-4042]. División o módulo por cero → [GBY-4043]. |
+ - \|\| |
Baja | Suma, resta y concat. \|\| reduce ambos lados a TEXT con la misma regla que CONCAT (NULL propaga). Promoción INT+FLOAT → FLOAT en +/-. |
- NULL en cualquier lado → NULL (3VL).
- Tipos incompatibles (
'abc' + 1,true * 2, …) →[GBY-4044]. - Para forzar precedencia distinta, usar paréntesis.
🧰 Funciones soportadas (G1 + G3)
| Familia | Función | Notas |
|---|---|---|
| String | LENGTH(s) |
Largo en caracteres (no bytes). Solo TEXT. Aliases: LEN, CHAR_LENGTH. |
| String | UPPER(s) / LOWER(s) |
Solo TEXT. |
| String | SUBSTR(s, from [, len]) |
from es 1-based; from <= 0 se trata como 1. Alias: SUBSTRING. |
| String | CONCAT(a, b, …) |
Convierte cada arg a texto. NULL propaga (ANSI). |
| String (G3) | TRIM(s) / LTRIM(s) / RTRIM(s) |
Solo TEXT. Strip de whitespace ambos lados / izq / der. |
| String (G3) | REPLACE(s, from, to) |
Solo TEXT. Reemplazo no-overlap. from = '' deja s sin cambios. |
| String (G3) | SPLIT_PART(s, sep, idx) |
1-based; idx <= 0 → [GBY-4035]; fuera de rango → ''. |
| Numéricas | ABS(x) |
INT o FLOAT. |
| Numéricas | ROUND(x) / ROUND(x, n) |
INT pasa tal cual; FLOAT redondea al entero o a n decimales. |
| Numéricas (G3) | CEIL(x) / CEILING(x) / FLOOR(x) |
INT pasa tal cual; FLOAT aplica .ceil() / .floor(). |
| Numéricas (G3) | MOD(a, b) |
Mismo semántica que el operador %. Cero → [GBY-4043]. |
| Numéricas (G3) | POWER(x, y) / POW(x, y) |
Devuelve FLOAT. POWER(0, y<0) → [GBY-4045]. |
| Numéricas (G3) | SQRT(x) |
Devuelve FLOAT. Negativo → [GBY-4045]. |
| Fecha / hora | NOW() / CURRENT_TIMESTAMP |
UTC, formato YYYY-MM-DD HH:MM:SS como TEXT. |
| Fecha / hora | CURRENT_DATE |
UTC, formato YYYY-MM-DD como TEXT. Alias: CURDATE. |
| Fecha / hora (G3) | DATE_ADD(d, n) / DATE_SUB(d, n) |
d es DATE o DATETIME; suma/resta n días al date-part, preservando time-part en DATETIME. |
| Fecha / hora (G3) | DATEDIFF(d1, d2) |
Días entre d1 y d2 (d1 - d2), usando solo date-part. |
| Fecha / hora (G3) | EXTRACT(field FROM d) |
field: YEAR/MONTH/DAY/HOUR/MINUTE/SECOND. Sintaxis especial (no es coma). |
| Fecha / hora (G3) | STRFTIME(fmt, d) |
Placeholders mínimos: %Y %m %d %H %M %S %%. Otros %X pasan literal. |
| Conversión | CAST(x AS TYPE) |
Tipos: INT, FLOAT, TEXT, BOOL, DATE, DATETIME, JSON. Errores → [GBY-4036]. |
| Condicional | COALESCE(a, b, …) |
Primer argumento no-NULL. Todos NULL → NULL. |
| Condicional | NULLIF(a, b) |
NULL si a = b, sino a. |
| Condicional | IFNULL(a, b) |
a si no-NULL, sino b. |
| Condicional | IF(cond, a, b) |
cond debe ser BOOL. Alias: IIF. |
| Condicional | CASE WHEN cond THEN val [...] [ELSE val] END |
Searched form: cond debe evaluar a BOOL (NULL = no-match). |
| Condicional | CASE expr WHEN x THEN val [...] [ELSE val] END |
Simple form: matchea x contra expr por igualdad ANSI (NULL ≠ NULL). |
✅ Ejemplos
SELECT id, UPPER(name) AS n FROM users WHERE id = 1;
SELECT
CASE WHEN score >= 90 THEN 'A'
WHEN score >= 75 THEN 'B'
ELSE 'C' END AS grade
FROM exams;
SELECT COALESCE(nickname, name, 'anónimo') FROM users;
SELECT CAST(price AS TEXT) || '?' FROM products; -- G3: `||` soportado
SELECT CONCAT(CAST(price AS TEXT), '?') FROM products; -- forma equivalente
-- G2: expresiones en WHERE, HAVING, UPDATE SET
SELECT id FROM users WHERE LENGTH(name) > 3;
SELECT id FROM users WHERE UPPER(name) = 'ANA';
SELECT id FROM users WHERE COALESCE(active, false) = true;
SELECT id FROM users WHERE CASE WHEN age > 18 THEN true ELSE false END = true;
SELECT g, COUNT(*) FROM t GROUP BY g HAVING UPPER(g) = 'X';
UPDATE users SET name = UPPER(name) WHERE id = 1;
UPDATE users SET descr = COALESCE(descr, 'sin descr') WHERE id = 2;
UPDATE users SET tier = CASE WHEN age >= 18 THEN 'adult' ELSE 'minor' END;
DELETE FROM users WHERE LENGTH(name) = 0;
-- G3: aritméticos, concat, postfix Expr y funciones P2/P3
SELECT precio * cantidad AS total FROM ventas;
SELECT id FROM ventas WHERE precio * 1.21 > 1000;
UPDATE ventas SET contador = contador + 1 WHERE id = 1;
SELECT nombre || ' ' || apellido AS fullname FROM users;
SELECT id FROM users WHERE LENGTH(name) IS NULL;
SELECT id FROM users WHERE UPPER(name) LIKE 'A%';
SELECT id FROM users WHERE LENGTH(name) IN (3, 4, 5);
SELECT id FROM users WHERE LENGTH(name) BETWEEN 3 AND 10;
SELECT TRIM(' hola '), REPLACE('a-b-c', '-', '_'), SPLIT_PART('a-b-c', '-', 2);
SELECT CEIL(1.2), FLOOR(1.8), MOD(10, 3), POWER(2, 10), SQRT(16);
SELECT DATE_ADD('2026-01-01', 31), DATEDIFF('2026-12-31', '2026-01-01');
SELECT EXTRACT(YEAR FROM '2026-05-26'), STRFTIME('%Y-%m', '2026-05-26');
❌ Errores típicos
| Error | Causa |
|---|---|
[GBY-4034] LENGTH: cantidad incorrecta de argumentos |
función llamada con la aridad equivocada (e.g. LENGTH()). |
[GBY-4035] LENGTH requiere TEXT, recibí INT |
argumento de un tipo no aceptado por la función. |
[GBY-4036] CAST('xyz' AS INT): no es un entero válido |
conversión imposible al tipo destino. |
[GBY-4037] función escalar desconocida: 'FOO' |
nombre no presente en la lista soportada. |
[GBY-4038] CASE WHEN: la condición debe ser BOOL, recibí INT |
CASE WHEN x THEN … con x no booleano. |
[GBY-4039] EXPR_IN_PREDICATE_NOT_SUPPORTED |
G2 (cerrado por G3): postfix sobre Expr ahora funciona; el código queda reservado y sin emisión activa. |
[GBY-4040] expresión en WHERE/HAVING debe evaluar a BOOL (o NULL) |
G2: predicado expresional sin comparador (WHERE LENGTH(x)) — falta >/=/etc. |
[GBY-4041] UPDATE sobre 't': el valor calculado para 'col' es TEXT y la columna es INT |
G2: la RHS de un SET col = <expr> rinde un tipo incompatible — envolver con CAST(... AS T). |
[GBY-4042] overflow aritmético en INT: 9223372036854775807 + 1 |
G3: operación entera con overflow — promover a FLOAT con CAST. |
[GBY-4043] división entera por cero |
G3: divisor cero en / o %. Usar NULLIF(div, 0) o pre-filtrar. |
[GBY-4044] operador '+' no acepta operandos TEXT y INT |
G3: operador aritmético sobre tipos incompatibles. ¿Quisiste decir \|\|? |
[GBY-4045] SQRT(-1) indefinido en reales (argumento negativo) |
G3: función matemática fuera del dominio real. |
[GBY-4046] DATE_ADD: '2026-13-01' no es DATE ni DATETIME válido |
G3: TEXT no parseable como fecha en una función de fecha. |
[GBY-4047] EXTRACT: campo 'CENTURY' no soportado |
G3: EXTRACT(<campo> FROM ...) con campo no permitido. |
Subqueries y derived tables (bloque H)
Bloque H (2026-05-26) cierra los P0+P1 de subqueries: derived tables,
NOT IN (SELECT), subquery escalar en SELECT list, y multi-predicate correlated EXISTS dentro deAND/OR/NOT.
📜 EBNF
from_source := tabla_ident [ alias ]
| "(" subquery ")" alias (* derived table — alias OBLIGATORIO *)
subquery := "SELECT" select_stmt
scalar_subq := "(" subquery ")" (* dentro de Expr *)
where_atom := … (formas pre-H) …
| columna [ "NOT" ] "IN" "(" subquery ")" (* H *)
| "EXISTS" "(" subquery ")" (* puede ir dentro de AND/OR/NOT — H *)
| "NOT" "EXISTS" "(" subquery ")" (* idem *)
expr_primary := … (formas pre-H) …
| scalar_subq (* H *)
✅ Ejemplos
-- Derived table: lista de cursos con cantidad de alumnos.
SELECT sub.curso_id, sub.total
FROM (SELECT curso_id, COUNT(*) AS total FROM alumnos GROUP BY curso_id) AS sub
ORDER BY sub.total DESC;
-- Derived table joineada con una tabla persistente.
SELECT cursos.nivel, sub.total
FROM cursos
INNER JOIN (SELECT curso_id, COUNT(*) AS total FROM alumnos GROUP BY curso_id) AS sub
ON cursos.id = sub.curso_id;
-- NOT IN con 3VL ANSI estricta.
SELECT id FROM cursos
WHERE id NOT IN (SELECT curso_id FROM alumnos WHERE edad = 19);
-- Subquery escalar en SELECT list (correlated).
SELECT cursos.id,
(SELECT COUNT(*) FROM alumnos WHERE alumnos.curso_id = cursos.id) AS cnt
FROM cursos;
-- EXISTS correlacionado combinado con otro predicado.
SELECT id FROM cursos
WHERE EXISTS (SELECT 1 FROM alumnos WHERE alumnos.curso_id = cursos.id)
AND id = 1;
⚠️ Reglas y limitaciones
- Alias obligatorio en derived tables (ANSI estricto,
[GBY-4048]). Sin él el parser rechaza. - Sin nombres duplicados en el output de una derived table (
[GBY-4049]). Usar alias internos para des-ambiguar. - Inferencia de tipo por columna del derived: si todos los valores no-NULL son de la misma variante (INT/FLOAT/BOOL/TEXT), el schema virtual usa ese tipo; mezcla → fallback a TEXT.
- Sin índices sobre derived (always full-scan en el outer). Sin UPDATE/DELETE/INSERT sobre una derived table.
- NOT IN + NULL: si la subquery proyecta algún NULL,
col NOT IN (...)devuelve NULL para todos los outer rows que no matcheen exactamente. Es la regla ANSI estricta — distinta deNOT (col IN ...)cuando hay match. - Subquery escalar: exactamente 1 columna y a lo sumo 1 fila. 0 filas → NULL. más de 1 →
[GBY-4014]. más de 1 columna →[GBY-4011]. - Correlated multi-predicado:
EXISTSycol = outer.colcorrelacionados ahora funcionan dentro deAND/OR/NOT(el código histórico[GBY-4024]queda deprecado).
❌ Errores típicos
| Mensaje | Por qué |
|---|---|
[GBY-4048] derived table (SELECT ...) requiere un alias obligatorio |
El parser vio FROM (SELECT ...) sin alias. |
[GBY-4049] derived table 'x' proyecta dos columnas con el mismo nombre 'id' |
La subquery devuelve nombres duplicados. Aliasear: SELECT a AS x, b AS y. |
[GBY-4014] subquery escalar … devolvió 5 filas |
Subquery en SELECT list o en WHERE = (SELECT ...) con más de una fila. |
[GBY-4011] subquery … debe devolver exactamente 1 columna |
La subquery escalar/IN devuelve múltiples columnas. |
Set operations y VALUES (bloque I)
Operaciones de conjunto entre queries (
UNION/INTERSECT/EXCEPT) con su varianteALL, aliasMINUSde Oracle, yVALUESusable como query standalone o como tabla virtual dentro del FROM.
📜 EBNF
select_query ::= select_term { ( "UNION" | "EXCEPT" | "MINUS" ) [ "ALL" ] intersect_term }*
[ "ORDER" "BY" ident [ "ASC" | "DESC" ] ]
[ "LIMIT" int ] [ "OFFSET" int ]
intersect_term ::= select_term { "INTERSECT" [ "ALL" ] select_term }*
select_term ::= select_stmt
| values_stmt
| "(" select_query ")"
values_stmt ::= "VALUES" row { "," row }*
row ::= "(" expr { "," expr }* ")"
🪜 Precedencia (ANSI)
INTERSECT(más alta — ata más fuerte).UNIONyEXCEPT/MINUS(misma precedencia, asociativos a izquierda).
A UNION B INTERSECT C se parsea como A UNION (B INTERSECT C). Para forzar otro orden, usar paréntesis.
🧮 Semántica de multisets
UNION ALL: append (preserva duplicados).count_out = count_l + count_r.UNION(sinALL): append + dedup.count_out = 1para cada fila distinta.INTERSECT ALL:count_out = min(count_l, count_r)por fila distinta.INTERSECT:count_out = 1para cada fila presente en ambos.EXCEPT ALL:count_out = max(0, count_l - count_r)por fila distinta.EXCEPT:count_out = 1para cada fila presente en LHS y no en RHS.- Dos
NULLson iguales acá (comportamiento ANSI de set ops).
🪧 Compatibilidad columna a columna
- Ambos lados deben tener la misma arity (
[GBY-4054]si no). - Los tipos deben ser compatibles: INT/FLOAT promueven entre sí, cualquier otra mezcla rompe con
[GBY-4055]. NULL no chequea tipo. - Los headers del output vienen del LHS (regla ANSI: el primer SELECT impone los nombres).
✅ Ejemplos
-- Unión simple con dedup
SELECT id FROM a UNION SELECT id FROM b ORDER BY id ASC;
-- Unión preservando duplicados
SELECT nombre FROM clientes_2024 UNION ALL SELECT nombre FROM clientes_2025;
-- Intersección + ORDER BY al nivel del resultado
(SELECT id FROM activos) INTERSECT (SELECT id FROM premium) ORDER BY id DESC LIMIT 10;
-- Diferencia con alias MINUS
SELECT id FROM a MINUS SELECT id FROM b;
-- VALUES standalone (devuelve ResultSet con headers column1, column2, ...)
VALUES (1, 'a'), (2, 'b'), (3, 'c');
-- VALUES como tabla virtual en FROM
SELECT id, name FROM (VALUES (1, 'a'), (2, 'b')) AS t(id, name) ORDER BY id ASC;
-- JOIN entre persistente y VALUES virtual
SELECT a.id, t.tag
FROM a INNER JOIN (VALUES (1, 'uno'), (3, 'tres')) AS t(id, tag) ON a.id = t.id;
-- Precedencia: INTERSECT ata más fuerte que UNION
-- ( == A UNION (B INTERSECT C) )
VALUES (1), (2) UNION VALUES (3), (4) INTERSECT VALUES (4), (5);
-- → 1, 2, 4
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4054] UNION entre queries con N y M columnas |
Distinta arity entre LHS y RHS. |
[GBY-4055] UNION: la columna K del LHS es Int y la del RHS es Text |
Tipos incompatibles (sin promoción INT↔FLOAT). |
[GBY-4052] VALUES en FROM requiere alias de tabla obligatorio |
Falta AS t(c1, c2, ...) tras (VALUES ...). |
[GBY-4053] lista de aliases de columna tiene X entradas pero las filas de VALUES tienen Y |
Mismatch entre t(c1, c2) y la arity de las tuplas. |
[GBY-4056] VALUES: fila K tiene N expresiones pero la fila 1 tiene M |
Dos filas del VALUES con distinta arity. |
[GBY-4057] VALUES requiere al menos una fila |
VALUES; sin tuplas. |
⚠️ No soportado todavía
ORDER BY 1posicional sobre el output de un set op (usar nombre).- Set ops dentro de
UPDATE/DELETE(no es ANSI estándar). ALL/ANY/SOMEsobre subqueries (backlog H-P2).
CTEs no-recursivas (WITH, bloque W1)
Common Table Expressions: dar nombre a una subquery para reusarla en
FROM/JOIN/ subqueries (IN,EXISTS, scalar) y en cualquiera de las ramas de un set op. Múltiples CTEs encadenables —bpuede referenciaradeclarada antes.
📜 EBNF
with_query ::= "WITH" cte_def { "," cte_def }* select_query
cte_def ::= ident "AS" "(" select_stmt ")"
🧠 Semántica
- Shadowing ANSI: el nombre de la CTE prevalece sobre cualquier tabla real homónima del catálogo.
- Orden de declaración estricto:
WITH a AS (...), b AS (SELECT FROM a)está OK; al revés (bantes quea) rompe con “tabla no existe” al parsearb. - Visible desde set ops:
WITH x AS (...) SELECT FROM x UNION SELECT FROM xresuelvexen ambas ramas. - Re-ejecución: si la CTE se referencia N veces, el body se materializa N veces (deuda explícita — futura optimización por memoización, ver ADR-0026 §1).
✅ Ejemplos
-- CTE simple
WITH seniors AS (SELECT id, nombre FROM empleados WHERE salario >= 100)
SELECT nombre FROM seniors ORDER BY nombre;
-- Encadenamiento
WITH ventas_2025 AS (SELECT * FROM ventas WHERE anio = 2025),
top_clientes AS (SELECT cliente_id, SUM(monto) AS total
FROM ventas_2025 GROUP BY cliente_id)
SELECT cliente_id FROM top_clientes WHERE total >= 10000;
-- CTE en JOIN
WITH big AS (SELECT uid FROM orders WHERE total >= 100)
SELECT u.name FROM users u INNER JOIN big ON u.id = big.uid;
-- CTE en subquery
WITH high AS (SELECT id FROM t WHERE v >= 20)
SELECT id FROM t WHERE id IN (SELECT id FROM high);
-- CTE visible desde ambas ramas de un UNION
WITH x AS (SELECT id FROM t WHERE id = 1)
SELECT id FROM x UNION SELECT id FROM t WHERE id = 2 ORDER BY id;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4079] WITH: el nombre 'X' aparece más de una vez |
Dos CTEs con el mismo nombre en el mismo WITH (case-insensitive). |
[GBY-4081] WITH cte(...) AS (...): los column aliases en la cabecera están diferidos |
Column aliases en la cabecera de la CTE. Workaround: aliasar dentro del body (SELECT x AS c1, y AS c2 FROM ...). |
⚠️ No soportado todavía
- Column aliases en la cabecera (
WITH cte(c1, c2) AS (...)) — workaround inline disponible. - Window functions (
ROW_NUMBER,RANK,SUM() OVER (...)) — bloque W3. - CTEs sobre
UPDATE/DELETE— sóloSELECTpor ahora.
CTEs recursivas (WITH RECURSIVE, bloque W2)
Common Table Expressions recursivas: el body se materializa por fixpoint base+step. Soporta exactamente UNA CTE recursive por statement, con body canónico
anchor UNION [ALL] step. Casos típicos: generadores de números, traversal de jerarquías (descendientes / ancestros), expansión transitiva.
📜 EBNF
recursive_query ::= "WITH" "RECURSIVE" ident "AS" "(" recursive_body ")" select_query
recursive_body ::= select_stmt "UNION" [ "ALL" ] select_stmt
El primer SELECT es el anchor (caso base — no puede referenciar la CTE). El segundo es el step (caso recursivo — debe referenciar la CTE por nombre en algún FROM).
🧠 Semántica (delta semantics, ANSI)
- Iteración 0:
accum = anchor.exec();delta = accum. - Iteración N≥1: el step se ejecuta con la CTE bindeada SOLO a
delta(no alaccumcumulativo); el resultado se filtra por dedup contraaccumcuando esUNION(noALL);accumse extiende con las filas nuevas;delta = filas nuevas. - Terminación: cuando
delta = ∅. - Guards de runaway:
MAX_RECURSIVE_ITERATIONS = 1000yMAX_RECURSIVE_ROWS = 100_000. Recursión bien terminada no se acerca a estos límites.
✅ Ejemplos
-- Generador 1..100
WITH RECURSIVE nums AS (
SELECT 1 AS n
UNION ALL
SELECT n + 1 FROM nums WHERE n < 100
)
SELECT n FROM nums;
-- Descendientes de un nodo en un árbol (id, parent)
WITH RECURSIVE descendants AS (
SELECT id FROM tree WHERE id = :root_id
UNION ALL
SELECT t.id FROM tree t INNER JOIN descendants d ON t.parent = d.id
)
SELECT id FROM descendants ORDER BY id;
-- UNION (no ALL) — termina natural por dedup
WITH RECURSIVE r AS (
SELECT 1 AS n
UNION
SELECT 1 FROM r -- step produce SIEMPRE 1; dedup ⇒ delta vacío ⇒ stop
)
SELECT n FROM r;
-- JOIN entre la CTE recursive y una tabla persistente
WITH RECURSIVE nums AS (
SELECT 1 AS n UNION ALL SELECT n + 1 FROM nums WHERE n < 5
)
SELECT l.lab FROM nums INNER JOIN labels l ON l.n = nums.n ORDER BY l.lab;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4082] WITH RECURSIVE: soportamos exactamente UNA CTE recursive por statement |
Más de una CTE dentro del mismo WITH RECURSIVE. Workaround: anidar. |
[GBY-4083] WITH RECURSIVE r: superó el límite de 1000 iteraciones |
Recursión sin condición de corte. Agregar WHERE n < N al step, o usar UNION (no UNION ALL) para dedup. |
[GBY-4084] WITH RECURSIVE r: superó el límite de 100000 filas |
Mismo diagnóstico. |
[GBY-4085] WITH RECURSIVE r: el step proyecta N columnas pero el anchor proyectó M |
Schema inconsistente — ANSI exige arity posicional idéntica. |
[GBY-4086] WITH RECURSIVE r: el body debe ser anchor UNION [ALL] step |
Body que no es la forma canónica. Si no necesita ser recursive, quitar la palabra RECURSIVE. |
⚠️ No soportado todavía
- Múltiples CTEs recursive en el mismo
WITH— usar un soloWITH RECURSIVEo anidar. - Cumulative semantics (step lee el accum, no solo el delta) — diferido; mayoría de casos prácticos funcionan con delta.
- Window functions sobre el resultado del fixpoint — bloque W3.
Window functions (bloque W3)
Funciones que computan un valor por fila usando OTRAS filas de la misma partition — clásicas: enumerar dentro de un grupo (
ROW_NUMBER), rankear (RANK/DENSE_RANK), totales corridos (SUM(x) OVER (ORDER BY d)), mirar fila anterior/siguiente (LAG/LEAD), partir en buckets (NTILE), valores extremos de la partition (FIRST_VALUE/LAST_VALUE).
📜 EBNF
window_call ::= window_func "(" [ expr {"," expr}* ] ")"
"OVER" "(" [ "PARTITION" "BY" expr {"," expr}* ]
[ "ORDER" "BY" expr [ "ASC" | "DESC" ]
{"," expr [ "ASC" | "DESC" ]}* ] ")"
window_func ::= "ROW_NUMBER" | "RANK" | "DENSE_RANK" | "NTILE"
| "COUNT" | "SUM" | "AVG" | "MIN" | "MAX"
| "LAG" | "LEAD" | "FIRST_VALUE" | "LAST_VALUE"
🧠 Defaults de frame (sin spec explícita — ROWS BETWEEN ... no soportado)
| Familia | Con ORDER BY |
Sin ORDER BY |
|---|---|---|
Ranking (ROW_NUMBER/RANK/DENSE_RANK/NTILE) |
per-row | rank arbitrario (no determinístico) |
Aggregate (SUM/COUNT/AVG/MIN/MAX) |
running (acumula hasta CURRENT ROW) | full partition |
LAG/LEAD |
offset hacia atrás/adelante | ❌ [GBY-4088] requerido |
NTILE(n) |
distribución balanceada | ❌ [GBY-4088] requerido |
FIRST_VALUE |
primera fila de la partition ordenada | primera del orden source |
LAST_VALUE |
última de la partition (no CURRENT ROW; deviation de ANSI) | última del orden source |
✅ Ejemplos
-- Numerar dentro de cada región por salario descendente
SELECT id, region, ROW_NUMBER() OVER (PARTITION BY region ORDER BY salary DESC) AS rk
FROM employees;
-- Rankear con ties (RANK salta, DENSE_RANK no)
SELECT id, score,
RANK() OVER (ORDER BY score DESC) AS r,
DENSE_RANK() OVER (ORDER BY score DESC) AS dr
FROM students;
-- Total corrido por fecha
SELECT date, amount, SUM(amount) OVER (ORDER BY date) AS running_total
FROM tx;
-- Total por región (full partition, sin ORDER BY)
SELECT id, region, SUM(amount) OVER (PARTITION BY region) AS region_total
FROM sales;
-- Mirar fila anterior con default
SELECT date, price, LAG(price, 1, 0) OVER (ORDER BY date) AS prev_or_zero
FROM ticks;
-- Partir en quartiles
SELECT id, score, NTILE(4) OVER (ORDER BY score DESC) AS quartile
FROM students;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4088] LAG: requiere ORDER BY dentro del OVER |
LAG/LEAD/NTILE sin ORDER BY. También: nombre always-window sin OVER. |
[GBY-4089] ROW_NUMBER: arity esperada [0, 0], recibí 1 |
Arity incorrecta. |
[GBY-4090] window functions mezcladas con GROUP BY ... |
Mezcla con GROUP BY/HAVING/agregado clásico en el mismo SELECT. Workaround: derived table sobre el GROUP BY. |
[GBY-4091] RETURNING no admite window functions |
Window fuera de SELECT list top-level. |
⚠️ No soportado todavía
- Frame specs explícitas:
ROWS BETWEEN N PRECEDING AND M FOLLOWING,RANGE BETWEEN ...,GROUPS BETWEEN .... Los defaults aplican; no se pueden customizar. WINDOW w AS (...)named windows: reusar una spec entre múltiples columnas.PERCENT_RANK,CUME_DIST: funciones de distribución.- Mezcla con
GROUP BY: usar derived table como workaround. LAST_VALUEcon semántica ANSI (CURRENT ROW default con ORDER BY): llegará cuando se implementen frames explícitas.
Triggers (bloques X1 + X2)
CREATE TRIGGER ... {BEFORE|AFTER} ... FOR EACH ROW <body>: ejecuta un body DML antes o después de cada fila afectada por un INSERT/UPDATE/DELETE. Body puede ser una sola sentencia (X1) o un bloqueBEGIN ... ENDcon múltiples sentencias (X2). Persistido en el catálogo (bump VERSION 13 → 14 en X1; X2 sin bump).
📜 EBNF
create_trigger ::= "CREATE" "TRIGGER" ident ("BEFORE"|"AFTER") ("INSERT"|"UPDATE"|"DELETE")
"ON" ident "FOR" "EACH" "ROW" trigger_body
trigger_body ::= dml_stmt
| "BEGIN" dml_stmt {";" dml_stmt}* [";"] "END"
drop_trigger ::= "DROP" "TRIGGER" ["IF" "EXISTS"] ident
dml_stmt ::= insert_stmt | update_stmt | delete_stmt | replace_stmt
🧠 Referencias NEW.col / OLD.col
| Evento | NEW disponible | OLD disponible |
|---|---|---|
INSERT |
✅ (fila recién insertada) | ❌ → [GBY-4094] |
UPDATE |
✅ (fila post-update) | ✅ (fila pre-update) |
DELETE |
❌ → [GBY-4094] |
✅ (fila recién borrada) |
Las referencias se substituyen a nivel de TOKEN antes de re-parsear el body — funciona en cualquier contexto (INSERT VALUES, UPDATE SET, WHERE, etc.).
✅ Ejemplos
-- Auditoría simple
CREATE TABLE audit (id INT PRIMARY KEY, action TEXT, who INT);
CREATE TRIGGER audit_user_insert AFTER INSERT ON users
FOR EACH ROW INSERT INTO audit (id, action, who)
VALUES (NEW.id, 'inserted', NEW.id);
-- Log de cambios con NEW y OLD
CREATE TRIGGER log_price_change AFTER UPDATE ON products
FOR EACH ROW INSERT INTO price_log (id, old_price, new_price)
VALUES (NEW.id, OLD.price, NEW.price);
-- Tombstone table
CREATE TRIGGER tomb AFTER DELETE ON items
FOR EACH ROW INSERT INTO removed (id, name) VALUES (OLD.id, OLD.name);
-- Borrado
DROP TRIGGER IF EXISTS audit_user_insert;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4092] CREATE TRIGGER '...': ya existe un objeto ... |
Colisión de nombre con tabla / vista / trigger. |
[GBY-4093] el body debe arrancar con INSERT, UPDATE, DELETE o REPLACE (también body inválido por otras razones) |
Body con shape no soportado. Históricamente este mismo código bloqueaba BEFORE triggers en X1 (pre-X2); BEFORE está soportado desde X2 (2026-05-28). |
[GBY-4093] el body debe arrancar con INSERT, UPDATE, DELETE o REPLACE |
Body es SELECT u otra cosa. |
[GBY-4094] trigger 'X': NEW.y no es una columna válida |
Referencia a columna inexistente o NEW/OLD en evento incompatible. |
[GBY-4095] cascada de triggers excedió la profundidad 16 |
Recursión runaway (un trigger dispara otro DML que dispara más triggers). |
[GBY-4096] DROP TRIGGER '...': no existe |
Nombre desconocido sin IF EXISTS. |
🧠 BEFORE vs AFTER
| Aspecto | BEFORE | AFTER |
|---|---|---|
| Cuándo corre | Antes del write | Después del write |
NEW construido |
INSERT: user-stated + NULL; UPDATE: OLD + assignments | El row tal cual quedó persistido |
NEW mutable |
❌ (X2 read-only) | ❌ |
| Aborta el DML principal si rebota | ✅ (no se escribe nada) | ✅ (lo escrito + el trigger se rollback-ean) |
🧠 Body BEGIN ... END (X2)
Las sentencias dentro del block se separan con ; y se ejecutan en orden. Si alguna falla, las restantes NO se ejecutan y el error se propaga al DML principal — toda la transacción rollback. BEGIN ... END puede anidarse aunque no agrega expresividad en X2 (no hay scope ni control de flujo).
⚠️ No soportado (estado al 2026-06-15)
Entregado posteriormente (notar que esta sección estaba escrita para X1/X2):
Control de flujo en el body (✅ entregado por X4 → X4f (PL/pgSQL completo, 2026-05-29).IF/LOOP/WHILE, variablesDECLARE)IF/CASE/WHILE/FOR/LOOP/DECLARE/SET/RAISE/EXCEPTION/RETURN. Ver secciones X4b–X4f y X5–X6 abajo.✅ entregado por X4c (2026-05-28). SintaxisRAISE EXCEPTION/RAISE NOTICERAISE [EXCEPTION|NOTICE] 'msg'.Lenguaje procedural (variables, IF/THEN, LOOP, EXCEPTION)✅ entregado por X4 → X6.
Sigue pendiente:
- NEW mutable en BEFORE (
NEW.updated_at := NOW()desde dentro del body del trigger). Lo que pasó el caller persiste tal cual. FOR EACH STATEMENT(vsFOR EACH ROW) — fuera de scope.INSTEAD OFtriggers (sobre vistas) — fuera de scope.- OLD en UPSERT que terminó en UPDATE — el path
INSERT ... ON CONFLICT DO UPDATEque dispara AFTER UPDATE fire con OLD=None.
Stored procedures (bloque X3)
CREATE PROCEDURE name(p1 TYPE, ...) AS <body>+CALL name(args)+DROP PROCEDURE: encapsular side effects parametrizados. Body es DML simple oBEGIN ... ENDmulti-stmt. CALL es un statement standalone — no se puede usar como expresión (funciones invocables en SELECT llegan en X3b). Persistido en catálogo (bump VERSION 14 → 15).
📜 EBNF
create_proc ::= "CREATE" "PROCEDURE" ident "(" [param {"," param}*] ")"
"AS" proc_body
param ::= ident type_name
proc_body ::= dml_stmt
| "BEGIN" dml_stmt {";" dml_stmt}* [";"] "END"
drop_proc ::= "DROP" "PROCEDURE" ["IF" "EXISTS"] ident
call_stmt ::= "CALL" ident "(" [expr {"," expr}*] ")"
✅ Ejemplos
-- Procedure simple
CREATE PROCEDURE log_msg(p_id INT, p_msg TEXT) AS
INSERT INTO log (id, msg) VALUES (p_id, p_msg);
CALL log_msg(42, 'hello');
-- Body multi-statement
CREATE PROCEDURE log_both(p_id INT) AS BEGIN
INSERT INTO log_a (id) VALUES (p_id);
INSERT INTO log_b (id) VALUES (p_id);
END;
CALL log_both(99);
-- Args como expresiones
CALL log_msg(10 + 5, UPPER('abc'));
-- Cleanup
DROP PROCEDURE IF EXISTS log_msg;
⚠️ Limitación conocida — choque param/columna
Si una columna tiene el mismo nombre que un parámetro, el ident de la columna también se substituye (token-level) y la query rompe. Workaround: prefijar param names (p_id, arg_name) — convención estándar PG.
-- ❌ choque: `id` aparece como param Y como columna
CREATE PROCEDURE add_log(id INT) AS INSERT INTO log (id) VALUES (id);
-- ✅
CREATE PROCEDURE add_log(p_id INT) AS INSERT INTO log (id) VALUES (p_id);
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4097] CREATE PROCEDURE '...': ya existe un objeto ... |
Colisión de nombre con tabla / vista / trigger / procedure. |
[GBY-4098] el body debe arrancar con INSERT, UPDATE, DELETE, REPLACE o BEGIN |
Body no válido o BEGIN sin END. |
[GBY-4099] CALL '...': procedure no existe |
También DROP PROCEDURE sin IF EXISTS. |
[GBY-4100] CALL '...': esperaba N args, recibí M |
Arity mismatch. |
⚠️ No soportado todavía
CREATE FUNCTION ... RETURNS scalarinvocable desde SELECT/WHERE — X3b.- Type checking estricto en CALL (hoy el motor confía en que el DML downstream rebote).
- Args OUT/INOUT,
DECLAREvariables locales,IF/LOOP/WHILE— área de PL/pgSQL, X4+. - Recursion guard para procedures.
CREATE PROCEDURE ... RETURNS TABLE(tabla de salida).
User-defined functions (bloque X3b)
CREATE FUNCTION name(p1 TYPE, ...) RETURNS TYPE AS <expr>+DROP FUNCTION: funciones escalares invocables desde cualquier expresión (SELECT, WHERE, HAVING, ORDER BY, body de otra function…). Body es UNA expresión (no SELECT — desviación práctica de ANSI). Persistido en catálogo (bump VERSION 15 → 16).
📜 EBNF
create_function ::= "CREATE" "FUNCTION" ident "(" [param {"," param}*] ")"
"RETURNS" type_name "AS" expr
drop_function ::= "DROP" "FUNCTION" ["IF" "EXISTS"] ident
✅ Ejemplos
-- Doble
CREATE FUNCTION dbl(p_x INT) RETURNS INT AS p_x * 2;
SELECT id, dbl(v) AS doubled FROM t;
-- Saludar con CONCAT
CREATE FUNCTION greet(p_name TEXT) RETURNS TEXT AS CONCAT('Hi ', p_name);
SELECT greet(name) FROM users;
-- Predicado en WHERE
CREATE FUNCTION big(p_x INT) RETURNS BOOL AS p_x >= 100;
SELECT * FROM t WHERE big(v);
-- Composición
CREATE FUNCTION quad(p_x INT) RETURNS INT AS dbl(dbl(p_x));
SELECT quad(v) FROM t; -- = dbl(dbl(v)) = v * 4
-- Cleanup
DROP FUNCTION IF EXISTS dbl;
⚠️ Restricciones
- Body es UNA Expr, no un SELECT. Sin acceso a tablas dentro del body (workaround: pre-procesar con un trigger o usar CTE/view externos).
- NEW/OLD no aplican (esos son scope de triggers).
- CHECK constraints rechazan user functions para preservar pureza.
- Mismo workaround de procedures para choque param/columna: prefijar (
p_x).
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4101] CREATE FUNCTION '...': ya existe un objeto ... |
Colisión de nombre. |
[GBY-4102] body vacío |
AS sin expr siguiente, params duplicados. |
[GBY-4103] función '...' no existe |
Invocación a function inexistente; también DROP sin IF EXISTS. |
[GBY-4104] '...': esperaba N args, recibí M |
Arity mismatch. |
⚠️ No soportado todavía
- Body como SELECT (
RETURNS ... AS $$ SELECT ... $$ LANGUAGE SQL) — requiere SELECT sin FROM. RETURNS TABLE(table-valued functions).- Type checking estricto en args/return.
IMMUTABLE/STABLE/VOLATILEhints.- PL/pgSQL body (variables, IF, LOOP).
Control de flujo IF (bloque X4)
IF expr THEN <stmts> [ELSIF expr THEN <stmts>]* [ELSE <stmts>] END IF: control de flujo como statement top-level. Funciona en cualquier contexto donde quepa un statement — batches SQL planos, bodies de trigger / procedure, branches de otro IF (anidado).
📜 EBNF
if_stmt ::= "IF" expr "THEN" stmt_list
{ "ELSIF" expr "THEN" stmt_list }*
[ "ELSE" stmt_list ]
"END" "IF"
stmt_list ::= stmt {";" stmt}* [";"]
🧠 Semántica
- La condición debe evaluar a
BOOLoNULL(NULLse trata comoFALSE— 3VL). - Otros tipos →
[GBY-4105]. - Evalúa cada condición en orden; primer TRUE ejecuta su branch y sale (no evalúa las restantes).
- Si ninguna fue TRUE y hay
ELSE, ejecutaELSE. Sino, no-op.
✅ Ejemplos
-- Top-level
IF (SELECT COUNT(*) FROM orders) > 1000 THEN
INSERT INTO alerts (msg) VALUES ('alta carga');
END IF;
-- Chain ELSIF
IF total >= 1000 THEN INSERT INTO platinum VALUES (id);
ELSIF total >= 100 THEN INSERT INTO gold VALUES (id);
ELSIF total >= 10 THEN INSERT INTO silver VALUES (id);
ELSE INSERT INTO bronze VALUES (id);
END IF;
-- Dentro de trigger
CREATE TRIGGER classify AFTER INSERT ON t FOR EACH ROW BEGIN
IF NEW.v >= 100 THEN INSERT INTO big VALUES (NEW.id);
ELSE INSERT INTO small VALUES (NEW.id);
END IF;
END;
-- Dentro de procedure
CREATE PROCEDURE classify(p_id INT, p_v INT) AS BEGIN
IF p_v >= 100 THEN INSERT INTO log (id, label) VALUES (p_id, 'big');
ELSE INSERT INTO log (id, label) VALUES (p_id, 'small');
END IF;
END;
-- Anidado
IF cond1 THEN
IF cond2 THEN INSERT INTO log VALUES (1); END IF;
END IF;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4105] IF: la condición debe evaluar a BOOL ... |
IF 42 THEN ... o similar. Usar comparación / IS NULL / función que devuelva BOOL. |
[GBY-4106] IF: falta END IF para cerrar el bloque |
Falta END IF matching. |
[GBY-4106] IF: se esperaba THEN después de la condición |
Falta THEN. |
[GBY-4106] EOF antes de END IF |
El input se cortó dentro de un bloque IF abierto. |
⚠️ No soportado todavía
- Variables locales (
DECLARE x INT [DEFAULT expr]) — X4b. - Asignación (
SET x = expr/x := expr) — X4b. WHILE/LOOP/FOR/EXIT [WHEN]/CONTINUE— X4b.RAISE EXCEPTION/RAISE NOTICE— X4c.EXCEPTION WHEN ... THENhandlers — X4c.CASEstatement (vs CASE expression que ya existe en SELECT list) — futuro.
Variables + WHILE LOOP (bloque X4b)
DECLARE+SET+WHILE LOOP+EXIT [WHEN]: variables locales y loops. Top-level y dentro de bodies de trigger/procedure. Scope plano (limitación X4b — no nested scope por BEGIN..END).
📜 EBNF
declare_stmt ::= "DECLARE" ident type_name [ "DEFAULT" expr ]
set_stmt ::= "SET" ident "=" expr
while_stmt ::= "WHILE" expr "LOOP" stmt_list "END" "LOOP"
exit_stmt ::= "EXIT" [ "WHEN" expr ]
🧠 Semántica
DECLAREagrega variable al scope (init conDEFAULT expro NULL). Redeclarar →[GBY-4108].SETactualiza variable existente (RHS = Expr eval’d contra scope actual). Variable no declarada →[GBY-4107].WHILEitera mientras cond TRUE. GuardMAX_LOOP_ITERATIONS = 100_000→[GBY-4109].EXITsin WHEN: sale incondicional del loop innermost.EXIT WHEN cond: sale solo si cond TRUE.
⚠️ Limitación importante: variables NO en INSERT VALUES
DECLARE n INT DEFAULT 5;
-- ❌ falla — el parser de VALUES exige Value literal, no Expr
INSERT INTO t VALUES (n);
Workarounds:
-- ✅ INSERT SELECT acepta Expr
INSERT INTO t SELECT n FROM (VALUES (1)) AS dummy;
-- ✅ UPDATE SET acepta Expr (vars OK en RHS)
UPDATE t SET col = n WHERE id = 1;
-- ✅ En procedure: usar params (se substituyen a literal en CALL)
CREATE PROCEDURE foo(p_n INT) AS INSERT INTO t VALUES (p_n);
✅ Ejemplos
-- Counter loop
DECLARE i INT DEFAULT 0;
WHILE i < 10 LOOP
SET i = i + 1;
END LOOP;
-- Loop con EXIT WHEN
DECLARE counter INT DEFAULT 0;
WHILE TRUE LOOP
SET counter = counter + 1;
EXIT WHEN counter >= 42;
END LOOP;
-- Loop con IF + EXIT incondicional
DECLARE found INT DEFAULT 0;
WHILE found = 0 LOOP
SET found = (SELECT COUNT(*) FROM t WHERE flag = 1);
IF found > 0 THEN EXIT; END IF;
-- ... resto del trabajo
END LOOP;
-- En procedure body
CREATE PROCEDURE process(p_max INT) AS BEGIN
DECLARE i INT DEFAULT 0;
WHILE i < p_max LOOP
UPDATE counters SET v = v + 1 WHERE id = 1;
SET i = i + 1;
END LOOP;
END;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4107] SET '...': variable no declarada |
Falta DECLARE previo. |
[GBY-4108] DECLARE '...': ya declarada en el scope |
Redeclare. PG permite shadowing en sub-blocks; X4b scope plano no. |
[GBY-4109] WHILE LOOP: superó el límite de 100000 iter |
Condición no converge. Agregar EXIT WHEN o revisar SET. |
⚠️ No soportado al cierre de X4b (estado de la sub-sección X4b — desde 2026-06-15 muchos están entregados, ver bloques X4c/X4d/X4e/X4f/X5/X6 abajo)
Entregado posteriormente:
✅ X4c (2026-05-28).FOR i IN a..b LOOP✅ X6 (2026-05-29, ADR-0042).FOR row IN SELECT ... LOOP✅ X4c (2026-05-28).RAISE EXCEPTION✅ X4d (2026-05-28).EXCEPTION WHENhandlers✅ X4d (2026-05-28).LOOP ... END LOOPstandalone
Sigue pendiente:
- Nested scope real por BEGIN..END block — diferido.
- Type checking estricto en DECLARE/SET — diferido (parser acepta sin validar el tipo declarado).
- Variables en
INSERT VALUES— requiere lift de la restricción de VALUES.
RAISE + FOR LOOP (bloque X4c)
RAISE [EXCEPTION|NOTICE] 'msg': aborto explícito con mensaje (EXCEPTION) o info logging (NOTICE).FOR ident IN start TO end LOOP <body> END LOOP: range loop con auto-declaración de la variable de iteración.
📜 EBNF
raise_stmt ::= "RAISE" [ "EXCEPTION" | "NOTICE" ] string_literal
for_stmt ::= "FOR" ident "IN" expr "TO" expr "LOOP" stmt_list "END" "LOOP"
🧠 Semántica
RAISE:
- Si no se especifica level, default es EXCEPTION.
- EXCEPTION →
[GBY-4111]con el mensaje del user. Propaga normalmente; el wrap caller hace rollback de la transacción. - NOTICE → ResultSet vacío con
message = "NOTICE: <msg>". No interrumpe el flujo. - En X4c no había
EXCEPTION WHEN ...handlers — entregados en X4d (2026-05-28, ADR-0036).
FOR:
ise auto-declara envar_scopeal inicio (shadowing si ya existe; se restaura al terminar).- Iteración inclusiva:
i = start, start+1, ..., end. - Si
start > end, no itera (sin error). - STEP fijo en 1, ascendente.
start/enddeben ser INT (→[GBY-4113]si no).EXIT [WHEN]funciona dentro de FOR (mismo sentinel que WHILE).- Guard
MAX_LOOP_ITERATIONS = 100_000compartido.
Sintaxis non-PG: PG usa FOR i IN 1..10 LOOP. gabysql usa start TO end para evitar ambigüedad con qualifier ident (tabla.col).
✅ Ejemplos
-- RAISE en validación
CREATE TRIGGER validate AFTER INSERT ON orders FOR EACH ROW BEGIN
IF NEW.amount < 0 THEN
RAISE EXCEPTION 'negative amount not allowed';
END IF;
END;
-- NOTICE para logging
RAISE NOTICE 'job started';
-- FOR loop básico
FOR i IN 1 TO 10 LOOP
INSERT INTO log SELECT i FROM (VALUES (1)) AS x;
END LOOP;
-- FOR + EXIT WHEN
FOR i IN 1 TO 100 LOOP
EXIT WHEN i = 5;
END LOOP;
-- FOR en procedure
CREATE PROCEDURE process_n(p_max INT) AS BEGIN
DECLARE total INT DEFAULT 0;
FOR i IN 1 TO p_max LOOP
SET total = total + i;
END LOOP;
END;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4111] <msg-del-user> |
RAISE EXCEPTION 'msg' disparada por el código. |
[GBY-4112] RAISE: se esperaba un literal STRING |
RAISE con ident o número en lugar de string. |
[GBY-4113] FOR ...: start/end debe ser INT |
bounds non-INT. |
⚠️ No soportado todavía
EXCEPTION WHEN ... THENhandlers (BEGIN ... EXCEPTION ... END) — X4d.FOR row IN SELECT ... LOOP(resultset iteration) — X4d.LOOP ... END LOOPstandalone (sin WHILE/FOR) — X4d.RETURN exprdentro de functions — X4d.STEP nyREVERSE(descendente) en FOR — futuro.- Formato
%en RAISE (RAISE EXCEPTION 'value % invalid', x) — futuro. RAISE WARNING/INFO— futuro.
EXCEPTION handlers + LOOP standalone (bloque X4d)
BEGIN <body> [EXCEPTION WHEN OTHERS THEN <handler>] END: try/catch con catch-all (X4d sólo soportaWHEN OTHERS).LOOP <body> END LOOPstandalone: infinite hastaEXIToMAX_LOOP_ITERATIONS.
📜 EBNF
block_stmt ::= "BEGIN" stmt_list [ "EXCEPTION" "WHEN" "OTHERS" "THEN" stmt_list ] "END"
loop_stmt ::= "LOOP" stmt_list "END" "LOOP"
🧠 Semántica
BEGIN..END:
- Lookahead distingue
BEGIN [TRANSACTION];(transaction) deBEGIN <stmt>...(block). - Body ejecuta en orden. Si alguno rebota:
- EXIT sentinel → re-propagar (debe alcanzar el LOOP outer).
- Sin handler → propagar error.
- Con handler → ejecutar handler en su lugar, retornar OK con
"EXCEPTION caught: <orig>".
WHEN OTHERSes catch-all — atrapa cualquier error (RAISE, runtime, PK dup, etc.).- Re-raise condicional posible dentro del handler:
RAISE EXCEPTION 'new msg'.
LOOP standalone:
- Infinite hasta EXIT (sentinel) o hit
MAX_LOOP_ITERATIONS = 100_000. - Sin EXIT →
[GBY-4109].
✅ Ejemplos
-- Try/catch básico
BEGIN
INSERT INTO t (id) VALUES (1);
EXCEPTION WHEN OTHERS THEN
INSERT INTO log VALUES (now(), 'failed');
END;
-- LOOP standalone
DECLARE i INT DEFAULT 0;
LOOP
SET i = i + 1;
EXIT WHEN i = 100;
END LOOP;
-- EXCEPTION en trigger body (no aborta el INSERT principal)
CREATE TRIGGER safe AFTER INSERT ON orders FOR EACH ROW BEGIN
BEGIN
UPDATE counters SET n = n + 1 WHERE id = NEW.id;
EXCEPTION WHEN OTHERS THEN
INSERT INTO err_log VALUES (NEW.id);
END;
END;
-- EXCEPTION dentro de WHILE (handler corre cada iteración)
DECLARE i INT DEFAULT 0;
DECLARE caught INT DEFAULT 0;
WHILE i < 5 LOOP
SET i = i + 1;
BEGIN
RAISE EXCEPTION 'iter %', i;
EXCEPTION WHEN OTHERS THEN
SET caught = caught + 1;
END;
END LOOP;
-- caught == 5
-- LOOP con condición compleja vía EXIT WHEN
DECLARE done INT DEFAULT 0;
LOOP
UPDATE jobs SET status = 'processing' WHERE id IN
(SELECT id FROM jobs WHERE status = 'pending' LIMIT 10);
SET done = (SELECT COUNT(*) FROM jobs WHERE status = 'pending');
EXIT WHEN done = 0;
END LOOP;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4114] BEGIN ...: se esperaba END para cerrar |
Falta END. |
[GBY-4114] EXCEPTION WHEN ...: solo se soporta OTHERS en X4d |
WHEN <code> específico (diferido). |
[GBY-4115] LOOP ...: se esperaba END LOOP |
Falta END LOOP. |
[GBY-4109] LOOP standalone: superó 100000 iter |
Loop infinito sin EXIT. |
⚠️ No soportado todavía
EXCEPTION WHEN <code>filtros específicos (WHEN no_data_found, etc.) — X4e.FOR row IN SELECT ... LOOP— X4e.RETURN expren functions — X4e.CASEstatement — diferido.- Múltiples
WHENbranches en EXCEPTION.
CASE statement + EXCEPTION filtrada (bloque X4e)
CASE WHEN cond THEN <stmts> [WHEN cond THEN <stmts>]* [ELSE <stmts>] END CASE— statement-level (vs CASE expression que vive en SELECT list).EXCEPTION WHEN <code> THEN <handler>— filtros por código numérico en BEGIN..END handlers, múltiples WHEN encadenados con OTHERS fallback opcional.
📜 EBNF
case_stmt ::= "CASE" "WHEN" expr "THEN" stmt_list
{ "WHEN" expr "THEN" stmt_list }*
[ "ELSE" stmt_list ]
"END" "CASE"
exc_handler ::= "WHEN" ( integer | "OTHERS" ) "THEN" stmt_list
🧠 Semántica
CASE statement:
- Searched form solo (no operando inicial — usar
CASE WHEN x = v THEN ...). - Semánticamente idéntico a
IF/ELSIF/ELSE/END IF. - Primer WHEN cuya cond evalúe TRUE corre; resto se ignora.
- ELSE corre si ninguna WHEN matchea. Sin ELSE + sin match → no-op.
EXCEPTION WHEN <code>:
- Filtro = literal entero (
4111,3001, etc.) — el código[GBY-NNNN]sin prefijo. - Múltiples WHEN encadenados → primer filter que matchee el código del error gana.
OTHERSmatchea cualquier error no atrapado por filtros previos. Va al final.- Sin handler match → re-propaga el error.
✅ Ejemplos
-- CASE statement con clasificación
DECLARE amt INT DEFAULT 500;
CASE
WHEN amt >= 1000 THEN INSERT INTO platinum VALUES (id);
WHEN amt >= 100 THEN INSERT INTO gold VALUES (id);
WHEN amt >= 10 THEN INSERT INTO silver VALUES (id);
ELSE INSERT INTO bronze VALUES (id);
END CASE;
-- EXCEPTION filtrada por código
BEGIN
INSERT INTO orders (id, amount) VALUES (1, -5);
EXCEPTION
WHEN 3008 THEN -- CHECK violation
INSERT INTO bad_orders (id) VALUES (1);
WHEN 3001 THEN -- duplicate PK
UPDATE orders SET amount = -5 WHERE id = 1;
WHEN OTHERS THEN
INSERT INTO err_log (msg) VALUES ('unknown');
END;
-- CASE dentro de procedure
CREATE PROCEDURE tier(p_id INT, p_amt INT) AS BEGIN
CASE
WHEN p_amt >= 100 THEN INSERT INTO tier_log VALUES (p_id, 'gold');
WHEN p_amt >= 50 THEN INSERT INTO tier_log VALUES (p_id, 'silver');
ELSE INSERT INTO tier_log VALUES (p_id, 'bronze');
END CASE;
END;
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4116] CASE statement: se esperaba WHEN |
Falta la primera branch (searched form requiere WHEN). |
[GBY-4116] CASE statement: se esperaba END CASE |
Falta el cierre. |
[GBY-4117] EXCEPTION WHEN: se esperaba OTHERS o código entero |
Filtro inválido (ident, string, etc.). |
⚠️ No soportado todavía
EXCEPTION WHEN <name>filtros simbólicos (WHEN no_data_found,WHEN unique_violation) — X4f.CASE expr WHEN val THEN ...simple form como statement — diferido.- Múltiples códigos en un mismo WHEN (
WHEN 3001 OR 3002 THEN) — diferido.
RETURN en function bodies (bloque X4f)
CREATE FUNCTION ... AS BEGIN ... RETURN expr; END— function bodies multi-statement conRETURN exprcomo mecanismo de salida. La forma single-expression body de X3b (AS x + 1) sigue funcionando.
📜 EBNF
create_function ::= "CREATE" "FUNCTION" ident "(" [ param { "," param }* ] ")"
"RETURNS" type "AS" function_body
function_body ::= expr
| "BEGIN" stmt_list "END"
return_stmt ::= "RETURN" expr
🧠 Semántica
- El parser detecta
BEGINjusto después deASy conmuta a body multi-statement (depth tracking idéntico a procedure/trigger). RETURN exprevalúa la expresión y termina la function devolviendo ese valor al caller.- RETURN burbujea a través de IF/WHILE/FOR/CASE/BEGIN sin código extra (mismo patrón de sentinel que EXIT).
- Si la function llega al final sin RETURN, devuelve
NULL. RETURNfuera de un function body multi-statement →[GBY-4118].- Funciones que llaman funciones funcionan correctamente — cada invocación snapshot-tea/restaura su pending RETURN.
✅ Ejemplos
-- IF con early return
CREATE FUNCTION sign(x INT) RETURNS TEXT AS BEGIN
IF x < 0 THEN
RETURN 'negative';
ELSIF x = 0 THEN
RETURN 'zero';
ELSE
RETURN 'positive';
END IF;
END;
SELECT sign(-3), sign(0), sign(7); -- 'negative', 'zero', 'positive'
-- Variables locales + loop + RETURN
CREATE FUNCTION sum_to(n INT) RETURNS INT AS BEGIN
DECLARE i INT DEFAULT 1;
DECLARE total INT DEFAULT 0;
WHILE i <= n LOOP
SET total = total + i;
SET i = i + 1;
END LOOP;
RETURN total;
END;
SELECT sum_to(10); -- 55
-- Composición de functions
CREATE FUNCTION dbl(x INT) RETURNS INT AS x * 2;
CREATE FUNCTION quad(x INT) RETURNS INT AS BEGIN
RETURN dbl(dbl(x));
END;
SELECT quad(3); -- 12
❌ Errores típicos
| Mensaje | Causa |
|---|---|
[GBY-4118] RETURN fuera de function body |
RETURN expr usado en script top-level o dentro de procedure. |
⚠️ No soportado todavía
RETURNS TABLE(table-valued functions) — diferido.RETURN QUERY <select>estilo PL/pgSQL — diferido.OUT/INOUTparameters como mecanismo de retorno alternativo — diferido.
INTEGRITY CHECK
Recorre la DB abierta y reporta toda inconsistencia detectable: páginas con CRC inválido, filas no decodificables, entradas de índice secundario huérfanas (apuntan a PKs que ya no existen) y FKs huérfanas (valor no NULL sin parent). De solo lectura — no modifica nada.
🛤️ Railroad
flowchart LR
S([▶]) --> I[INTEGRITY] --> C[CHECK] --> SEMI[";"] --> E([■])
📜 EBNF
integrity_check ::= "INTEGRITY" "CHECK"
✅ Forma del resultado
INTEGRITY CHECK devuelve un ResultSet con columnas kind, object, detail — una fila por hallazgo. El campo message resume:
- DB sana:
OK · N tablas · M filas · K índices · F FKs · P páginas - DB con hallazgos:
FAIL · H hallazgos · ...
Ejemplo de respuesta sin hallazgos:
{
"columns": ["kind", "object", "detail"],
"rows": [],
"message": "OK · 2 tablas · 4 filas · 1 índices · 2 FKs · 4 páginas"
}
🏷️ Categorías de hallazgo
kind |
Significado |
|---|---|
page_corrupt |
El pager rechazó la página al cargarla (CRC mismatch o lectura corta). |
row_decode |
Los bytes de la fila no se ajustan al esquema actual de la tabla. Muy raro — solo ocurre si el encoder y el decoder se desincronizan. |
orphan_index_entry |
Una entrada de un índice secundario apunta a una PK que ya no existe en la tabla. |
fk_target_missing |
Una columna declara REFERENCES contra una tabla que ya no existe. |
fk_orphan |
Un valor no nulo de una columna FK no tiene parent en la tabla referenciada. |
Recomendado correrlo después de un crash, después de restaurar un backup, o como sanity check periódico. La complejidad es O(filas + entradas_de_índice + filas_con_FK), totalmente secuencial.
🧠 Combinaciones útiles dentro de una transacción
gabysql permite múltiples sentencias separadas por ; en un solo /exec HTTP o en un solo gabysql exec ... "...". Todas viajan dentro de la misma transacción: si una falla, todas se revierten.
-- Receta común: schema + datos seed + índice + verify, todo atómico
CREATE TABLE products (id INT PRIMARY KEY, sku TEXT, price FLOAT);
INSERT INTO products (id, sku, price) VALUES (1, 'A-001', 99.0);
INSERT INTO products (id, sku, price) VALUES (2, 'A-002', 149.5);
CREATE INDEX idx_products_sku ON products (sku);
SELECT * FROM products WHERE sku = 'A-001';
Si la 4ª sentencia (CREATE INDEX) falla por el motivo que sea, las 3 anteriores también se revierten: la DB queda en el estado previo.
🔭 Lo que no está implementado todavía
flowchart LR
A([Camino A]) --> NOT_NULL[NOT NULL] --> UNIQUE[UNIQUE] --> DEFAULT[DEFAULT]
DEFAULT --> ORDER_BY[ORDER BY indexado] --> COMPOSITE[Índice compuesto]
A --> EXPLAIN[EXPLAIN]
B([Camino B]) --> ALTER[ALTER TABLE] --> PREP[Prepared statements]
C([Camino C]) --> JOIN[JOIN] --> GROUP[GROUP BY] --> SUB[Subqueries] --> CTE[CTE / window]
Cada bucket vive en su camino correspondiente del COMMERCIAL_ROADMAP. Si necesitas alguno con prioridad para un caso de uso real, abre un Issue describiendo el escenario.