123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678(** Phase 0 SQL AST.
Covers: CREATE TABLE, INSERT INTO ... VALUES, SELECT ... FROM ... WHERE col = lit.
Extended in later phases for UPDATE, DELETE, JOINs, etc. *)typety=|Ty_int(** INTEGER column type *)|Ty_text(** TEXT column type *)|Ty_real(** REAL (float64) column type *)|Ty_blob(** BLOB (bytes) column type *)typeliteral=|L_intofint64|L_textofstring|L_null|L_realoffloat|L_blobofbytes|L_current_timestamp|L_current_date|L_current_timetypeparam=|Param_anon(** ? — assigned next slot in encounter order *)|Param_indexofint(** ?1, ?2 ... — explicit 1-based slot *)|Param_nameofstring(** :name @name $name *)typeconflict_action=|CA_rollback|CA_abort|CA_fail|CA_ignore|CA_replacetypebinop=|Eq|Ne|Lt|Le|Gt|Ge(** comparison *)|Add|Sub|Mul|Div(** arithmetic *)|And|Or(** logical *)|Concat(** string concatenation || *)|Mod(** modulo % *)|Bit_and|Bit_or(** bitwise & | *)|Lshift|Rshift(** shift << >> *)|Like|Glob(** pattern matching *)(** Aggregate functions supported in Phase 2 Task 6. *)typeagg_func=|Agg_count|Agg_sum|Agg_avg|Agg_min|Agg_max|Agg_group_concatofstringoption(** GROUP_CONCAT(col) → None (default sep ","); GROUP_CONCAT(col, 'sep') → Some sep *)(** Scalar functions supported in Phase 5 Task 1. *)typescalar_func=|Fn_length|Fn_lower|Fn_upper|Fn_abs|Fn_coalesce|Fn_ifnull|Fn_substr(** SUBSTR(s, start[, len]) — 1-indexed *)|Fn_trim|Fn_ltrim|Fn_rtrim(** TRIM, LTRIM, RTRIM — optional 2nd arg for chars *)|Fn_replace(** REPLACE(s, old, new) *)|Fn_instr(** INSTR(s, sub) → 1-indexed position or 0 *)|Fn_round(** ROUND(n[, digits]) *)|Fn_typeof(** TYPEOF(x) → 'integer'|'real'|'text'|'blob'|'null' *)|Fn_date(** DATE(ts[, mod...]) → 'YYYY-MM-DD' *)|Fn_time(** TIME(ts[, mod...]) → 'HH:MM:SS' *)|Fn_datetime(** DATETIME(ts[, mod...]) → 'YYYY-MM-DD HH:MM:SS' *)|Fn_strftime(** STRFTIME(fmt, ts[, mod...]) → formatted string *)|Fn_julianday(** JULIANDAY(ts[, mod...]) → float *)|Fn_unixepoch(** UNIXEPOCH(ts[, mod...]) → integer *)|Fn_ceil(** CEIL(x) / CEILING(x) — round up *)|Fn_floor(** FLOOR(x) — round down *)|Fn_sqrt(** SQRT(x) — square root *)|Fn_pow(** POW(x,y) / POWER(x,y) — x^y *)|Fn_exp(** EXP(x) — e^x *)|Fn_ln(** LN(x) — natural log *)|Fn_log(** LOG(x) → ln x; LOG(B,x) → log base B of x *)|Fn_log2(** LOG2(x) — log base 2 *)|Fn_log10(** LOG10(x) — log base 10 *)|Fn_sign(** SIGN(x) → -1 | 0 | 1 as integer *)|Fn_trunc(** TRUNC(x[,d]) — truncate toward zero *)|Fn_pi(** PI() — constant π, zero args *)|Fn_sin(** SIN(x) *)|Fn_cos(** COS(x) *)|Fn_tan(** TAN(x) *)|Fn_asin(** ASIN(x) *)|Fn_acos(** ACOS(x) *)|Fn_atan(** ATAN(x) *)|Fn_atan2(** ATAN2(y,x) — two-argument arctangent *)|Fn_degrees(** DEGREES(x) — radians to degrees *)|Fn_radians(** RADIANS(x) — degrees to radians *)|Fn_json_extract(** json_extract(json, path) *)|Fn_json_object(** json_object(k,v,...) *)|Fn_json_array(** json_array(v,...) *)|Fn_json_type(** json_type(json[,path]) *)|Fn_json_valid(** json_valid(json) → 0|1 *)|Fn_json_set(** json_set(json, path, val[, path, val ...]) *)|Fn_json_insert(** json_insert — insert only if absent *)|Fn_json_replace(** json_replace — update only if present *)|Fn_json_remove(** json_remove(json, path[, path ...]) *)|Fn_hex(** HEX(x) — hex encoding of blob, text, or integer *)|Fn_char(** CHAR(x,...) — Unicode code points to UTF-8 string *)|Fn_unicode(** UNICODE(s) — first Unicode code point of string, or NULL *)|Fn_printf(** PRINTF(fmt,...) / FORMAT(fmt,...) — printf-style formatting *)|Fn_zeroblob(** ZEROBLOB(n) — blob of n zero bytes *)|Fn_random(** RANDOM() — random 64-bit integer *)|Fn_randomblob(** RANDOMBLOB(n) — random blob of n bytes *)|Fn_changes(** CHANGES() — rows affected by last DML *)|Fn_last_insert_rowid(** LAST_INSERT_ROWID() — rowid of last INSERT *)|Fn_total_changes(** TOTAL_CHANGES() — total rows affected since connection open *)|Fn_sqlite_version(** SQLITE_VERSION() — constant text version string *)typeset_op=|Union|Union_all|Intersect|Excepttypetrigger_timing=|TT_before|TT_after|TT_instead_oftypetrigger_event=|TE_insert|TE_update|TE_deletetypeorder_dir=|Asc|Desctypecollation=|Collate_binary|Collate_nocase|Collate_rtrimtypeframe_unit=|Frame_rows|Frame_rangetypeframe_bound=|FB_unbounded_preceding|FB_precedingofint|FB_current_row|FB_followingofint|FB_unbounded_followingtypeframe_spec={unit:frame_unit;start:frame_bound;end_:frame_bound}typejoin_kind=|Inner|Lefttypefk_action=Granary_catalog.Catalog.fk_action=|FA_no_action|FA_restrict|FA_cascade|FA_set_null|FA_set_defaulttypetable_constraint=|TC_uniqueofstringlist(** UNIQUE(col1, col2, ...) *)|TC_primary_keyof{pk_cols:stringlist(** PRIMARY KEY(col1, col2, ...) *);autoincrement:bool(** #312: AUTOINCREMENT appeared on a column inside the table-level
PRIMARY KEY(...). Only legal on a single-column INTEGER PK
(validated in {!Granary_sql.Sema}); composite is rejected. *)}|TC_foreign_keyof{local_cols:stringlist;parent_table:string;parent_cols:stringlist;on_delete:fk_action;on_update:fk_action;deferrable:bool}(** FOREIGN KEY(local_cols) REFERENCES parent_table(parent_cols) ON DELETE/UPDATE action *)typerefresh_mode=|Refresh_auto|Refresh_full|Refresh_delta(** Expressions, statements, and column_def are mutually recursive because
column_def.check embeds an [expr], and subquery expressions embed a [stmt]. *)typeexpr=|E_litofliteral|E_colofstring(** unqualified column reference *)|E_tbl_colofstring*string(** qualified: table.col *)|E_binopofbinop*expr*expr|E_notofexpr|E_is_nullofexpr|E_is_not_nullofexpr|E_negofexpr(** unary minus *)|E_bitnotofexpr(** bitwise NOT ~ *)|E_betweenofexpr*expr*expr(** subject BETWEEN lo AND hi *)|E_inofexpr*exprlist(** subject IN (val1, val2, ...) *)|E_aggofagg_func*exproption(** Aggregate call; [None] argument means [COUNT( * )]. *)|E_funcofscalar_func*exprlist(** Scalar function call. *)|E_paramofparam(** parameter: ?, ?1, :name, @name, $name *)|E_matchofstring*string(** [E_match (table_name, query_string)]: [WHERE table MATCH 'query'] *)|E_subqueryofstmt(** scalar subquery: (SELECT ...) in expr position *)|E_existsofstmt(** EXISTS (SELECT ...) *)|E_in_selectofexpr*stmt(** x IN (SELECT ...) *)|E_caseof{scrutinee:exproption(** None = searched form, Some = simple form *);branches:(expr*expr)list(** (WHEN condition/value, THEN result) *);else_:exproption}|E_castofexpr*ty(** CAST(expr AS type) — SQLite type coercion *)|E_windowof{func:window_func;args:exprlist;window:window_spec}(** Window function call: FUNC(...) OVER (PARTITION BY ... ORDER BY ...) *)|E_collateofexpr*collation(** expr COLLATE collation_name *)|E_fts_snippetof{table:string;col_idx:int;start_tag:string;end_tag:string;ellipsis:string;n_tokens:int}(** snippet(table, col_idx, start_tag, end_tag, ellipsis, n_tokens) *)andwindow_func=|WF_row_number|WF_rank|WF_dense_rank|WF_ntile|WF_lag|WF_lead|WF_first_value|WF_last_value|WF_nth_value|WF_aggofagg_func|WF_percent_rank|WF_cume_distandwindow_spec={partition_by:exprlist;order_by:order_keylist;frame:frame_specoption}andorder_key={expr:expr;dir:order_dir;nulls:[`Nulls_first|`Nulls_last]option}andjoin_clause={kind:join_kind;table:string(** right-side table name *);alias:stringoption(** optional alias — used in E_tbl_col resolution *);on:expr(** join condition (predicate over both tables) *)}andupsert_update={conflict_cols:stringlist;assignments:(string*expr)list}andgroup_by_item=string*stringoptionandstmt=|S_create_tableof{name:string;columns:column_deflist;constraints:table_constraintlist;if_not_exists:bool;without_rowid:bool;using_columnstore:bool}|S_insertof{table:string;columns:stringlist(** named columns; empty = "all in order" *);values:exprlistlist(** one inner list per VALUES row *);on_conflict:conflict_actionoption;returning:exprlist(** empty = no RETURNING *);upsert_update:upsert_updateoption}|S_insert_selectof{table:string;columns:stringlist(** empty = all non-generated columns *);on_conflict:conflict_actionoption;select:stmt}|S_selectof{distinct:bool;proj:[`All|`Colsofstringlist|`Exprsof(expr*stringoption)list](** [`Exprs] supports arbitrary projection expressions (used for
aggregates). Plain column projection still parses to
[`Cols]. *);table:string;table_alias:stringoption(** optional AS alias for the FROM table *);joins:join_clauselist(** empty list = no joins *);where:exproption;group_by:group_by_itemlist(** column names; empty = no GROUP BY *);having:exproption(** HAVING predicate (may reference aggregates) *);order:order_keylist(** empty = no ORDER BY *);limit:intoption;offset:intoption}|S_create_indexof{name:string;table:string;columns:exprlist(* E_col "name" for plain cols, any expr for expression indexes *);where_clause:exproption;unique:bool;if_not_exists:bool}|S_updateof{table:string;assignments:(string*expr)list(** [(col_name, new_value_expr)] *);where:exproption;order:order_keylist;limit:intoption;offset:intoption;returning:exprlist}|S_deleteof{table:string;where:exproption;order:order_keylist;limit:intoption;offset:intoption;returning:exprlist}|S_drop_tableof{name:string;if_exists:bool}|S_drop_indexof{name:string;if_exists:bool}|S_alter_tableof{table:string;action:alter_action}|S_begin|S_commit|S_rollback|S_savepointofstring(** SAVEPOINT name *)|S_releaseofstring(** RELEASE name *)|S_rollback_toofstring(** ROLLBACK TO name *)|S_compoundof{op:set_op;left:stmt;right:stmt;order:order_keylist(** ORDER BY applied to the combined result; empty if absent.
Parser lifts a trailing ORDER BY from the right arm so it
binds at the compound level (SQL semantics). *);limit:intoption;offset:intoption}|S_create_fts_tableof{name:string;columns:stringlist}|S_pragmaofpragma_kind|S_const_selectof{exprs:(expr*stringoption)list}(** FROM-less SELECT that evaluates constant expressions — returns one row.
Used when a SELECT has no FROM clause.
Each entry pairs the expression with an optional column alias. *)|S_with_cteof{name:string;def:stmt;query:stmt;recursive:bool}|S_create_viewof{name:string;query:stmt}|S_create_reactive_viewof{name:string;query:stmt;refresh:refresh_mode}|S_drop_viewof{name:string;if_exists:bool}|S_create_triggerof{name:string;timing:trigger_timing;event:trigger_event;table:string;when_:exproption(** WHEN clause; None if absent *);body:stmtlist(** statements between BEGIN…END *)}|S_drop_triggerof{name:string;if_exists:bool}|S_explainof{analyze:bool(** false = EXPLAIN; true = EXPLAIN ANALYZE *);stmt:stmt}|S_vacuum(** Compact-rebuild the database file in place (phase 37 / #120).
Executed by [Db.vacuum]; surfaces a sema error if invoked on a
non-file-backed database. *)|S_attachof{path:string(** filesystem path to the database file *);schema:string(** schema name under which to register *)}(** [ATTACH DATABASE 'path' AS schema] — phase 40 / #64.
Opens a sub-handle and registers it under [schema] for subsequent
statements addressed via [PRAGMA active_database = schema]. *)|S_detachof{schema:string(** schema name to detach *)}(** [DETACH DATABASE schema] — phase 40 / #64. Closes and removes
a previously attached sub-handle. *)andpragma_kind=|Pragma_table_infoofstring(* PRAGMA table_info(tbl) *)|Pragma_index_listofstring(* PRAGMA index_list(tbl) *)|Pragma_foreign_key_listofstring(* PRAGMA foreign_key_list(tbl) *)|Pragma_foreign_keys(* PRAGMA foreign_keys → read flag *)|Pragma_foreign_keys_setofbool(* PRAGMA foreign_keys = 0/1 → set flag *)|Pragma_recursive_triggers(* PRAGMA recursive_triggers → read flag *)|Pragma_recursive_triggers_setofbool(* PRAGMA recursive_triggers = 0/1 → set *)|Pragma_defer_foreign_keys(* PRAGMA defer_foreign_keys → read flag *)|Pragma_defer_foreign_keys_setofbool(* PRAGMA defer_foreign_keys = 0/1 → set *)|Pragma_user_version(* PRAGMA user_version → read from meta *)|Pragma_user_version_setofint64(* PRAGMA user_version = N → write *)|Pragma_journal_mode(* PRAGMA journal_mode → "delete" *)|Pragma_integrity_check(* PRAGMA integrity_check → errors or "ok" *)|Pragma_wal_checkpoint(* PRAGMA wal_checkpoint — migrate WAL → main *)|Pragma_wal_autocheckpoint(* PRAGMA wal_autocheckpoint — read threshold *)|Pragma_wal_autocheckpoint_setofint64(* PRAGMA wal_autocheckpoint = N — set per-connection threshold (0 disables) *)|Pragma_synchronous(* PRAGMA synchronous — read mode (#298) *)|Pragma_synchronous_setofstring(* PRAGMA synchronous = full|batched|off *)|Pragma_wal_batch_commits(* PRAGMA wal_batch_commits — read N (#298) *)|Pragma_wal_batch_commits_setofint64(* PRAGMA wal_batch_commits = N *)|Pragma_wal_batch_interval_ms(* PRAGMA wal_batch_interval_ms — read T (#298) *)|Pragma_wal_batch_interval_ms_setofint64(* PRAGMA wal_batch_interval_ms = T *)|Pragma_database_list(* PRAGMA database_list — list main + attached *)|Pragma_active_database(* PRAGMA active_database — read current schema name *)|Pragma_active_database_setofstring(* PRAGMA active_database = name → route subsequent stmts *)|Pragma_setofstring*string(* fallback no-op setter *)andcolumn_def={name:string;ty:ty;not_null:bool;primary_key:bool;autoincrement:bool(** [AUTOINCREMENT] on a column [PRIMARY KEY] (#299). Only ever [true] for
a single-column ascending INTEGER PRIMARY KEY on a rowid table; the
placement is validated in {!Granary_sql.Sema}. *);pk_desc:bool(** #312: [PRIMARY KEY DESC] on this column. SQLite treats an
[INTEGER PRIMARY KEY DESC] as a NON-alias (hidden rowid + real index),
not the rowid alias. Only meaningful when [primary_key] is set. *);default:literaloption(* None = no DEFAULT *);check:exproption(* None = no CHECK constraint *);fk_ref:(string*string*fk_action*fk_action*bool)option(** [(parent_table, parent_col, on_delete, on_update, deferrable)]. None = no FK. *);generated_as:(expr*[`Stored|`Virtual])option(** GENERATED ALWAYS AS (expr) [STORED | VIRTUAL]. None = not generated. *)}andalter_action=|AA_add_columnofcolumn_def|AA_rename_tableofstring(* new table name *)|AA_rename_columnofstring*string(* old_col_name * new_col_name *)|AA_drop_columnofstring(** column name to drop *)letbinop_to_sql=function|Eq->"="|Ne->"!="|Lt->"<"|Le->"<="|Gt->">"|Ge->">="|Add->"+"|Sub->"-"|Mul->"*"|Div->"/"|Mod->"%"|And->"AND"|Or->"OR"|Concat->"||"|Bit_and->"&"|Bit_or->"|"|Lshift->"<<"|Rshift->">>"|Like->"LIKE"|Glob->"GLOB";;letfunc_to_sql=function|Fn_length->"LENGTH"|Fn_lower->"LOWER"|Fn_upper->"UPPER"|Fn_abs->"ABS"|Fn_coalesce->"COALESCE"|Fn_ifnull->"IFNULL"|Fn_substr->"SUBSTR"|Fn_trim->"TRIM"|Fn_ltrim->"LTRIM"|Fn_rtrim->"RTRIM"|Fn_replace->"REPLACE"|Fn_instr->"INSTR"|Fn_round->"ROUND"|Fn_typeof->"TYPEOF"|Fn_date->"DATE"|Fn_time->"TIME"|Fn_datetime->"DATETIME"|Fn_strftime->"STRFTIME"|Fn_julianday->"JULIANDAY"|Fn_unixepoch->"UNIXEPOCH"|Fn_ceil->"CEIL"|Fn_floor->"FLOOR"|Fn_sqrt->"SQRT"|Fn_pow->"POW"|Fn_exp->"EXP"|Fn_ln->"LN"|Fn_log->"LOG"|Fn_log2->"LOG2"|Fn_log10->"LOG10"|Fn_sign->"SIGN"|Fn_trunc->"TRUNC"|Fn_pi->"PI"|Fn_sin->"SIN"|Fn_cos->"COS"|Fn_tan->"TAN"|Fn_asin->"ASIN"|Fn_acos->"ACOS"|Fn_atan->"ATAN"|Fn_atan2->"ATAN2"|Fn_degrees->"DEGREES"|Fn_radians->"RADIANS"|Fn_json_extract->"JSON_EXTRACT"|Fn_json_object->"JSON_OBJECT"|Fn_json_array->"JSON_ARRAY"|Fn_json_type->"JSON_TYPE"|Fn_json_valid->"JSON_VALID"|Fn_json_set->"JSON_SET"|Fn_json_insert->"JSON_INSERT"|Fn_json_replace->"JSON_REPLACE"|Fn_json_remove->"JSON_REMOVE"|Fn_hex->"HEX"|Fn_char->"CHAR"|Fn_unicode->"UNICODE"|Fn_printf->"PRINTF"|Fn_zeroblob->"ZEROBLOB"|Fn_random->"RANDOM"|Fn_randomblob->"RANDOMBLOB"|Fn_changes->"CHANGES"|Fn_last_insert_rowid->"LAST_INSERT_ROWID"|Fn_total_changes->"TOTAL_CHANGES"|Fn_sqlite_version->"SQLITE_VERSION";;letrecexpr_to_sql=function|E_lit(L_intn)->Int64.to_stringn|E_lit(L_texts)->Printf.sprintf"'%s'"(String.concat"''"(String.split_on_char'\''s))|E_litL_null->"NULL"|E_lit(L_realf)->Printf.sprintf"%.17g"f|E_lit(L_blob_)->failwith"expr_to_sql: BLOB literals not supported in CHECK constraints"|E_litL_current_timestamp->"CURRENT_TIMESTAMP"|E_litL_current_date->"CURRENT_DATE"|E_litL_current_time->"CURRENT_TIME"|E_colname->name|E_tbl_col(t,c)->Printf.sprintf"%s.%s"tc|E_paramParam_anon->"?"|E_param(Param_indexi)->Printf.sprintf"?%d"i|E_param(Param_namen)->Printf.sprintf":%s"n|E_binop(op,a,b)->Printf.sprintf"(%s %s %s)"(expr_to_sqla)(binop_to_sqlop)(expr_to_sqlb)|E_note->Printf.sprintf"NOT (%s)"(expr_to_sqle)|E_is_nulle->Printf.sprintf"(%s) IS NULL"(expr_to_sqle)|E_is_not_nulle->Printf.sprintf"(%s) IS NOT NULL"(expr_to_sqle)|E_nege->Printf.sprintf"(-(%s))"(expr_to_sqle)|E_bitnote->Printf.sprintf"(~(%s))"(expr_to_sqle)|E_between(x,lo,hi)->Printf.sprintf"(%s) BETWEEN (%s) AND (%s)"(expr_to_sqlx)(expr_to_sqllo)(expr_to_sqlhi)|E_in(x,vals)->Printf.sprintf"(%s) IN (%s)"(expr_to_sqlx)(String.concat", "(List.mapexpr_to_sqlvals))|E_func(f,args)->Printf.sprintf"%s(%s)"(func_to_sqlf)(String.concat", "(List.mapexpr_to_sqlargs))|E_case{scrutinee;branches;else_}->letscr=matchscrutineewith|None->""|Somee->" "^expr_to_sqleinletbrs=String.concat" "(List.map(fun(cond,res)->Printf.sprintf"WHEN %s THEN %s"(expr_to_sqlcond)(expr_to_sqlres))branches)inletel=matchelse_with|None->""|Somee->" ELSE "^expr_to_sqleinPrintf.sprintf"CASE%s %s%s END"scrbrsel|E_cast(e,ty)->lettn=matchtywith|Ty_int->"INTEGER"|Ty_text->"TEXT"|Ty_real->"REAL"|Ty_blob->"BLOB"inPrintf.sprintf"CAST(%s AS %s)"(expr_to_sqle)tn|E_collate(e,c)->letcname=matchcwith|Collate_nocase->"NOCASE"|Collate_binary->"BINARY"|Collate_rtrim->"RTRIM"inPrintf.sprintf"(%s) COLLATE %s"(expr_to_sqle)cname|E_fts_snippet{table;col_idx;start_tag;end_tag;ellipsis;n_tokens}->Printf.sprintf"snippet(%s,%d,'%s','%s','%s',%d)"tablecol_idxstart_tagend_tagellipsisn_tokens|E_agg_|E_match_|E_subquery_|E_exists_|E_in_select_|E_window_->failwith"expr_to_sql: unsupported expression form";;[@@@ai_disclosure"ai-generated"][@@@ai_model"claude-opus-4-7"][@@@ai_provider"Anthropic"]