Cada sentència recorre el mateix pipeline curt:

lexer tokens parser descens recursiu AST planificador escaneig vs cerca puntual executor files
Cinc etapes escrites a mà, sense parser ni optimitzador externs. Les decisions del planificador són deterministes.

Tipus

Tipus SQL Value Notes
INTEGER Integer(i64) INTEGER PRIMARY KEY ⇒ àlies del rowid (a l’estil de SQLite)
REAL Real(f64)
TEXT Text(String) validat com a UTF-8
BLOB Blob(Vec<u8>) literals x'…'
BOOLEAN Bool(bool) TRUE / FALSE
Null

Restriccions de columna: PRIMARY KEY, NOT NULL, DEFAULT <literal>, UNIQUE, CHECK (expr) i REFERENCES (claus foranes). El text de CHECK es desa al catàleg (esquema v7) i es torna a analitzar quan s’avalua.

Sentències

-- DDL
CREATE TABLE [IF NOT EXISTS] t (col TYPE [constraints], … [, table_constraint …]);
  -- column-level: PRIMARY KEY | NOT NULL | DEFAULT v | UNIQUE | CHECK (expr) | REFERENCES parent […]
  -- table-level:  UNIQUE (c…) | CHECK (expr) | FOREIGN KEY (c…) REFERENCES parent (p…) […]
CREATE TABLE [IF NOT EXISTS] t AS SELECT …;        -- CTAS: column names + types from the query
DROP TABLE [IF EXISTS] t;
ALTER TABLE t ADD [COLUMN] col TYPE [DEFAULT v] [NOT NULL];  -- appended; never rewrites rows
ALTER TABLE t MOVE COLUMN col {FIRST | BEFORE x | AFTER x};  -- logical reorder (presentation only)
ALTER TABLE t REORDER COLUMNS (col, …);                     -- fix the whole logical order
ALTER TABLE t RENAME [COLUMN] old TO new;                   -- metadata only
ALTER TABLE t DROP [COLUMN] col;                            -- logical DROP (tombstone); no rewrite
CREATE VIEW [IF NOT EXISTS] v AS <select>;                  -- a stored, named SELECT
DROP VIEW [IF EXISTS] v;
CREATE TRIGGER [IF NOT EXISTS] tr {BEFORE|AFTER|INSTEAD OF} {INSERT|UPDATE|DELETE} ON t
  [FOR EACH {ROW|STATEMENT}] BEGIN <dml>; … END;            -- body with OLD./NEW.
DROP TRIGGER [IF EXISTS] tr;

-- DML
INSERT INTO t [(cols)] {VALUES (expr, …)[, …] | SELECT …}
  [ON CONFLICT [(cols)] DO {NOTHING | UPDATE SET col = expr [, …] [WHERE expr]}]
  [RETURNING list | *];
UPDATE t SET col = expr [, …] [WHERE expr] [RETURNING list | *];
DELETE FROM t [WHERE expr] [RETURNING list | *];

-- Query
[WITH [RECURSIVE] cte AS (SELECT …) [, …]]        -- CTEs; RECURSIVE supported
SELECT [DISTINCT] list | *
  [FROM {table | (SELECT …)} [alias]              -- FROM is optional; a subquery here is a derived table
  [{INNER|LEFT} JOIN src2 ON expr]]               -- nested-loop, predicate pushdown
  [WHERE expr]
  [GROUP BY e1, … [HAVING cond]]
  [{UNION [ALL] | INTERSECT | EXCEPT} SELECT …]    -- set operators; ALL keeps duplicates
  [ORDER BY {col | expr | alias | ordinal} [ASC|DESC] [, …]]
  [LIMIT n [OFFSET m]]
  [AS OF {VERSION n | TIMESTAMP 'rfc3339'}];       -- arkeion extension

-- Transactions
BEGIN; COMMIT; ROLLBACK;
SAVEPOINT s;  ROLLBACK TO [SAVEPOINT] s;  RELEASE [SAVEPOINT] s;   -- inside a BEGIN

Consultes

Joins. Joins INNER i LEFT, avaluats amb nested-loop i amb pushdown de predicats (un WHERE que restringeix el costat intern s’aplica a mesura que es produeixen les files, no pas després).

Operadors de conjunts. UNION (deduplica), UNION ALL (conserva els duplicats), INTERSECT (files presents als dos costats) i EXCEPT (files del costat esquerre acumulat que no són al dret). Es combinen amb associativitat per l’esquerra; un ORDER BY després d’una operació de conjunts ordena per columna de sortida o per ordinal.

Subconsultes — escalars (SELECT …), x [NOT] IN (SELECT …) i [NOT] EXISTS (SELECT …). Les subconsultes correlacionades són compatibles: quan la consulta interna referencia una columna de la fila externa, es torna a avaluar per cada fila externa amb els valors d’aquella fila. Una subconsulta escalar que retorni més d’una fila és un error; zero files és NULL.

Taules derivades — un (SELECT …) dins de la clàusula FROM, opcionalment amb àlies, es tracta com una taula i es pot unir i filtrar com qualsevol altra font.

CTE (WITH n AS (SELECT …)) — taules amb nom, materialitzades i visibles per a la consulta que ve a continuació; cadascuna veu les anteriors i eclipsa una taula real amb el mateix nom. WITH RECURSIVE executa una CTE recursiva: un cas base i després un pas que referencia la mateixa CTE, iterat fins que no produeix cap fila nova (UNION deduplica la frontera, UNION ALL conserva totes les files).

Vistes (CREATE VIEW v AS SELECT …) — com una CTE però persistent: el text del SELECT es desa al catàleg i es materialitza en llegir, de manera que sempre reflecteix les dades actuals. Una vista sobre una altra vista funciona, i els noms de vista no col·lideixen mai amb els de taula. La definició d’una vista està versionada, així que un AS OF anterior a la seva creació no la veu.

AgregatsCOUNT(*), COUNT(col), SUM, AVG, MIN, MAX, GROUP_CONCAT(x[, sep]), tots compatibles amb DISTINCT (COUNT(DISTINCT x), …). SELECT DISTINCT deduplica les files projectades. MIN/MAX també tenen una forma escalar amb ≥2 arguments (MIN(a, b, …)); amb un sol argument són agregats, com a SQLite.

GROUP BY / HAVING — agrupa pel valor d’unes expressions (normalment columnes) i emet una fila per grup, plegant els agregats de la projecció sobre cada grup. Una columna de la projecció que no sigui agregada ha d’aparèixer al GROUP BY. HAVING filtra els grups agregats.

Funcions de finestrafunc(args) OVER ([PARTITION BY …] [ORDER BY …]) dins de la llista del SELECT, calculades sobre el conjunt filtrat abans de l’ORDER BY/LIMIT exterior. Disponibles: ROW_NUMBER(), RANK(), DENSE_RANK(), NTILE(k), LAG, LEAD, FIRST_VALUE, LAST_VALUE i SUM/COUNT/AVG/MIN/MAX com a agregats de finestra. Marcs explícits ROWS BETWEEN start AND end (ROWS físics, no RANGE) amb UNBOUNDED {PRECEDING|FOLLOWING}, N {PRECEDING|FOLLOWING} i CURRENT ROW.

SELECT sense FROM avalua expressions constants contra una única fila implícita (SELECT 1 + 1, SELECT UPPER('hi') AS g). Un WHERE constant la pot filtrar. Sense taula, la projecció rebutja columnes, *, agregats i clàusules que necessiten files (JOIN/GROUP BY/HAVING/ORDER BY/AS OF).

Escriptures

INSERT / UPSERTINSERT … ON CONFLICT [(cols)] DO {NOTHING | UPDATE SET …}. Si una fila col·lideix amb la PK o amb un índex UNIQUE, s’omet (DO NOTHING) o bé s’actualitza la fila existent (DO UPDATE). Dins de DO UPDATE, SET/WHERE veuen la fila existent i excluded.col = la fila proposada; un WHERE fals descarta aquella actualització.

RETURNINGINSERT/UPDATE/DELETE … RETURNING list | * retorna les files afectades (les files inserides, les actualitzades amb els seus valors NEW, o les esborrades) en comptes de només un recompte. Executa-ho amb query; amb execute l’escriptura es fa igualment, però les files es descarten.

Claus foranescol REFERENCES parent[(col)] o, a nivell de taula, FOREIGN KEY (a, b) REFERENCES parent (x, y) (composta), amb [ON DELETE action] [ON UPDATE action] i action ∈ {RESTRICT | CASCADE | SET NULL}. Les columnes referenciades han de ser la PK del pare o estar cobertes per un índex UNIQUE. Es comproven en INSERT/UPDATE; l’acció es dispara en el DELETE/UPDATE del pare. Les autoreferències (arbres) no són cap problema.

Triggers{BEFORE|AFTER|INSTEAD OF} {INSERT|UPDATE|DELETE}, FOR EACH {ROW|STATEMENT}, amb un cos d’INSERT/UPDATE/DELETE. FOR EACH ROW (per defecte) es dispara un cop per cada fila afectada amb OLD./NEW. lligats; FOR EACH STATEMENT es dispara un sol cop. INSTEAD OF (només per a vistes) fa que una vista sigui escrivible traduïnt l’escriptura a les taules base. Les escriptures reentrants estan protegides contra la recursió descontrolada — la base per a registres d’auditoria a prova de manipulacions.

Canvis lògics d’esquemaMOVE COLUMN / REORDER COLUMNS només canvien l’ordre de presentació (l’expansió de * i els INSERT posicionals); RENAME COLUMN només canvia el nom; DROP COLUMN posa una làpida a la columna (deixa d’aparèixer a * i pel seu nom) però en congela els bytes físics. Tots són O(1) i no reescriuen mai cap fila, de manera que el viatge en el temps es manté intacte — un AS OF anterior al canvi veu l’esquema de la seva època, perquè el catàleg està versionat al mateix b-tree que les dades. Els bytes morts els recupera el vacuum.

Expressions

expr    := or
or      := and ( OR and )*
and     := not ( AND not )*
not     := [NOT] cmp
cmp     := add ( ( = | != | <> | < | <= | > | >= | LIKE | IS [NOT] NULL ) add )?
add     := mul ( ( + | - ) mul )*
mul     := unary ( ( * | / | % ) unary )*
unary   := [-] primary
primary := literal | column | table.column | ?N | :name | ( expr ) | function(args)
  • Paràmetres: posicionals ?1, ?2, … o amb nom :name (repetibles); no es barregen dins d’una mateixa sentència.
  • LIKE amb % / _, sensible a majúscules i minúscules.
  • Comparar tipus diferents és un error de tipus, no pas una coerció silenciosa (el comportament sorprenent es tracta com un bug) — excepte INTEGERREAL, que es promociona.
  • Divisió / mòdul per zero → NULL (compatibilitat amb SQLite). Un resultat NaN (p. ex. inf - inf) es normalitza a NULL; ±inf es conserva. El desbordament d’enters és un error (+/-/*, i i64::MIN / -1).
  • Operadors i formes: || (concatenació), CAST(x AS type), CASE WHEN … THEN … [ELSE …] END, x [NOT] BETWEEN a AND b.

Funcions escalars (no distingeixen majúscules de minúscules; NULL es propaga si no s’indica el contrari):

  • Text: UPPER, LOWER, LENGTH/CHAR_LENGTH, TRIM/LTRIM/RTRIM, SUBSTR/SUBSTRING, REPLACE, INSTR, REVERSE, HEX, CONCAT, CONCAT_WS, LPAD/RPAD, UNICODE, CHAR, QUOTE, PRINTF/FORMAT, GLOB.
  • Numèriques: ABS, ROUND, CEIL/CEILING, FLOOR, TRUNC, SQRT, POW/POWER, MOD, SIGN, EXP, LN, LOG/LOG10/LOG2/LOG(base, x), SIN/COS/TAN, ASIN/ACOS/ATAN/ATAN2, PI(), RADIANS/DEGREES, RANDOM (no determinista).
  • Condicionals / NULL: COALESCE, IFNULL, NULLIF, TYPEOF, IIF(c, a, b).
  • Data / hora: NOW, DATE, TIME, DATETIME, STRFTIME, JULIANDAY, UNIXEPOCH. L’enter de temps són mil·lisegons d’època en UTC — la mateixa unitat que les marques de temps d’auditoria.
  • JSON (a l’estil de JSON1, en Rust pur): JSON, JSON_VALID, JSON_TYPE, JSON_EXTRACT, JSON_ARRAY_LENGTH, JSON_OBJECT, JSON_ARRAY, JSON_QUOTE, més json -> key (resultat com a JSON) i json ->> key (resultat com a valor SQL).

L’extensió AS OF

AS OF és una extensió d’Arkeion que tanca un SELECT (després de totes les altres clàusules). Fixa tota la sentència a un únic snapshot històric — no hi ha barreges per taula.

SELECT total, status FROM invoices WHERE id = 7 AS OF VERSION 1042;
SELECT * FROM invoices AS OF TIMESTAMP '2026-05-01T00:00:00Z';
  • AS OF VERSION n — exacte; un error VersionNotFound si n s’ha compactat.
  • AS OF TIMESTAMP t — es resol a la versió més alta amb marca de temps de commit ≤ t. La marca de temps és informativa; la versió és l’autoritat.
  • Només SELECT. No es pot escriure al passat — per a això hi ha les branques.

L’executor és genèric respecte de la seva font de dades, així que fixa l’snapshot un sol cop i ho avalua tot contra ell: joins, subconsultes (incloses les correlacionades), CTE (incloses les recursives), operadors de conjunts, agregats, vistes i el catàleg (les columnes, l’ordre i les FK estan versionats al mateix b-tree). L’única excepció és el rellotge: now() / date('now') retornen l’hora d’execucióAS OF fa retrocedir les dades, no el rellotge de paret de la sentència.

SELECT … JOIN … WHERE (SELECT …) AS OF VERSION 1042 joins · subconsultes · agregats · vistes — tot fixa un snapshot historial de versions v1042 ara
Una clàusula, un snapshot: tota la consulta llegeix el commit fixat, així que un informe és reproduïble byte a byte.

Identificadors

Els identificadors sense cometes comencen amb una lletra o _ i continuen amb lletres, dígits o _; les lletres Unicode compten (café, 名前), com a SQLite. Les cometes dobles ("…", amb "" per a una cometa literal) permeten paraules reservades o espais i símbols: SELECT "select", "my col" FROM t. Les cometes simples són sempre una cadena de text, mai un identificador.

Fora d’abast deliberadament

Exclòs Per què
ALTER TABLE físic (canvi de tipus, DROP que reescriu files) trencaria el viatge en el temps sense una època per fila; els ADD/MOVE/REORDER/RENAME/DROP lògics cobreixen els casos habituals
Recuperació instantània d’espai en un DROP COLUMN l’esborrat lògic deixa bytes morts; el vacuum els recupera en una reescriptura
Optimitzador basat en costos (estadístiques) substituït per un planificador determinista basat en regles — índex vs. escaneig i pushdown de predicats —, de manera que els plans són reproduïbles i auditables

Codegen

El catàleg es serialitza a una representació JSON estable mitjançant Database::schema(). Els generadors de codi externs consumeixen això, no pas el text SQL: el contracte del codegen és el catàleg, no la cadena de la sentència.