···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(* ── CREATE TABLE ──────────────────────────────────────────────── *)
77+88+type col_token = Word of string | Number of string | Parens of col_token list
99+1010+type table_constraint =
1111+ | Unique of string list
1212+ | Primary_key of string list
1313+ | Other
1414+1515+type column_def = {
1616+ name : string;
1717+ affinity : string;
1818+ is_rowid_alias : bool;
1919+ has_unique : bool;
2020+ has_primary_key : bool;
2121+ has_not_null : bool;
2222+ collation : string option; (** declared [COLLATE name], uppercased *)
2323+}
2424+2525+(* Column constraint keywords — when we see one of these after the column
2626+ name and type, everything that follows is constraints, not type. *)
2727+let constraint_kw = function
2828+ | "PRIMARY" | "NOT" | "UNIQUE" | "DEFAULT" | "CHECK" | "REFERENCES"
2929+ | "COLLATE" | "GENERATED" | "AUTOINCREMENT" | "CONSTRAINT" | "ON" | "ASC"
3030+ | "DESC" ->
3131+ true
3232+ | _ -> false
3333+3434+let rec token_string = function
3535+ | Word s | Number s -> s
3636+ | Parens inner -> "(" ^ String.concat "" (List.map token_string inner) ^ ")"
3737+3838+(* The affinity string for a column's type tokens. Parenthesised params attach
3939+ to the preceding name without a space: DECIMAL(10,2), not DECIMAL (10,2). *)
4040+let rec affinity_of_tokens = function
4141+ | [] -> ""
4242+ | [ t ] -> token_string t
4343+ | t :: (Parens _ :: _ as rest) -> token_string t ^ affinity_of_tokens rest
4444+ | t :: rest -> token_string t ^ " " ^ affinity_of_tokens rest
4545+4646+(* Classify a column's token stream into type affinity + constraints. *)
4747+let classify_column name tokens =
4848+ (* Split tokens into type part and constraint part.
4949+ Type tokens: everything before the first constraint keyword.
5050+ Constraint tokens: everything from the first constraint keyword on. *)
5151+ let rec split_type acc = function
5252+ | Word w :: rest when constraint_kw (String.uppercase_ascii w) ->
5353+ (List.rev acc, Word w :: rest)
5454+ | (Word _ as t) :: rest -> split_type (t :: acc) rest
5555+ | (Number _ as t) :: rest -> split_type (t :: acc) rest
5656+ | (Parens _ as t) :: rest ->
5757+ (* Parenthesized content after a type name is type params like
5858+ DECIMAL(10,2). But only if we've seen a type name. *)
5959+ if acc = [] then (List.rev acc, Parens [] :: rest)
6060+ else split_type (t :: acc) rest
6161+ | [] -> (List.rev acc, [])
6262+ in
6363+ let type_tokens, constraint_tokens = split_type [] tokens in
6464+ let affinity = affinity_of_tokens type_tokens in
6565+ (* Scan constraint tokens for PRIMARY KEY and UNIQUE *)
6666+ let words =
6767+ List.filter_map
6868+ (function Word w -> Some (String.uppercase_ascii w) | _ -> None)
6969+ constraint_tokens
7070+ in
7171+ let rec has_pk = function
7272+ | "PRIMARY" :: "KEY" :: _ -> true
7373+ | _ :: rest -> has_pk rest
7474+ | [] -> false
7575+ in
7676+ let rec has_nn = function
7777+ | "NOT" :: "NULL" :: _ -> true
7878+ | _ :: rest -> has_nn rest
7979+ | [] -> false
8080+ in
8181+ let has_unique = List.mem "UNIQUE" words in
8282+ let has_primary_key = has_pk words in
8383+ let has_not_null = has_nn words in
8484+ let is_rowid_alias =
8585+ String.uppercase_ascii affinity = "INTEGER" && has_primary_key
8686+ in
8787+ (* A declared [COLLATE name] on the column: the word after COLLATE. *)
8888+ let rec find_collation = function
8989+ | Word w :: Word n :: _ when String.uppercase_ascii w = "COLLATE" ->
9090+ Some (String.uppercase_ascii n)
9191+ | _ :: rest -> find_collation rest
9292+ | [] -> None
9393+ in
9494+ let collation = find_collation constraint_tokens in
9595+ {
9696+ name;
9797+ affinity;
9898+ is_rowid_alias;
9999+ has_unique;
100100+ has_primary_key;
101101+ has_not_null;
102102+ collation;
103103+ }
104104+105105+(* ── Query subset ──────────────────────────────────────────────── *)
106106+107107+(* A SQL literal / column value -- the engine core's value type, so the frontend,
108108+ the storage adapter, and the engine all speak one type with no conversion. *)
109109+type value = Catalog.Value.t =
110110+ | Null
111111+ | Int of int64
112112+ | Float of float
113113+ | Blob of string
114114+ | Text of string
115115+116116+type binop =
117117+ | Eq
118118+ | Neq
119119+ | Lt
120120+ | Le
121121+ | Gt
122122+ | Ge
123123+ | And
124124+ | Or
125125+ | Like
126126+ | Glob
127127+ | Add
128128+ | Sub
129129+ | Mul
130130+ | Div
131131+ | Mod
132132+ | Bit_and
133133+ | Bit_or
134134+ | Shl
135135+ | Shr
136136+ | Concat
137137+ | Json_get (** [->]: extract as JSON text *)
138138+ | Json_get_text (** [->>]: extract as a SQL value *)
139139+140140+type join_type =
141141+ | Inner (** [JOIN] / [INNER JOIN] / comma cross-join *)
142142+ | Left (** [LEFT [OUTER] JOIN]: unmatched left rows kept, right cols NULL *)
143143+ | Right
144144+ (** [RIGHT [OUTER] JOIN]: unmatched right rows kept, left cols NULL *)
145145+ | Full (** [FULL OUTER JOIN]: unmatched rows from both sides kept *)
146146+147147+type expr =
148148+ | Lit of value
149149+ | Param of int (** [?] placeholder; numbered left-to-right from 0. *)
150150+ | Col of string option * string (** [t.col] or bare [col]. *)
151151+ | Star (** [*], as a select item or the argument of [count]. *)
152152+ | Qualified_star of string
153153+ (** [t.*]: every column of the table aliased [t], as a select item. *)
154154+ | Unop_not of expr
155155+ | Binary of binop * expr * expr
156156+ | Is_null of expr
157157+ | Is_not_null of expr
158158+ | Is of expr * expr
159159+ | In_list of expr * expr list
160160+ | In_select of expr * select
161161+ | Scalar_select of select
162162+ | Exists of select
163163+ | Func of string * expr list (** lowercased name, e.g. ["coalesce"]. *)
164164+ | Distinct_agg of string * expr
165165+ (** [agg(DISTINCT e)]: an aggregate over the distinct non-NULL values of
166166+ [e] within the group, e.g. [count(DISTINCT x)]. *)
167167+ | Filter of expr * expr
168168+ (** [agg(...) FILTER (WHERE p)]: the aggregate over only the group rows
169169+ satisfying [p]. The left expr is the aggregate call. *)
170170+ | Ordered_agg of expr * (expr * dir) list
171171+ (** [agg(... ORDER BY ...)]: the aggregate over the group rows sorted by
172172+ the keys (matters for order-sensitive aggregates like group_concat).
173173+ The left expr is the aggregate call. *)
174174+ | Window of expr * window
175175+ (** [f(...) OVER (PARTITION BY ... ORDER BY ...)]: a window function. The
176176+ left expr is the function call (e.g. [row_number()], [sum(x)]). *)
177177+ | Cast of expr * string
178178+ (** [CAST(e AS type)]; the type name drives SQLite type affinity. *)
179179+ | Case of expr option * (expr * expr) list * expr option
180180+ (** [CASE [base] WHEN c THEN r ... [ELSE e] END]. With a [base] (simple
181181+ form) each [c] is compared to [base] with [=] semantics; without
182182+ (searched form) each [c] is a boolean. First match wins; no match and
183183+ no [ELSE] yields NULL. *)
184184+ | Collate of expr * string
185185+186186+and result_col = { expr : expr; alias : string option }
187187+188188+and table_ref = {
189189+ name : string;
190190+ alias : string option;
191191+ subquery : select option;
192192+}
193193+194194+and window = {
195195+ partition : expr list;
196196+ order : (expr * dir) list;
197197+ frame : frame option;
198198+ (** explicit [ROWS]/[RANGE] frame; [None] uses the default frame (the
199199+ whole partition without ORDER BY, else running to the current row's
200200+ peers). *)
201201+}
202202+(** An [OVER (PARTITION BY ... ORDER BY ...)] window specification. *)
203203+204204+and frame = { rows : bool; start : frame_bound; end_ : frame_bound }
205205+(** A frame: [rows] is [ROWS] (positional) vs [RANGE] (peer-based). *)
206206+207207+and frame_bound =
208208+ | Unbounded_preceding
209209+ | Preceding of expr (** [n PRECEDING] *)
210210+ | Current_row
211211+ | Following of expr (** [n FOLLOWING] *)
212212+ | Unbounded_following
213213+214214+and join = {
215215+ table : table_ref;
216216+ on : expr;
217217+ type_ : join_type;
218218+ natural : bool;
219219+ (** [NATURAL]: the ON is implicit (equality on the columns common to the
220220+ left and this table), resolved at execution, and [SELECT *] lists each
221221+ common column once. *)
222222+ using : string list;
223223+ (** [USING (cols)]: like NATURAL but on these named columns; [] when
224224+ absent. *)
225225+}
226226+227227+and dir = Asc | Desc
228228+and set_op = Union | Union_all | Except | Intersect
229229+230230+and from_clause =
231231+ | Unit (** No [FROM]: [SELECT <expr>] evaluates over a single empty row. *)
232232+ | Table of table_ref
233233+ | Select of { fs_select : select; fs_alias : string option }
234234+ (** A derived table: a parenthesised subquery in [FROM]. *)
235235+ | Values of expr list list
236236+ (** A [VALUES (..), (..)] row source; columns named [column1]..[columnN].
237237+ *)
238238+ | Table_function of {
239239+ tf_name : string;
240240+ (** the table-valued function, e.g. [generate_series] *)
241241+ tf_args : expr list;
242242+ tf_alias : string option;
243243+ }
244244+ (** A table-valued function in [FROM], e.g. [generate_series(1, 10)]: it
245245+ produces rows that the query scans like a table. *)
246246+247247+and cte = {
248248+ name : string;
249249+ columns : string list; (** optional column rename; [] uses the query's *)
250250+ query : select;
251251+}
252252+(** A [WITH] common table expression: a named subquery usable in [FROM]. *)
253253+254254+and select = {
255255+ ctes : cte list; (** [WITH] table expressions in scope; empty when absent. *)
256256+ distinct : bool;
257257+ cols : result_col list;
258258+ from : from_clause;
259259+ joins : join list;
260260+ where : expr option;
261261+ group_by : expr list; (** [GROUP BY] keys; empty when absent. *)
262262+ having : expr option; (** [HAVING] group filter; evaluated per group. *)
263263+ compound : (set_op * select) list;
264264+ (** trailing [UNION]/[EXCEPT]/[INTERSECT] arms; [order_by]/[limit] apply
265265+ to the whole compound. Each arm carries only its own core. *)
266266+ order_by : (expr * dir) list;
267267+ limit : expr option; (** [LIMIT] count; an expression so [LIMIT ?] works. *)
268268+ offset : expr option; (** [OFFSET] skip count; only valid with [limit]. *)
269269+}
270270+271271+type check_constraint = {
272272+ expr : expr; (** the [CHECK (...)] predicate, parsed *)
273273+ start : int; (** byte offset of the predicate's source in the CREATE text *)
274274+ stop : int; (** byte offset just past the predicate's source *)
275275+}
276276+(** A [CHECK] constraint. The offsets bracket the predicate's verbatim source so
277277+ a violation can name it exactly as written (e.g. ["x<5"]). *)
278278+279279+type fk_action =
280280+ | No_action (** the default: reject if it would orphan a child *)
281281+ | Restrict
282282+ | Cascade
283283+ | Set_null
284284+ | Set_default
285285+286286+type foreign_key = {
287287+ cols : string list; (** the child (referencing) columns *)
288288+ parent : string; (** the referenced parent table *)
289289+ parent_cols : string list;
290290+ (** the referenced parent columns; [] means the parent's primary key *)
291291+ on_delete : fk_action;
292292+ on_update : fk_action;
293293+}
294294+295295+type create_table = {
296296+ tbl_name : string;
297297+ ct_if_not_exists : bool;
298298+ columns : column_def list;
299299+ table_constraints : table_constraint list;
300300+ checks : check_constraint list;
301301+ (** every [CHECK], column- and table-level, in declared order *)
302302+ defaults : (string * expr) list;
303303+ (** [(column, DEFAULT expr)] for each column with a [DEFAULT] clause *)
304304+ generated : (string * expr) list;
305305+ (** [(column, generation expr)] for each [... AS (expr)] /
306306+ [GENERATED ALWAYS AS (expr)] column. The value is computed from the
307307+ row's other columns on every write (VIRTUAL and STORED both behave as
308308+ stored here, which gives identical query results). *)
309309+ foreign_keys : foreign_key list;
310310+ (** every [FOREIGN KEY] / column [REFERENCES], in declared order *)
311311+ ct_without_rowid : bool;
312312+ (** [CREATE TABLE ... WITHOUT ROWID]: the table is ordered by its PRIMARY
313313+ KEY and has no implicit rowid. *)
314314+}
315315+316316+(* Recover a column-level foreign key from a column's token stream
317317+ ([col REFERENCES parent [(pcol, ...)]]), the same token-classification path as
318318+ {!classify_column}. Referential actions on a column reference are not picked up
319319+ here (only the table-level FOREIGN KEY grammar carries them), so the default
320320+ NO ACTION applies. *)
321321+let column_foreign_key name tokens =
322322+ let rec find = function
323323+ | Word w :: rest when String.uppercase_ascii w = "REFERENCES" -> Some rest
324324+ | _ :: rest -> find rest
325325+ | [] -> None
326326+ in
327327+ match find tokens with
328328+ | Some (Word parent :: rest) ->
329329+ let parent_cols =
330330+ match rest with
331331+ | Parens inner :: _ ->
332332+ List.filter_map (function Word c -> Some c | _ -> None) inner
333333+ | _ -> []
334334+ in
335335+ Some
336336+ {
337337+ cols = [ name ];
338338+ parent;
339339+ parent_cols;
340340+ on_delete = No_action;
341341+ on_update = No_action;
342342+ }
343343+ | _ -> None
344344+345345+type create_view = {
346346+ name : string;
347347+ if_not_exists : bool;
348348+ columns : string list; (** optional column rename; [] uses the query's *)
349349+ select : select; (** the stored query the view stands for *)
350350+}
351351+352352+type create_table_as = { name : string; if_not_exists : bool; query : select }
353353+354354+type insert_source =
355355+ | Values of expr list list (** one list per [VALUES (...)] tuple *)
356356+ | Select of select (** [INSERT ... SELECT]: rows from a query *)
357357+ | Default_values
358358+359359+type upsert_action =
360360+ | Nothing (** skip the row on a uniqueness conflict *)
361361+ | Update of (string * expr) list * expr option
362362+ (** [DO UPDATE SET ... [WHERE ...]]: rewrite the conflicting row;
363363+ [excluded.col] in the expressions is the would-be-inserted value. *)
364364+365365+type upsert = {
366366+ target : string list; (** [ON CONFLICT (cols)] target; [] when omitted *)
367367+ action : upsert_action;
368368+}
369369+370370+type insert_conflict = Abort | Fail | Ignore | Replace | Rollback
371371+372372+type insert = {
373373+ conflict : insert_conflict;
374374+ table : string;
375375+ columns : string list; (** empty means all columns, in declared order *)
376376+ source : insert_source;
377377+ upsert : upsert option; (** [ON CONFLICT ...] clause; [None] when absent *)
378378+ returning : result_col list; (** [RETURNING] columns; [] when absent *)
379379+}
380380+381381+type delete = {
382382+ ctes : cte list;
383383+ table : string;
384384+ alias : string option;
385385+ (** [DELETE FROM t AS x]: the alias the WHERE qualifies columns by *)
386386+ where : expr option;
387387+ returning : result_col list;
388388+}
389389+390390+type update = {
391391+ ctes : cte list;
392392+ table : string;
393393+ sets : (string * expr) list; (** [SET col = expr] assignments, in order *)
394394+ from : (from_clause * join list) option;
395395+ where : expr option;
396396+ returning : result_col list;
397397+}
398398+399399+type create_index = {
400400+ if_not_exists : bool;
401401+ unique : bool; (** [CREATE UNIQUE INDEX]: enforce distinct keys *)
402402+ name : string;
403403+ table : string;
404404+ columns : string list;
405405+ where : expr option;
406406+ (** [CREATE INDEX ... WHERE p]: a partial index, indexing only the rows
407407+ satisfying [p]. For a non-unique index this is a pure optimisation, so
408408+ the predicate is ignored (the index is built over every row, giving
409409+ identical query results); a partial {e unique} index is rejected, as
410410+ ignoring [p] would over-enforce uniqueness. *)
411411+ has_expr : bool;
412412+ (** [CREATE INDEX ... ON t(a+b)]: an expression key. The executor cannot
413413+ use such an index, so for a non-unique index no index is built (a pure
414414+ optimisation, identical results); an expression {e unique} index is
415415+ rejected, as its uniqueness cannot be enforced. *)
416416+}
417417+418418+type drop_object = Table | Index | View | Trigger
419419+420420+type drop = {
421421+ object_ : drop_object;
422422+ name : string;
423423+ if_exists : bool; (** [DROP ... IF EXISTS]: a missing object is a no-op *)
424424+}
425425+426426+type pragma = { name : string; value : value option }
427427+428428+type alter_op =
429429+ | Add_column of {
430430+ ac_name : string;
431431+ ac_affinity : string; (** column type text, [""] when none was given *)
432432+ ac_default : value option;
433433+ (** the [DEFAULT] literal; [None] for no clause, [Some Null] for
434434+ [DEFAULT NULL] -- both leave existing rows reading NULL *)
435435+ }
436436+ | Rename_table of string (** [RENAME TO new_name] *)
437437+ | Rename_column of string * string (** [RENAME COLUMN old TO new] *)
438438+ | Drop_column of string (** [DROP COLUMN name] *)
439439+440440+type alter_table = { table : string; op : alter_op }
441441+type trigger_timing = Before | After | Instead_of
442442+443443+type trigger_event =
444444+ | Insert
445445+ | Delete
446446+ | Update of string list (** [UPDATE OF cols]; [] means any column *)
447447+448448+type create_trigger = {
449449+ name : string;
450450+ if_not_exists : bool;
451451+ timing : trigger_timing;
452452+ event : trigger_event;
453453+ table : string;
454454+ when_ : expr option; (** [WHEN] guard, evaluated per row before the body *)
455455+ body : stmt list; (** action statements between [BEGIN] and [END] *)
456456+}
457457+458458+and stmt =
459459+ | Select of select
460460+ | Insert of insert
461461+ | Delete of delete
462462+ | Update of update
463463+ | Create_table of create_table
464464+ | Create_table_as of create_table_as
465465+ | Create_index of create_index
466466+ | Create_view of create_view
467467+ | Create_trigger of create_trigger
468468+ | Drop of drop
469469+ | Alter_table of alter_table
470470+ | Pragma of pragma
471471+ | Begin
472472+ | Commit
473473+ | Rollback
474474+ | Savepoint of string (** [SAVEPOINT name]: a nested rollback point *)
475475+ | Release of string (** [RELEASE [SAVEPOINT] name]: discard a savepoint *)
476476+ | Rollback_to of string (** [ROLLBACK TO [SAVEPOINT] name] *)
477477+ | Vacuum
478478+ | Analyze
479479+ (** [ANALYZE [schema | table | index]]: gather query-planner statistics.
480480+ It changes only the planner's stat tables, never query results, so it
481481+ is run as a no-op here. *)
···11+(** SQLite SQL AST: CREATE TABLE classification plus the query subset. *)
22+33+(** {1 CREATE TABLE} *)
44+55+type col_token = Word of string | Number of string | Parens of col_token list
66+77+type table_constraint =
88+ | Unique of string list
99+ | Primary_key of string list
1010+ | Other
1111+1212+type column_def = {
1313+ name : string;
1414+ affinity : string;
1515+ is_rowid_alias : bool;
1616+ has_unique : bool;
1717+ has_primary_key : bool;
1818+ has_not_null : bool;
1919+ collation : string option; (** declared [COLLATE name], uppercased *)
2020+}
2121+2222+val classify_column : string -> col_token list -> column_def
2323+(** [classify_column name tokens] splits [tokens] into type affinity and
2424+ constraints, returning a column definition. *)
2525+2626+(** {1 Query subset} *)
2727+2828+type value = Catalog.Value.t =
2929+ | Null
3030+ | Int of int64
3131+ | Float of float
3232+ | Blob of string
3333+ | Text of string
3434+ (** A SQL literal / column value -- the engine core's {!Catalog.Value.t},
3535+ so the frontend, the storage adapter, and the engine all speak one
3636+ value type with no conversion. *)
3737+3838+type binop =
3939+ | Eq
4040+ | Neq
4141+ | Lt
4242+ | Le
4343+ | Gt
4444+ | Ge
4545+ | And
4646+ | Or
4747+ | Like
4848+ | Glob
4949+ | Add
5050+ | Sub
5151+ | Mul
5252+ | Div
5353+ | Mod
5454+ | Bit_and
5555+ | Bit_or
5656+ | Shl
5757+ | Shr
5858+ | Concat
5959+ | Json_get (** [->]: extract as JSON text *)
6060+ | Json_get_text (** [->>]: extract as a SQL value *)
6161+6262+type join_type =
6363+ | Inner (** [JOIN] / [INNER JOIN] / comma cross-join *)
6464+ | Left (** [LEFT [OUTER] JOIN]: unmatched left rows kept, right cols NULL *)
6565+ | Right
6666+ (** [RIGHT [OUTER] JOIN]: unmatched right rows kept, left cols NULL *)
6767+ | Full (** [FULL OUTER JOIN]: unmatched rows from both sides kept *)
6868+6969+type expr =
7070+ | Lit of value
7171+ | Param of int (** [?] placeholder; numbered left-to-right from 0. *)
7272+ | Col of string option * string (** [t.col] or bare [col]. *)
7373+ | Star (** [*], as a select item or the argument of [count]. *)
7474+ | Qualified_star of string
7575+ (** [t.*]: every column of the table aliased [t], as a select item. *)
7676+ | Unop_not of expr
7777+ | Binary of binop * expr * expr
7878+ | Is_null of expr
7979+ | Is_not_null of expr
8080+ | Is of expr * expr
8181+ (** [a IS b]: NULL-safe equality (a is not distinct from b), 1 or 0, never
8282+ NULL. [a IS NOT b] is [NOT (a IS b)]; [a IS NULL] folds to {!Is_null}.
8383+ *)
8484+ | In_list of expr * expr list
8585+ | In_select of expr * select
8686+ | Scalar_select of select
8787+ (** [(SELECT ...)] used as a value: the first column of the first row, or
8888+ NULL if the subquery returns no rows. Correlated via the outer scope.
8989+ *)
9090+ | Exists of select
9191+ (** [EXISTS (SELECT ...)]: 1 if the subquery returns any row, else 0. *)
9292+ | Func of string * expr list (** lowercased name, e.g. ["coalesce"]. *)
9393+ | Distinct_agg of string * expr
9494+ (** [agg(DISTINCT e)]: an aggregate over the distinct non-NULL values of
9595+ [e] within the group, e.g. [count(DISTINCT x)]. *)
9696+ | Filter of expr * expr
9797+ (** [agg(...) FILTER (WHERE p)]: the aggregate over only the group rows
9898+ satisfying [p]. The left expr is the aggregate call. *)
9999+ | Ordered_agg of expr * (expr * dir) list
100100+ (** [agg(... ORDER BY ...)]: the aggregate over the group rows sorted by
101101+ the keys (matters for order-sensitive aggregates like group_concat).
102102+ The left expr is the aggregate call. *)
103103+ | Window of expr * window
104104+ (** [f(...) OVER (PARTITION BY ... ORDER BY ...)]: a window function. *)
105105+ | Cast of expr * string
106106+ (** [CAST(e AS type)]; the type name drives SQLite type affinity. *)
107107+ | Case of expr option * (expr * expr) list * expr option
108108+ (** [CASE [base] WHEN c THEN r ... [ELSE e] END]. With a [base] (simple
109109+ form) each [c] is compared to [base] with [=] semantics; without
110110+ (searched form) each [c] is a boolean. First match wins; no match and
111111+ no [ELSE] yields NULL. *)
112112+ | Collate of expr * string
113113+ (** [e COLLATE name]: assigns the collating sequence [name] (uppercased:
114114+ [BINARY], [NOCASE], [RTRIM]) to [e] for comparison and ordering. The
115115+ value of [e] is unchanged; only text comparisons consult it. *)
116116+117117+and result_col = { expr : expr; alias : string option }
118118+119119+and table_ref = {
120120+ name : string;
121121+ alias : string option;
122122+ subquery : select option;
123123+ (** [Some q] for a derived table [(q) alias] in JOIN position: [name] is
124124+ the alias the subquery's rows are bound under. [None] for a named
125125+ table. *)
126126+}
127127+128128+and window = {
129129+ partition : expr list;
130130+ order : (expr * dir) list;
131131+ frame : frame option;
132132+ (** explicit [ROWS]/[RANGE] frame; [None] uses the default frame. *)
133133+}
134134+(** An [OVER (PARTITION BY ... ORDER BY ...)] window specification. *)
135135+136136+and frame = { rows : bool; start : frame_bound; end_ : frame_bound }
137137+(** A frame: {!field-rows} is [ROWS] (positional) vs [RANGE] (peer-based). *)
138138+139139+and frame_bound =
140140+ | Unbounded_preceding
141141+ | Preceding of expr (** [n PRECEDING] *)
142142+ | Current_row
143143+ | Following of expr (** [n FOLLOWING] *)
144144+ | Unbounded_following
145145+146146+and join = {
147147+ table : table_ref;
148148+ on : expr;
149149+ type_ : join_type;
150150+ natural : bool;
151151+ (** [NATURAL]: the ON is implicit (equality on the columns common to the
152152+ left and this table), resolved at execution, and [SELECT *] lists each
153153+ common column once. *)
154154+ using : string list;
155155+ (** [USING (cols)]: like NATURAL but on these named columns; [] when
156156+ absent. *)
157157+}
158158+159159+and dir = Asc | Desc
160160+and set_op = Union | Union_all | Except | Intersect
161161+162162+and from_clause =
163163+ | Unit (** No [FROM]: [SELECT <expr>] evaluates over a single empty row. *)
164164+ | Table of table_ref
165165+ | Select of { fs_select : select; fs_alias : string option }
166166+ (** A derived table: a parenthesised subquery in [FROM]. *)
167167+ | Values of expr list list
168168+ (** A [VALUES (..), (..)] row source; columns named [column1]..[columnN].
169169+ *)
170170+ | Table_function of {
171171+ tf_name : string;
172172+ (** the table-valued function, e.g. [generate_series] *)
173173+ tf_args : expr list;
174174+ tf_alias : string option;
175175+ }
176176+ (** A table-valued function in [FROM], e.g. [generate_series(1, 10)]: it
177177+ produces rows that the query scans like a table. *)
178178+179179+and cte = {
180180+ name : string;
181181+ columns : string list; (** optional column rename; [] uses the query's *)
182182+ query : select;
183183+}
184184+(** A [WITH] common table expression: a named subquery usable in [FROM]. *)
185185+186186+and select = {
187187+ ctes : cte list; (** [WITH] table expressions in scope; empty when absent. *)
188188+ distinct : bool;
189189+ cols : result_col list;
190190+ from : from_clause;
191191+ joins : join list;
192192+ where : expr option;
193193+ group_by : expr list; (** [GROUP BY] keys; empty when absent. *)
194194+ having : expr option; (** [HAVING] group filter; evaluated per group. *)
195195+ compound : (set_op * select) list;
196196+ (** trailing [UNION]/[EXCEPT]/[INTERSECT] arms; [order_by]/[limit] apply
197197+ to the whole compound. Each arm carries only its own core. *)
198198+ order_by : (expr * dir) list;
199199+ limit : expr option; (** [LIMIT] count; an expression so [LIMIT ?] works. *)
200200+ offset : expr option; (** [OFFSET] skip count; only valid with [limit]. *)
201201+}
202202+203203+type check_constraint = {
204204+ expr : expr; (** the [CHECK (...)] predicate, parsed *)
205205+ start : int; (** byte offset of the predicate's source in the CREATE text *)
206206+ stop : int; (** byte offset just past the predicate's source *)
207207+}
208208+(** A [CHECK] constraint. The offsets bracket the predicate's verbatim source so
209209+ a violation can name it exactly as written (e.g. ["x<5"]). *)
210210+211211+type fk_action =
212212+ | No_action (** the default: reject if it would orphan a child *)
213213+ | Restrict
214214+ | Cascade
215215+ | Set_null
216216+ | Set_default
217217+218218+type foreign_key = {
219219+ cols : string list; (** the child (referencing) columns *)
220220+ parent : string; (** the referenced parent table *)
221221+ parent_cols : string list;
222222+ (** the referenced parent columns; [] means the parent's primary key *)
223223+ on_delete : fk_action;
224224+ on_update : fk_action;
225225+}
226226+227227+type create_table = {
228228+ tbl_name : string;
229229+ ct_if_not_exists : bool;
230230+ columns : column_def list;
231231+ table_constraints : table_constraint list;
232232+ checks : check_constraint list;
233233+ (** every [CHECK], column- and table-level, in declared order *)
234234+ defaults : (string * expr) list;
235235+ (** [(column, DEFAULT expr)] for each column with a [DEFAULT] clause *)
236236+ generated : (string * expr) list;
237237+ (** [(column, generation expr)] for each [... AS (expr)] /
238238+ [GENERATED ALWAYS AS (expr)] column; computed from the row's other
239239+ columns on every write (VIRTUAL and STORED both behave as stored). *)
240240+ foreign_keys : foreign_key list;
241241+ (** every [FOREIGN KEY] / column [REFERENCES], in declared order *)
242242+ ct_without_rowid : bool;
243243+ (** [CREATE TABLE ... WITHOUT ROWID]: ordered by its PRIMARY KEY, no
244244+ implicit rowid. *)
245245+}
246246+247247+val column_foreign_key : string -> col_token list -> foreign_key option
248248+(** [column_foreign_key name tokens] recovers a column-level
249249+ [name REFERENCES parent [(cols)]] foreign key from the column's token
250250+ stream, or [None] if it has no [REFERENCES]. *)
251251+252252+type create_view = {
253253+ name : string;
254254+ if_not_exists : bool;
255255+ columns : string list; (** optional column rename; [] uses the query's *)
256256+ select : select; (** the stored query the view stands for *)
257257+}
258258+259259+type create_table_as = {
260260+ name : string;
261261+ if_not_exists : bool;
262262+ query : select;
263263+ (** [CREATE TABLE name AS query]: the new table's columns are the query's
264264+ output column names, and it is populated with the query's rows. *)
265265+}
266266+267267+type insert_source =
268268+ | Values of expr list list (** one list per [VALUES (...)] tuple *)
269269+ | Select of select (** [INSERT ... SELECT]: rows from a query *)
270270+ | Default_values
271271+ (** [INSERT ... DEFAULT VALUES]: one row, every column its default. *)
272272+273273+type upsert_action =
274274+ | Nothing (** skip the row on a uniqueness conflict *)
275275+ | Update of (string * expr) list * expr option
276276+ (** [DO UPDATE SET ... [WHERE ...]]: rewrite the conflicting row;
277277+ [excluded.col] in the expressions is the would-be-inserted value. *)
278278+279279+type upsert = {
280280+ target : string list; (** [ON CONFLICT (cols)] target; [] when omitted *)
281281+ action : upsert_action;
282282+}
283283+284284+(** [INSERT OR <action>]: the conflict-resolution algorithm. FAIL and ROLLBACK
285285+ currently behave like ABORT (the violation errors); their undo-scope
286286+ distinction is a transaction-semantics follow-up. *)
287287+type insert_conflict =
288288+ | Abort
289289+ (** the default: a constraint violation undoes the statement, errors *)
290290+ | Fail (** stop at the violating row, keeping prior rows (no rollback) *)
291291+ | Ignore (** skip the violating row and continue, no error *)
292292+ | Replace (** delete the conflicting row(s), then insert *)
293293+ | Rollback (** a violation aborts the whole transaction *)
294294+295295+type insert = {
296296+ conflict : insert_conflict;
297297+ table : string;
298298+ columns : string list; (** empty means all columns, in declared order *)
299299+ source : insert_source;
300300+ upsert : upsert option; (** [ON CONFLICT ...] clause; [None] when absent *)
301301+ returning : result_col list; (** [RETURNING] columns; [] when absent *)
302302+}
303303+304304+type delete = {
305305+ ctes : cte list; (** leading [WITH] table expressions; [] when absent *)
306306+ table : string;
307307+ alias : string option;
308308+ (** [DELETE FROM t AS x]: the alias the WHERE qualifies columns by *)
309309+ where : expr option;
310310+ returning : result_col list;
311311+}
312312+313313+type update = {
314314+ ctes : cte list; (** leading [WITH] table expressions; [] when absent *)
315315+ table : string;
316316+ sets : (string * expr) list; (** [SET col = expr] assignments, in order *)
317317+ from : (from_clause * join list) option;
318318+ (** [UPDATE ... SET ... FROM t ...]: auxiliary tables joined to the
319319+ target; their columns are in scope in the SET expressions and WHERE.
320320+ *)
321321+ where : expr option;
322322+ returning : result_col list;
323323+}
324324+325325+type create_index = {
326326+ if_not_exists : bool;
327327+ unique : bool; (** [CREATE UNIQUE INDEX]: enforce distinct keys *)
328328+ name : string;
329329+ table : string;
330330+ columns : string list;
331331+ where : expr option;
332332+ (** [CREATE INDEX ... WHERE p]: a partial index. For a non-unique index
333333+ the predicate is ignored (a pure optimisation, identical query
334334+ results); a partial {e unique} index is rejected. *)
335335+ has_expr : bool;
336336+ (** [CREATE INDEX ... ON t(a+b)]: an expression key -- a non-unique such
337337+ index builds nothing (identical results); a unique one is rejected. *)
338338+}
339339+340340+type drop_object = Table | Index | View | Trigger
341341+342342+type drop = {
343343+ object_ : drop_object;
344344+ name : string;
345345+ if_exists : bool; (** [DROP ... IF EXISTS]: a missing object is a no-op *)
346346+}
347347+348348+type pragma = { name : string; value : value option }
349349+350350+type alter_op =
351351+ | Add_column of {
352352+ ac_name : string;
353353+ ac_affinity : string; (** column type text, [""] when none was given *)
354354+ ac_default : value option;
355355+ (** the [DEFAULT] literal; [None] for no clause, [Some Null] for
356356+ [DEFAULT NULL] -- both leave existing rows reading NULL *)
357357+ }
358358+ | Rename_table of string (** [RENAME TO new_name] *)
359359+ | Rename_column of string * string (** [RENAME COLUMN old TO new] *)
360360+ | Drop_column of string (** [DROP COLUMN name] *)
361361+362362+type alter_table = { table : string; op : alter_op }
363363+type trigger_timing = Before | After | Instead_of
364364+365365+type trigger_event =
366366+ | Insert
367367+ | Delete
368368+ | Update of string list (** [UPDATE OF cols]; [] means any column *)
369369+370370+type create_trigger = {
371371+ name : string;
372372+ if_not_exists : bool;
373373+ timing : trigger_timing;
374374+ event : trigger_event;
375375+ table : string;
376376+ when_ : expr option; (** [WHEN] guard, evaluated per row before the body *)
377377+ body : stmt list; (** action statements between [BEGIN] and [END] *)
378378+}
379379+380380+and stmt =
381381+ | Select of select
382382+ | Insert of insert
383383+ | Delete of delete
384384+ | Update of update
385385+ | Create_table of create_table
386386+ | Create_table_as of create_table_as
387387+ | Create_index of create_index
388388+ | Create_view of create_view
389389+ | Create_trigger of create_trigger
390390+ | Drop of drop
391391+ | Alter_table of alter_table
392392+ | Pragma of pragma
393393+ | Begin
394394+ | Commit
395395+ | Rollback
396396+ | Savepoint of string (** [SAVEPOINT name]: a nested rollback point *)
397397+ | Release of string (** [RELEASE [SAVEPOINT] name]: discard a savepoint *)
398398+ | Rollback_to of string (** [ROLLBACK TO [SAVEPOINT] name] *)
399399+ | Vacuum
400400+ | Analyze
401401+ (** [ANALYZE [schema | table | index]]: gather query-planner statistics.
402402+ It changes only the planner's stat tables, never query results, so it
403403+ is run as a no-op here. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(* SQLite date and time functions (lang_datefunc.html).
77+88+ Time is carried as a Julian Day count in integer milliseconds, exactly as
99+ sqlite3's date.c does, so day/second arithmetic is exact and the broken-down
1010+ calendar fields round-trip. The deterministic surface -- explicit time
1111+ strings and Julian/unix numbers, the unit and "start of" modifiers, the named
1212+ functions and strftime -- is implemented; the wall-clock forms ('now',
1313+ 'localtime', 'utc') need an injected clock and raise until one is wired. *)
1414+1515+let ms_per_day = 86_400_000L
1616+1717+(* Unix epoch 1970-01-01 is Julian Day 2440587.5. *)
1818+let unix_epoch_ms = 210_866_760_000_000L
1919+2020+let text_of_value = function
2121+ | Ast.Text s -> s
2222+ | Blob s -> s
2323+ | Int i -> Int64.to_string i
2424+ | Float f -> Fmt.str "%g" f
2525+ | Null -> ""
2626+2727+(* Broken-down calendar fields -> Julian Day in integer ms. Out-of-range day or
2828+ month values are accepted and normalised by the arithmetic, so Feb 31 lands
2929+ in March, as sqlite3 does for month arithmetic. *)
3030+let ijd_of_fields y mo d h mi s =
3131+ let y, mo = if mo <= 2 then (y - 1, mo + 12) else (y, mo) in
3232+ let a = y / 100 in
3333+ let b = 2 - a + (a / 4) in
3434+ let x1 = 36525 * (y + 4716) / 100 in
3535+ let x2 = 306001 * (mo + 1) / 10000 in
3636+ let days = float_of_int (x1 + x2 + d + b) -. 1524.5 in
3737+ let day_ms = Int64.of_float (days *. 86_400_000.) in
3838+ let time_ms = Int64.of_int ((h * 3_600_000) + (mi * 60_000)) in
3939+ let sec_ms = Int64.of_float (Float.round (s *. 1000.)) in
4040+ Int64.add (Int64.add day_ms time_ms) sec_ms
4141+4242+(* Julian Day in ms -> (year, month, day, hour, minute, seconds-with-fraction).
4343+ The inverse of {!ijd_of_fields}, using the standard Gregorian conversion. *)
4444+let fields_of_ijd ijd =
4545+ let z = Int64.to_int (Int64.div (Int64.add ijd 43_200_000L) ms_per_day) in
4646+ let a = int_of_float ((float_of_int z -. 1_867_216.25) /. 36524.25) in
4747+ let a = z + 1 + a - (a / 4) in
4848+ let b = a + 1524 in
4949+ let c = int_of_float ((float_of_int b -. 122.1) /. 365.25) in
5050+ let d = int_of_float (365.25 *. float_of_int c) in
5151+ let e = int_of_float (float_of_int (b - d) /. 30.6001) in
5252+ let x1 = int_of_float (30.6001 *. float_of_int e) in
5353+ let day = b - d - x1 in
5454+ let month = if e < 14 then e - 1 else e - 13 in
5555+ let year = if month > 2 then c - 4716 else c - 4715 in
5656+ let into = Int64.to_int (Int64.rem (Int64.add ijd 43_200_000L) ms_per_day) in
5757+ let into = if into < 0 then into + 86_400_000 else into in
5858+ let h = into / 3_600_000 in
5959+ let mi = into mod 3_600_000 / 60_000 in
6060+ let sec = float_of_int (into mod 60_000) /. 1000. in
6161+ (year, month, day, h, mi, sec)
6262+6363+(* ── Parsing a time value ────────────────────────────────────── *)
6464+6565+let sub_int s a b = int_of_string (String.sub s a (b - a))
6666+6767+(* Parse the [HH:MM[:SS[.fff]]] tail of a time string into (h, mi, seconds). *)
6868+let parse_hms t =
6969+ match String.split_on_char ':' t with
7070+ | [ h; m ] -> (int_of_string h, int_of_string m, 0.)
7171+ | [ h; m; s ] -> (int_of_string h, int_of_string m, float_of_string s)
7272+ | _ -> failwith "bad time"
7373+7474+(* Reject calendar fields outside sqlite3's accepted ranges; an out-of-range
7575+ literal date/time yields NULL rather than a value. *)
7676+let validate y mo d h mi s =
7777+ if
7878+ mo < 1 || mo > 12 || d < 1 || d > 31 || h > 24 || mi > 59 || s >= 60.
7979+ || y < 0
8080+ then failwith "date/time field out of range"
8181+8282+(* Strip a trailing 'Z' or +/-HH:MM timezone, returning the offset in minutes,
8383+ the remaining time text, and whether a timezone was present (so a later 'utc'
8484+ modifier knows the value is already UTC and is a no-op). *)
8585+let split_tz t =
8686+ let n = String.length t in
8787+ if n > 0 && (t.[n - 1] = 'Z' || t.[n - 1] = 'z') then
8888+ (0, String.sub t 0 (n - 1), true)
8989+ else if n >= 6 && (t.[n - 6] = '+' || t.[n - 6] = '-') && t.[n - 3] = ':' then
9090+ let sign = if t.[n - 6] = '+' then 1 else -1 in
9191+ let hh = int_of_string (String.sub t (n - 5) 2) in
9292+ let mm = int_of_string (String.sub t (n - 2) 2) in
9393+ (sign * ((hh * 60) + mm), String.sub t 0 (n - 6), true)
9494+ else (0, t, false)
9595+9696+let parse_string str =
9797+ let str = String.trim str in
9898+ if String.length str >= 6 && String.contains str '-' then begin
9999+ (* date, optionally followed by ' ' or 'T' and a time *)
100100+ let date_part, time_part =
101101+ match String.index_opt str ' ' with
102102+ | Some i ->
103103+ ( String.sub str 0 i,
104104+ Some (String.sub str (i + 1) (String.length str - i - 1)) )
105105+ | None -> (
106106+ match String.index_opt str 'T' with
107107+ | Some i ->
108108+ ( String.sub str 0 i,
109109+ Some (String.sub str (i + 1) (String.length str - i - 1)) )
110110+ | None -> (str, None))
111111+ in
112112+ let y = sub_int date_part 0 4 in
113113+ let mo = sub_int date_part 5 7 in
114114+ let d = sub_int date_part 8 10 in
115115+ let offset, h, mi, s, had_tz =
116116+ match time_part with
117117+ | None -> (0, 0, 0, 0., false)
118118+ | Some t ->
119119+ let offset, t, had_tz = split_tz t in
120120+ let h, mi, s = parse_hms (String.trim t) in
121121+ (offset, h, mi, s, had_tz)
122122+ in
123123+ validate y mo d h mi s;
124124+ (* Shift an offset-bearing local time back to UTC (SQLite stores UTC); the
125125+ Julian Day is in integer milliseconds. *)
126126+ ( Int64.sub (ijd_of_fields y mo d h mi s) (Int64.of_int (offset * 60_000)),
127127+ had_tz )
128128+ end
129129+ else if String.contains str ':' then begin
130130+ let offset, t, had_tz = split_tz (String.trim str) in
131131+ let h, mi, s = parse_hms (String.trim t) in
132132+ validate 2000 1 1 h mi s;
133133+ ( Int64.sub (ijd_of_fields 2000 1 1 h mi s) (Int64.of_int (offset * 60_000)),
134134+ had_tz )
135135+ end
136136+ else failwith "unrecognized date/time string"
137137+138138+(* Parse the leading time value, honouring an 'unixepoch' modifier (numbers are
139139+ unix seconds) or treating a bare number as a Julian Day. Returns the Julian
140140+ Day in integer milliseconds and whether the value is already in UTC (a number,
141141+ or a tz-bearing string), so a 'utc' modifier is a no-op. *)
142142+let parse_value ~unixepoch v =
143143+ match v with
144144+ | Ast.Int _ | Float _ ->
145145+ let n =
146146+ match v with Int i -> Int64.to_float i | Float f -> f | _ -> 0.
147147+ in
148148+ let ijd =
149149+ if unixepoch then Int64.add unix_epoch_ms (Int64.of_float (n *. 1000.))
150150+ else Int64.of_float (n *. 86_400_000.)
151151+ in
152152+ (ijd, true)
153153+ | _ ->
154154+ let s = String.trim (text_of_value v) in
155155+ if unixepoch then
156156+ ( Int64.add unix_epoch_ms (Int64.of_float (float_of_string s *. 1000.)),
157157+ true )
158158+ else parse_string s
159159+160160+(* ── Modifiers ───────────────────────────────────────────────── *)
161161+162162+(* "+N unit" / "-N unit" / "N unit": the numeric prefix and the unit word. *)
163163+let split_amount s =
164164+ let n = String.length s in
165165+ let i = ref 0 in
166166+ if !i < n && (s.[!i] = '+' || s.[!i] = '-') then incr i;
167167+ while !i < n && (s.[!i] = '.' || (s.[!i] >= '0' && s.[!i] <= '9')) do
168168+ incr i
169169+ done;
170170+ let num = String.sub s 0 !i in
171171+ let unit = String.trim (String.sub s !i (n - !i)) in
172172+ (float_of_string num, unit)
173173+174174+let normalize_month y mo =
175175+ let total = (y * 12) + (mo - 1) in
176176+ (total / 12, (total mod 12) + 1)
177177+178178+let apply_modifier ~had_tz ijd m =
179179+ let m = String.lowercase_ascii (String.trim m) in
180180+ if m = "start of day" then
181181+ let y, mo, d, _, _, _ = fields_of_ijd ijd in
182182+ ijd_of_fields y mo d 0 0 0.
183183+ else if m = "start of month" then
184184+ let y, mo, _, _, _, _ = fields_of_ijd ijd in
185185+ ijd_of_fields y mo 1 0 0 0.
186186+ else if m = "start of year" then
187187+ let y, _, _, _, _, _ = fields_of_ijd ijd in
188188+ ijd_of_fields y 1 1 0 0 0.
189189+ else if m = "utc" then
190190+ (* The value is stored in UTC. 'utc' is a no-op when it already carries a
191191+ timezone (so [datetime('..Z','utc')] keeps the time); otherwise it would
192192+ assume localtime and needs the machine clock, which is not wired. *)
193193+ if had_tz then ijd
194194+ else failwith "date/time: 'utc' on a local time needs a timezone"
195195+ else if m = "localtime" then
196196+ failwith "date/time: 'localtime' needs the machine timezone"
197197+ else
198198+ let amount, unit = split_amount m in
199199+ match unit with
200200+ | "day" | "days" -> Int64.add ijd (Int64.of_float (amount *. 86_400_000.))
201201+ | "hour" | "hours" -> Int64.add ijd (Int64.of_float (amount *. 3_600_000.))
202202+ | "minute" | "minutes" -> Int64.add ijd (Int64.of_float (amount *. 60_000.))
203203+ | "second" | "seconds" -> Int64.add ijd (Int64.of_float (amount *. 1000.))
204204+ | "month" | "months" ->
205205+ let y, mo, d, h, mi, s = fields_of_ijd ijd in
206206+ let y, mo = normalize_month y (mo + int_of_float amount) in
207207+ ijd_of_fields y mo d h mi s
208208+ | "year" | "years" ->
209209+ let y, mo, d, h, mi, s = fields_of_ijd ijd in
210210+ ijd_of_fields (y + int_of_float amount) mo d h mi s
211211+ | _ -> Fmt.failwith "date/time: unknown modifier %S" m
212212+213213+(* The full evaluation: parse the time value, then fold the modifiers. Any
214214+ unparseable value, out-of-range field, or unknown modifier yields None, which
215215+ the SQL functions surface as NULL -- sqlite3 never errors on a bad date. *)
216216+let evaluate args =
217217+ match args with
218218+ | [] -> None
219219+ | Ast.Null :: _ -> None
220220+ | v :: mods -> (
221221+ try
222222+ let mod_strings = List.map text_of_value mods in
223223+ let is_unix m = String.lowercase_ascii (String.trim m) = "unixepoch" in
224224+ let unixepoch = List.exists is_unix mod_strings in
225225+ let mods = List.filter (fun m -> not (is_unix m)) mod_strings in
226226+ let ijd, had_tz = parse_value ~unixepoch v in
227227+ Some (List.fold_left (apply_modifier ~had_tz) ijd mods)
228228+ with Failure _ | Invalid_argument _ -> None)
229229+230230+(* ── Formatting ──────────────────────────────────────────────── *)
231231+232232+let day_of_week ijd =
233233+ (* 0 = Sunday. JD+0.5 floored is the civil day; +1 mod 7 puts Sunday at 0. *)
234234+ let z = Int64.to_int (Int64.div (Int64.add ijd 43_200_000L) ms_per_day) in
235235+ ((z mod 7) + 7 + 1) mod 7
236236+237237+let day_of_year y mo d =
238238+ let jan1 = ijd_of_fields y 1 1 0 0 0. in
239239+ let this = ijd_of_fields y mo d 0 0 0. in
240240+ 1 + Int64.to_int (Int64.div (Int64.sub this jan1) ms_per_day)
241241+242242+let fmt_date ijd =
243243+ let y, mo, d, _, _, _ = fields_of_ijd ijd in
244244+ Fmt.str "%04d-%02d-%02d" y mo d
245245+246246+let fmt_time ijd =
247247+ let _, _, _, h, mi, s = fields_of_ijd ijd in
248248+ Fmt.str "%02d:%02d:%02d" h mi (int_of_float s)
249249+250250+let fmt_datetime ijd = fmt_date ijd ^ " " ^ fmt_time ijd
251251+let julianday ijd = Int64.to_float ijd /. 86_400_000.
252252+let unixepoch_of ijd = Int64.div (Int64.sub ijd unix_epoch_ms) 1000L
253253+254254+(* Left-pad an integer with zeros to [width] digits. *)
255255+let zpad width n =
256256+ let s = string_of_int n in
257257+ let p = width - String.length s in
258258+ if p <= 0 then s else String.make p '0' ^ s
259259+260260+(* strftime substitutions. The field strings are computed once, so the loop over
261261+ the format only selects among them. *)
262262+let strftime fmt ijd =
263263+ let y, mo, d, h, mi, s = fields_of_ijd ijd in
264264+ let doy = day_of_year y mo d in
265265+ let dow = day_of_week ijd in
266266+ let sec_frac =
267267+ let whole = int_of_float s in
268268+ let frac =
269269+ int_of_float (Float.round ((s -. float_of_int whole) *. 1000.))
270270+ in
271271+ zpad 2 whole ^ "." ^ zpad 3 frac
272272+ in
273273+ (* Monday-based week number, as sqlite3's %W computes it. *)
274274+ let week = zpad 2 ((doy + 6 - ((dow + 6) mod 7)) / 7) in
275275+ let subst = function
276276+ | 'Y' -> zpad 4 y
277277+ | 'm' -> zpad 2 mo
278278+ | 'd' -> zpad 2 d
279279+ | 'H' -> zpad 2 h
280280+ | 'M' -> zpad 2 mi
281281+ | 'S' -> zpad 2 (int_of_float s)
282282+ | 'j' -> zpad 3 doy
283283+ | 'w' -> string_of_int dow
284284+ | 'W' -> week
285285+ | 'f' -> sec_frac
286286+ | 's' -> Int64.to_string (unixepoch_of ijd)
287287+ | 'J' -> string_of_float (julianday ijd)
288288+ | '%' -> "%"
289289+ | c -> "%" ^ String.make 1 c
290290+ in
291291+ let buf = Buffer.create (String.length fmt) in
292292+ let n = String.length fmt in
293293+ let i = ref 0 in
294294+ while !i < n do
295295+ if fmt.[!i] = '%' && !i + 1 < n then begin
296296+ Buffer.add_string buf (subst fmt.[!i + 1]);
297297+ i := !i + 2
298298+ end
299299+ else begin
300300+ Buffer.add_char buf fmt.[!i];
301301+ incr i
302302+ end
303303+ done;
304304+ Buffer.contents buf
305305+306306+(* ── Public entry: the SQL date/time functions ───────────────── *)
307307+308308+let call name args =
309309+ match name with
310310+ | "date" -> (
311311+ match evaluate args with
312312+ | Some ijd -> Ast.Text (fmt_date ijd)
313313+ | None -> Null)
314314+ | "time" -> (
315315+ match evaluate args with
316316+ | Some ijd -> Ast.Text (fmt_time ijd)
317317+ | None -> Null)
318318+ | "datetime" -> (
319319+ match evaluate args with
320320+ | Some ijd -> Ast.Text (fmt_datetime ijd)
321321+ | None -> Null)
322322+ | "julianday" -> (
323323+ match evaluate args with
324324+ | Some ijd -> Ast.Float (julianday ijd)
325325+ | None -> Null)
326326+ | "unixepoch" -> (
327327+ match evaluate args with
328328+ | Some ijd -> Ast.Int (unixepoch_of ijd)
329329+ | None -> Null)
330330+ | "strftime" -> (
331331+ match args with
332332+ | fmt :: rest -> (
333333+ match evaluate rest with
334334+ | Some ijd -> Ast.Text (strftime (text_of_value fmt) ijd)
335335+ | None -> Null)
336336+ | [] -> Null)
337337+ | _ -> invalid_arg ("Datetime.call: not a date/time function: " ^ name)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** SQLite date and time functions (lang_datefunc.html).
77+88+ Time is carried internally as a Julian Day count in integer milliseconds,
99+ exactly as sqlite3 does, so the deterministic surface -- explicit time
1010+ strings, Julian and unix numbers, the unit and "start of" modifiers -- is
1111+ exact and round-trips. The wall-clock forms ('now', 'localtime', 'utc')
1212+ yield NULL until a clock is injected. *)
1313+1414+val call : string -> Ast.value list -> Ast.value
1515+(** [call name args] evaluates the date/time function [name] (one of ["date"],
1616+ ["time"], ["datetime"], ["julianday"], ["unixepoch"], ["strftime"]) on
1717+ [args]: the first argument (after the format string for [strftime]) is the
1818+ time value and the rest are modifiers. An unparseable value, out-of-range
1919+ field, or unknown modifier yields [Null], as in sqlite3.
2020+2121+ @raise Invalid_argument if [name] is not a date/time function. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2026 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(* SQLite's built-in scalar and table-valued functions, over {!Catalog.Value.t}.
77+ A pure value-level layer (no AST, no evaluator): the dispatcher {!apply} maps a
88+ function name and its already-evaluated argument values to a result, sharing
99+ the value semantics in {!Value_ops}, so the tree-walker and the register VM
1010+ apply a function the same way. *)
1111+1212+open Catalog.Value
1313+open Value_ops
1414+1515+type value = Catalog.Value.t
1616+1717+let series_cap = 10_000_000
1818+let format_cap = 1_000_000
1919+2020+(* [generate_series(start, stop [, step])] yields one [value] column from [start]
2121+ to [stop] inclusive, stepping by [step] (default 1, must be non-zero). A
2222+ single-argument (unbounded) or over-[series_cap] range cannot be materialised
2323+ eagerly and raises, rather than hanging. *)
2424+let generate_series_rows (args : value list) : value list list =
2525+ let to_int = function
2626+ | Int n -> n
2727+ | Float f -> Int64.of_float f
2828+ | _ -> failwith "generate_series: integer arguments expected"
2929+ in
3030+ let start, stop, step =
3131+ match args with
3232+ | [ a; b ] -> (to_int a, to_int b, 1L)
3333+ | [ a; b; c ] -> (to_int a, to_int b, to_int c)
3434+ | [ _ ] -> failwith "generate_series: a stop bound is required"
3535+ | _ -> failwith "generate_series: 1 to 3 arguments expected"
3636+ in
3737+ if step = 0L then failwith "generate_series: step must be non-zero";
3838+ let span = if step > 0L then Int64.sub stop start else Int64.sub start stop in
3939+ let count =
4040+ if span < 0L then 0L else Int64.add (Int64.div span (Int64.abs step)) 1L
4141+ in
4242+ if count > Int64.of_int series_cap then
4343+ failwith "generate_series: range too large to materialise";
4444+ List.init (Int64.to_int count) (fun i ->
4545+ [ Int (Int64.add start (Int64.mul step (Int64.of_int i))) ])
4646+4747+(* A table-valued function's output column names (without materialising rows). *)
4848+let table_function_columns name =
4949+ match name with
5050+ | "generate_series" -> [ "value" ]
5151+ | _ -> Fmt.failwith "no such table-valued function: %s" name
5252+5353+(* Dispatch a table-valued function to its [(column names, rows)]. *)
5454+let table_function name (args : value list) : string list * value list list =
5555+ match name with
5656+ | "generate_series" -> ([ "value" ], generate_series_rows args)
5757+ | _ -> Fmt.failwith "no such table-valued function: %s" name
5858+5959+(* Number of UTF-8 characters (each malformed byte sequence counts as one),
6060+ matching SQLite's length() on TEXT. *)
6161+let utf8_length s = Uutf.String.fold_utf_8 (fun n _ _ -> n + 1) 0 s
6262+6363+(* upper/lower fold ASCII only by default (lang_corefunc.html); other bytes pass
6464+ through, matching SQLite built without ICU. *)
6565+let ascii_upper =
6666+ String.map (fun c ->
6767+ if c >= 'a' && c <= 'z' then Char.chr (Char.code c - 32) else c)
6868+6969+let ascii_lower =
7070+ String.map (fun c ->
7171+ if c >= 'A' && c <= 'Z' then Char.chr (Char.code c + 32) else c)
7272+7373+(* round(X, n): always REAL, rounding half away from zero (OCaml's Float.round).
7474+ Scaling by 10^n is exact enough for ordinary magnitudes (SQLite's own result
7575+ is bounded by IEEE-754 double precision regardless). *)
7676+let round_value x n =
7777+ let n = max 0 (min 30 n) in
7878+ let s = 10. ** float_of_int n in
7979+ Float (Float.round (x *. s) /. s)
8080+8181+(* Trim characters in [set] (default a single space, per SQLite) from [side] of
8282+ [s]. Byte-based, exact for ASCII; multi-byte trim sets are a TODO. *)
8383+let trim_value side set s =
8484+ let is_trim c = String.contains set c in
8585+ let n = String.length s in
8686+ let i = ref 0 and j = ref n in
8787+ if side <> `Right then
8888+ while !i < !j && is_trim s.[!i] do
8989+ incr i
9090+ done;
9191+ if side <> `Left then
9292+ while !j > !i && is_trim s.[!j - 1] do
9393+ decr j
9494+ done;
9595+ String.sub s !i (!j - !i)
9696+9797+let typeof_name = function
9898+ | Null -> "null"
9999+ | Int _ -> "integer"
100100+ | Float _ -> "real"
101101+ | Text _ -> "text"
102102+ | Blob _ -> "blob"
103103+104104+let as_float = function
105105+ | Int i -> Int64.to_float i
106106+ | Float f -> f
107107+ | v -> (
108108+ match float_of_string_opt (String.trim (text_of_value v)) with
109109+ | Some f -> f
110110+ | None -> 0.)
111111+112112+let trim_func side v set =
113113+ match (v, set) with
114114+ | Null, _ | _, Null -> Null
115115+ | _ -> Text (trim_value side (text_of_value set) (text_of_value v))
116116+117117+(* A real-valued math function: NULL on NULL, and NULL when the result is not
118118+ finite (domain errors like sqrt(-1) -> NaN, ln(0) -> -inf), per
119119+ lang_mathfunc.html. *)
120120+let math1 f = function
121121+ | Null -> Null
122122+ | v ->
123123+ let r = f (as_float v) in
124124+ if Float.is_finite r then Float r else Null
125125+126126+let math2 f a b =
127127+ match (a, b) with
128128+ | Null, _ | _, Null -> Null
129129+ | _ ->
130130+ let r = f (as_float a) (as_float b) in
131131+ if Float.is_finite r then Float r else Null
132132+133133+(* ceil/floor/trunc preserve an integer argument and otherwise round a real
134134+ (func7.test: floor(17) -> 17, ceil(42.2) -> 43.0). *)
135135+let round_like f = function
136136+ | Null -> Null
137137+ | Int _ as v -> v
138138+ | v -> Float (f (as_float v))
139139+140140+(* SQLite hex() and the X'..' blob literal use uppercase hex; Ohex.encode is
141141+ lowercase. *)
142142+let hex_of_string s = String.uppercase_ascii (Ohex.encode s)
143143+144144+(* UTF-8 encoding of a Unicode code point, for char(); an out-of-range value
145145+ yields the replacement character, as SQLite substitutes for invalid input. *)
146146+let utf8_of_cp cp =
147147+ let b = Buffer.create 4 in
148148+ let u = if Uchar.is_valid cp then Uchar.of_int cp else Uchar.rep in
149149+ Uutf.Buffer.add_utf_8 b u;
150150+ Buffer.contents b
151151+152152+(* Code point of the first character of a UTF-8 string, for unicode(). *)
153153+let first_cp s =
154154+ Uutf.String.fold_utf_8
155155+ (fun acc _ d ->
156156+ match acc with
157157+ | Some _ -> acc
158158+ | None -> (
159159+ match d with
160160+ | `Uchar u -> Some (Uchar.to_int u)
161161+ | `Malformed _ -> Some (Uchar.to_int Uchar.rep)))
162162+ None s
163163+164164+let fn_coalesce args =
165165+ match List.find_opt (fun v -> v <> Null) args with
166166+ | Some v -> v
167167+ | None -> Null
168168+169169+(* NULL for NULL, else apply [f] to the text form. *)
170170+let fn_text_map f v =
171171+ match v with Null -> Null | _ -> Text (f (text_of_value v))
172172+173173+(* length(X): characters for TEXT (lang_corefunc.html), bytes for BLOB, the
174174+ length of the string form for numbers, NULL for NULL. *)
175175+let fn_length v =
176176+ match v with
177177+ | Null -> Null
178178+ | Text s -> Int (Int64.of_int (utf8_length s))
179179+ | Blob s -> Int (Int64.of_int (String.length s))
180180+ | Int _ | Float _ -> Int (Int64.of_int (String.length (text_of_value v)))
181181+182182+(* octet_length(X): the number of bytes in X's text/blob representation (so
183183+ multi-byte UTF-8 counts each byte), NULL for NULL. *)
184184+let fn_octet_length = function
185185+ | Null -> Null
186186+ | v -> Int (Int64.of_int (String.length (text_of_value v)))
187187+188188+let is_hex_digit c =
189189+ (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
190190+191191+(* unhex(X): the blob whose bytes are the hex digit pairs of X; NULL if X has an
192192+ odd length or any non-hex character. *)
193193+let fn_unhex = function
194194+ | Null -> Null
195195+ | v ->
196196+ let s = text_of_value v in
197197+ let n = String.length s in
198198+ if n mod 2 <> 0 || not (String.for_all is_hex_digit s) then Null
199199+ else begin
200200+ let b = Bytes.create (n / 2) in
201201+ for i = 0 to (n / 2) - 1 do
202202+ Bytes.set b i
203203+ (Char.chr (int_of_string ("0x" ^ String.sub s (2 * i) 2)))
204204+ done;
205205+ Blob (Bytes.unsafe_to_string b)
206206+ end
207207+208208+(* zeroblob(N): a blob of N zero bytes (N clamped at 0), NULL for NULL. *)
209209+let fn_zeroblob = function
210210+ | Null -> Null
211211+ | v -> Blob (String.make (max 0 (Int64.to_int (intify v))) '\000')
212212+213213+(* instr(X, Y): 1-based byte position of the first Y in X, 0 if absent, NULL
214214+ if either is NULL. Byte-based, so exact for ASCII/BLOB. *)
215215+let fn_instr x y =
216216+ match (x, y) with
217217+ | Null, _ | _, Null -> Null
218218+ | _ ->
219219+ let hay = text_of_value x and needle = text_of_value y in
220220+ let pos =
221221+ if needle = "" then 1
222222+ else
223223+ match Re.exec_opt Re.(compile (str needle)) hay with
224224+ | None -> 0
225225+ | Some g -> Re.Group.start g 0 + 1
226226+ in
227227+ Int (Int64.of_int pos)
228228+229229+(* substr(X, Y[, Z]): substring from 1-based byte offset Y (negative counts
230230+ from the end) of length Z (to the end if omitted). *)
231231+let fn_substr x y rest =
232232+ match x with
233233+ | Null -> Null
234234+ | _ ->
235235+ (* substr of a blob is a blob (byte offsets); of anything else, text. *)
236236+ let wrap =
237237+ match x with Blob _ -> fun s -> Blob s | _ -> fun s -> Text s
238238+ in
239239+ let s = text_of_value x in
240240+ let len = String.length s in
241241+ let y =
242242+ match y with
243243+ | Null -> 0
244244+ | v -> Int64.to_int (Int64.of_float (float_of_value v))
245245+ in
246246+ let start = if y < 0 then max 0 (len + y) else max 0 (y - 1) in
247247+ let avail = len - start in
248248+ let n =
249249+ match rest with
250250+ | z :: _ ->
251251+ min avail (max 0 (Int64.to_int (Int64.of_float (float_of_value z))))
252252+ | [] -> avail
253253+ in
254254+ if start >= len then wrap "" else wrap (String.sub s start n)
255255+256256+(* abs(X): type-preserving; the most-negative integer overflows (func-18.32);
257257+ text/blob coerce to a number. *)
258258+let fn_abs v =
259259+ match v with
260260+ | Null -> Null
261261+ | Int i ->
262262+ if i = Int64.min_int then Fmt.failwith "integer overflow"
263263+ else Int (Int64.abs i)
264264+ | Float f -> Float (Float.abs f)
265265+ | _ -> Float (Float.abs (as_float v))
266266+267267+let fn_round1 v = match v with Null -> Null | _ -> round_value (as_float v) 0
268268+269269+let fn_round2 v d =
270270+ match (v, d) with
271271+ | Null, _ | _, Null -> Null
272272+ | _ -> round_value (as_float v) (Int64.to_int (Int64.of_float (as_float d)))
273273+274274+(* replace(X, Y, Z): all non-overlapping; empty Y returns X; NULL is
275275+ contagious. *)
276276+let fn_replace x y z =
277277+ match (x, y, z) with
278278+ | Null, _, _ | _, Null, _ | _, _, Null -> Null
279279+ | _ ->
280280+ let hay = text_of_value x and needle = text_of_value y in
281281+ if needle = "" then Text hay
282282+ else
283283+ Text
284284+ (Re.replace_string
285285+ Re.(compile (str needle))
286286+ ~by:(text_of_value z) hay)
287287+288288+(* sign(X): integer -1/0/+1 for a number, NULL otherwise (func7.test). *)
289289+let fn_sign v =
290290+ match v with
291291+ | Null -> Null
292292+ | Int _ | Float _ ->
293293+ let f = as_float v in
294294+ Int (if f < 0. then -1L else if f > 0. then 1L else 0L)
295295+ | _ -> Null
296296+297297+let fn_quote v =
298298+ Text
299299+ (match v with
300300+ | Null -> "NULL"
301301+ (* quote() renders an overflowed real as sqlite3's roundtrippable
302302+ 9.0e+999 sentinel, not the "Inf" text form. *)
303303+ | Float f when f = Float.infinity -> "9.0e+999"
304304+ | Float f when f = Float.neg_infinity -> "-9.0e+999"
305305+ | Int _ | Float _ -> text_of_value v
306306+ | Text s -> "'" ^ String.concat "''" (String.split_on_char '\'' s) ^ "'"
307307+ | Blob s -> "X'" ^ hex_of_string s ^ "'")
308308+309309+let fn_char args =
310310+ Text
311311+ (String.concat ""
312312+ (List.map
313313+ (fun v -> utf8_of_cp (Int64.to_int (Int64.of_float (as_float v))))
314314+ args))
315315+316316+let fn_unicode v =
317317+ match v with
318318+ | Null -> Null
319319+ | _ -> (
320320+ match first_cp (text_of_value v) with
321321+ | Some cp -> Int (Int64.of_int cp)
322322+ | None -> Null)
323323+324324+(* concat/concat_ws: NULL arguments are skipped (func9.test); concat_ws takes
325325+ the separator first and is NULL if the separator is NULL. *)
326326+let fn_concat args =
327327+ Text
328328+ (String.concat ""
329329+ (List.filter_map
330330+ (function Null -> None | v -> Some (text_of_value v))
331331+ args))
332332+333333+let fn_concat_ws sep rest =
334334+ match sep with
335335+ | Null -> Null
336336+ | _ ->
337337+ Text
338338+ (String.concat (text_of_value sep)
339339+ (List.filter_map
340340+ (function Null -> None | v -> Some (text_of_value v))
341341+ rest))
342342+343343+(* Scalar max/min: NULL if any argument is NULL, else the argument that wins
344344+ under [keep] (a sign-of-comparison predicate) against the running extreme. *)
345345+let fn_extreme keep = function
346346+ | [] -> Null
347347+ | v :: vs ->
348348+ if List.exists (fun x -> x = Null) (v :: vs) then Null
349349+ else
350350+ List.fold_left
351351+ (fun a x -> if keep (compare_values x a) then x else a)
352352+ v vs
353353+354354+(* printf/format: a C-style formatter, matching sqlite3's printf() (the SQL
355355+ [format] function). Supports flags [- + space 0 #], width and precision
356356+ (literal or [*] consuming an argument), and conversions d i u x X o f e E g G
357357+ s c % plus sqlite's q (escape '), Q (quote + NULL-aware). sqlite extensions
358358+ like the ',' grouping flag are accepted and ignored. *)
359359+type fmt_spec = {
360360+ left : bool;
361361+ zero : bool;
362362+ plus : bool;
363363+ space : bool;
364364+ alt : bool;
365365+ width : int;
366366+ prec : int option;
367367+}
368368+369369+let coerce_float v =
370370+ match v with
371371+ | Int i -> Int64.to_float i
372372+ | Float x -> x
373373+ | _ -> (
374374+ match float_of_string_opt (String.trim (text_of_value v)) with
375375+ | Some x -> x
376376+ | None -> 0.)
377377+378378+let fmt_quote s = String.concat "''" (String.split_on_char '\'' s)
379379+380380+(* Right- or left-justify [s] in the field width with spaces. *)
381381+let fmt_pad spec s =
382382+ let p = spec.width - String.length s in
383383+ if p <= 0 then s
384384+ else if spec.left then s ^ String.make p ' '
385385+ else String.make p ' ' ^ s
386386+387387+(* Place [pfx] (sign or radix prefix) then [digits] in the field; zero-padding
388388+ goes between the prefix and the digits, unless a precision was given. *)
389389+let fmt_pad_num spec pfx digits =
390390+ let body = pfx ^ digits in
391391+ let p = spec.width - String.length body in
392392+ if p <= 0 then body
393393+ else if spec.left then body ^ String.make p ' '
394394+ else if spec.zero && spec.prec = None then pfx ^ String.make p '0' ^ digits
395395+ else String.make p ' ' ^ body
396396+397397+(* Left-pad a digit string with zeros up to the precision. *)
398398+let fmt_prec prec d =
399399+ match prec with
400400+ | Some p when String.length d < p -> String.make (p - String.length d) '0' ^ d
401401+ | _ -> d
402402+403403+let fmt_int spec conv v =
404404+ match conv with
405405+ | 'u' -> fmt_pad_num spec "" (Fmt.kstr (fmt_prec spec.prec) "%Lu" (intify v))
406406+ | 'x' | 'X' | 'o' ->
407407+ let x = intify v in
408408+ let body =
409409+ fmt_prec spec.prec
410410+ (match conv with
411411+ | 'x' -> Fmt.str "%Lx" x
412412+ | 'X' -> Fmt.str "%LX" x
413413+ | _ -> Fmt.str "%Lo" x)
414414+ in
415415+ let pfx =
416416+ if (not spec.alt) || x = 0L then ""
417417+ else match conv with 'x' -> "0x" | 'X' -> "0X" | _ -> "0"
418418+ in
419419+ fmt_pad_num spec pfx body
420420+ | _ ->
421421+ let x = intify v in
422422+ let neg = x < 0L in
423423+ let mag = if neg then Int64.neg x else x in
424424+ let pfx =
425425+ if neg then "-"
426426+ else if spec.plus then "+"
427427+ else if spec.space then " "
428428+ else ""
429429+ in
430430+ fmt_pad_num spec pfx (Fmt.kstr (fmt_prec spec.prec) "%Lu" mag)
431431+432432+let fmt_float_core conv p x =
433433+ match conv with
434434+ | 'f' -> Fmt.str "%.*f" p x
435435+ | 'e' -> Fmt.str "%.*e" p x
436436+ | 'E' -> Fmt.str "%.*E" p x
437437+ | 'g' -> Fmt.str "%.*g" p x
438438+ | _ -> Fmt.str "%.*G" p x
439439+440440+(* Zero-pad [signed] (which may carry a leading sign) to the field width, with
441441+ the zeros falling after any sign. *)
442442+let fmt_float_zeropad spec signed =
443443+ let has_sign =
444444+ signed <> "" && (signed.[0] = '-' || signed.[0] = '+' || signed.[0] = ' ')
445445+ in
446446+ let zeros = String.make (spec.width - String.length signed) '0' in
447447+ if has_sign then
448448+ String.sub signed 0 1 ^ zeros
449449+ ^ String.sub signed 1 (String.length signed - 1)
450450+ else zeros ^ signed
451451+452452+let fmt_float spec conv v =
453453+ let x = coerce_float v in
454454+ let core = fmt_float_core conv (Option.value spec.prec ~default:6) x in
455455+ let signed =
456456+ if x >= 0. && spec.plus then "+" ^ core
457457+ else if x >= 0. && spec.space then " " ^ core
458458+ else core
459459+ in
460460+ if (not spec.left) && spec.zero && String.length signed < spec.width then
461461+ fmt_float_zeropad spec signed
462462+ else fmt_pad spec signed
463463+464464+(* Render one conversion [conv] under [spec], pulling argument(s) from [pop]. *)
465465+let fmt_conv spec conv pop =
466466+ let text () = match pop () with Null -> "" | v -> text_of_value v in
467467+ match conv with
468468+ | 'd' | 'i' | 'u' | 'x' | 'X' | 'o' -> fmt_int spec conv (pop ())
469469+ | 'f' | 'e' | 'E' | 'g' | 'G' -> fmt_float spec conv (pop ())
470470+ | 's' ->
471471+ let s = text () in
472472+ let s =
473473+ match spec.prec with
474474+ | Some p when p < String.length s -> String.sub s 0 p
475475+ | _ -> s
476476+ in
477477+ fmt_pad spec s
478478+ | 'c' ->
479479+ let s = text () in
480480+ fmt_pad spec (if s = "" then "" else String.sub s 0 1)
481481+ | 'q' -> fmt_pad spec (fmt_quote (text ()))
482482+ | 'Q' -> (
483483+ match pop () with
484484+ | Null -> fmt_pad spec "NULL"
485485+ | v -> fmt_pad spec ("'" ^ fmt_quote (text_of_value v) ^ "'"))
486486+ | c -> String.make 1 c
487487+488488+(* Parse the flags of a conversion spec starting at [i]; returns the flag record
489489+ and the index of the first non-flag character. *)
490490+let fmt_parse_flags f n i =
491491+ let left = ref false
492492+ and zero = ref false
493493+ and plus = ref false
494494+ and space = ref false
495495+ and alt = ref false in
496496+ let j = ref i and scanning = ref true in
497497+ while !scanning && !j < n do
498498+ (match f.[!j] with
499499+ | '-' -> left := true
500500+ | '0' -> zero := true
501501+ | '+' -> plus := true
502502+ | ' ' -> space := true
503503+ | '#' -> alt := true
504504+ | ',' | '!' -> ()
505505+ | _ -> scanning := false);
506506+ if !scanning then incr j
507507+ done;
508508+ ((!left, !zero, !plus, !space, !alt), !j)
509509+510510+(* Parse a width/precision number at [i]: a literal integer, a [*] consuming an
511511+ argument via [pop], or none. Returns the value (if any) and the next index. *)
512512+let fmt_checked_bound n =
513513+ let limit = Int64.of_int format_cap in
514514+ if n > limit || n < Int64.neg limit then
515515+ failwith "format: width or precision too large";
516516+ Int64.to_int n
517517+518518+let fmt_parse_num f n pop i =
519519+ if i < n && f.[i] = '*' then
520520+ (Some (fmt_checked_bound (intify (pop ()))), i + 1)
521521+ else
522522+ let j = ref i in
523523+ while !j < n && f.[!j] >= '0' && f.[!j] <= '9' do
524524+ incr j
525525+ done;
526526+ if !j > i then
527527+ let s = String.sub f i (!j - i) in
528528+ let n =
529529+ match Int64.of_string_opt s with
530530+ | Some n -> fmt_checked_bound n
531531+ | None -> failwith "format: width or precision too large"
532532+ in
533533+ (Some n, !j)
534534+ else (None, i)
535535+536536+(* Parse a full conversion spec just past the [%] at [start]. Returns the spec,
537537+ the conversion character (None for a trailing [%]), and the next index. *)
538538+let fmt_parse_spec f n pop start =
539539+ let (left, zero, plus, space, alt), i = fmt_parse_flags f n start in
540540+ let width0, i = fmt_parse_num f n pop i in
541541+ let left, width =
542542+ match width0 with
543543+ | Some w when w < 0 -> (true, -w)
544544+ | Some w -> (left, w)
545545+ | None -> (left, 0)
546546+ in
547547+ let prec, i =
548548+ if i < n && f.[i] = '.' then
549549+ let p, i = fmt_parse_num f n pop (i + 1) in
550550+ let p =
551551+ match p with
552552+ | Some p when p < 0 -> None
553553+ | Some p -> Some p
554554+ | None -> Some 0
555555+ in
556556+ (p, i)
557557+ else (None, i)
558558+ in
559559+ let spec = { left; zero; plus; space; alt; width; prec } in
560560+ if i < n then (spec, Some f.[i], i + 1) else (spec, None, i)
561561+562562+(* Emit the directive that begins just past the [%] at [start]; return the next
563563+ index. A doubled [%%] is a literal percent. *)
564564+let fmt_directive buf f n pop start =
565565+ if start < n && f.[start] = '%' then begin
566566+ Buffer.add_char buf '%';
567567+ start + 1
568568+ end
569569+ else
570570+ let spec, conv, j = fmt_parse_spec f n pop start in
571571+ (match conv with
572572+ | None -> ()
573573+ | Some c -> Buffer.add_string buf (fmt_conv spec c pop));
574574+ j
575575+576576+let fn_format args =
577577+ match args with
578578+ | [] | Null :: _ -> Null
579579+ | fmt :: rest ->
580580+ let f = text_of_value fmt in
581581+ let n = String.length f in
582582+ let buf = Buffer.create (n * 2) in
583583+ let pending = ref rest in
584584+ let pop () =
585585+ match !pending with
586586+ | x :: tl ->
587587+ pending := tl;
588588+ x
589589+ | [] -> Null
590590+ in
591591+ let i = ref 0 in
592592+ while !i < n do
593593+ if f.[!i] <> '%' then begin
594594+ Buffer.add_char buf f.[!i];
595595+ incr i
596596+ end
597597+ else i := fmt_directive buf f n pop (!i + 1)
598598+ done;
599599+ Text (Buffer.contents buf)
600600+601601+(* ── JSON (json1) ────────────────────────────────────────────── *)
602602+603603+let json_of_sql = function
604604+ | Null -> Jsonval.Null
605605+ | Int i -> Jsonval.Int i
606606+ | Float f -> Jsonval.real f
607607+ | Text s -> Jsonval.Str s
608608+ | Blob s -> Jsonval.Str s
609609+610610+(* A json_extract / ->> result as a SQL value: scalars become their SQL form,
611611+ containers their JSON text. *)
612612+let sql_of_json = function
613613+ | Jsonval.Null -> Null
614614+ | Jsonval.Bool b -> Int (if b then 1L else 0L)
615615+ | Jsonval.Int i -> Int i
616616+ | Jsonval.Real s -> (
617617+ match float_of_string_opt s with Some f -> Float f | None -> Null)
618618+ | Jsonval.Str s -> Text s
619619+ | (Jsonval.Arr _ | Jsonval.Obj _) as j -> Text (Jsonval.to_text j)
620620+621621+let json_parse_arg v =
622622+ match Jsonval.parse (text_of_value v) with
623623+ | Some j -> j
624624+ | None -> failwith "malformed JSON"
625625+626626+(* json_extract(X, P, ...): one path returns the SQL value at P (NULL if
627627+ absent); several paths return a JSON array of the results. *)
628628+let fn_json_extract jv paths =
629629+ let j = json_parse_arg jv in
630630+ match paths with
631631+ | [ p ] -> (
632632+ match Jsonval.lookup j (text_of_value p) with
633633+ | Some r -> sql_of_json r
634634+ | None -> Null)
635635+ | _ ->
636636+ let pick p =
637637+ match Jsonval.lookup j (text_of_value p) with
638638+ | Some r -> r
639639+ | None -> Jsonval.Null
640640+ in
641641+ Text (Jsonval.to_text (Jsonval.Arr (List.map pick paths)))
642642+643643+(* The path of a [->]/[->>] right operand: a full [$...] path passes through, an
644644+ integer becomes [$[n]], any other label becomes [$.label]. *)
645645+let arrow_path = function
646646+ | Int i -> Fmt.str "$[%Ld]" i
647647+ | v ->
648648+ let s = text_of_value v in
649649+ if String.length s > 0 && s.[0] = '$' then s else "$." ^ s
650650+651651+(* a -> p : the element at p as JSON text. a ->> p : as a SQL value. *)
652652+let json_arrow ~as_text jv pv =
653653+ match (jv, pv) with
654654+ | Null, _ | _, Null -> Null
655655+ | _ -> (
656656+ let j = json_parse_arg jv in
657657+ match Jsonval.lookup j (arrow_path pv) with
658658+ | None -> Null
659659+ | Some r -> if as_text then sql_of_json r else Text (Jsonval.to_text r))
660660+661661+(* json_type / json_array_length, optionally at a path. *)
662662+let json_at jv path_opt =
663663+ let j = json_parse_arg jv in
664664+ match path_opt with
665665+ | None -> Some j
666666+ | Some p -> Jsonval.lookup j (text_of_value p)
667667+668668+let apply name args =
669669+ match (name, args) with
670670+ | "coalesce", _ -> fn_coalesce args
671671+ (* like(Y, X [, E]) is the function form of [X LIKE Y [ESCAPE E]] -- the
672672+ pattern is the first argument (lang_corefunc.html). *)
673673+ | "like", [ y; x ] -> like_op x y
674674+ | "like", [ y; x; e ] -> like_op ~escape:e x y
675675+ | "glob", [ y; x ] -> glob_op x y
676676+ | "length", [ v ] -> fn_length v
677677+ | "instr", [ x; y ] -> fn_instr x y
678678+ | "substr", x :: y :: rest -> fn_substr x y rest
679679+ | "upper", [ v ] -> fn_text_map ascii_upper v
680680+ | "lower", [ v ] -> fn_text_map ascii_lower v
681681+ | "abs", [ v ] -> fn_abs v
682682+ | "round", [ v ] -> fn_round1 v
683683+ | "round", [ v; d ] -> fn_round2 v d
684684+ | "trim", [ v ] -> fn_text_map (trim_value `Both " ") v
685685+ | "ltrim", [ v ] -> fn_text_map (trim_value `Left " ") v
686686+ | "rtrim", [ v ] -> fn_text_map (trim_value `Right " ") v
687687+ | "trim", [ v; set ] -> trim_func `Both v set
688688+ | "ltrim", [ v; set ] -> trim_func `Left v set
689689+ | "rtrim", [ v; set ] -> trim_func `Right v set
690690+ | "replace", [ x; y; z ] -> fn_replace x y z
691691+ | "ifnull", [ a; b ] -> if a <> Null then a else b
692692+ (* nullif(X, Y): NULL if X = Y (so nullif(1, NULL) is 1, since 1=NULL is not
693693+ true), else X. *)
694694+ | "nullif", [ a; b ] -> (
695695+ (* NULLIF(a,b) is a unless a and b are non-NULL and compare equal *)
696696+ match (a, b) with
697697+ | Null, _ | _, Null -> a
698698+ | _ -> if compare_values a b = 0 then Null else a)
699699+ | "typeof", [ v ] -> Text (typeof_name v)
700700+ (* Optimizer-hint no-ops (lang_corefunc.html): they return their first
701701+ argument unchanged. *)
702702+ | ("likely" | "unlikely"), [ x ] -> x
703703+ | "likelihood", [ x; _ ] -> x
704704+ (* Math functions (lang_mathfunc.html); all real-valued except ceil/floor/
705705+ trunc, which preserve an integer argument. *)
706706+ | "sqrt", [ v ] -> math1 sqrt v
707707+ | ("pow" | "power"), [ a; b ] -> math2 Float.pow a b
708708+ | ("ceil" | "ceiling"), [ v ] -> round_like Float.ceil v
709709+ | "floor", [ v ] -> round_like Float.floor v
710710+ | "trunc", [ v ] -> round_like Float.trunc v
711711+ | "exp", [ v ] -> math1 exp v
712712+ | "ln", [ v ] -> math1 log v
713713+ | ("log10" | "log"), [ v ] -> math1 log10 v
714714+ | "log2", [ v ] -> math1 Float.log2 v
715715+ | "log", [ b; v ] -> math2 (fun b x -> log x /. log b) b v
716716+ | "sin", [ v ] -> math1 sin v
717717+ | "cos", [ v ] -> math1 cos v
718718+ | "tan", [ v ] -> math1 tan v
719719+ | "asin", [ v ] -> math1 asin v
720720+ | "acos", [ v ] -> math1 acos v
721721+ | "atan", [ v ] -> math1 atan v
722722+ | "atan2", [ y; x ] -> math2 atan2 y x
723723+ | "degrees", [ v ] -> math1 (fun r -> r *. 180. /. Float.pi) v
724724+ | "radians", [ v ] -> math1 (fun d -> d *. Float.pi /. 180.) v
725725+ | "pi", [] -> Float Float.pi
726726+ | "sign", [ v ] -> fn_sign v
727727+ | "hex", [ v ] -> fn_text_map hex_of_string v
728728+ | "unhex", [ v ] -> fn_unhex v
729729+ | "octet_length", [ v ] -> fn_octet_length v
730730+ | "zeroblob", [ v ] -> fn_zeroblob v
731731+ | "quote", [ v ] -> fn_quote v
732732+ | "char", args -> fn_char args
733733+ | "unicode", [ v ] -> fn_unicode v
734734+ | "concat", args -> fn_concat args
735735+ | "concat_ws", sep :: rest -> fn_concat_ws sep rest
736736+ | ("printf" | "format"), args -> fn_format args
737737+ | ( ("date" | "time" | "datetime" | "julianday" | "unixepoch" | "strftime"),
738738+ args ) ->
739739+ Datetime.call name args
740740+ | "json", [ Null ] -> Null
741741+ | "json", [ v ] -> Text (Jsonval.to_text (json_parse_arg v))
742742+ | "json_valid", [ Null ] -> Null
743743+ | "json_valid", [ v ] ->
744744+ Int (if Jsonval.parse (text_of_value v) <> None then 1L else 0L)
745745+ | "json_quote", [ v ] -> Text (Jsonval.to_text (json_of_sql v))
746746+ | "json_array", args ->
747747+ Text (Jsonval.to_text (Jsonval.Arr (List.map json_of_sql args)))
748748+ | "json_object", args ->
749749+ let rec pairs = function
750750+ | k :: v :: rest -> (text_of_value k, json_of_sql v) :: pairs rest
751751+ | _ -> []
752752+ in
753753+ Text (Jsonval.to_text (Jsonval.Obj (pairs args)))
754754+ | "json_extract", jv :: (_ :: _ as paths) -> fn_json_extract jv paths
755755+ | "json_type", [ v ] -> Text (Jsonval.type_name (json_parse_arg v))
756756+ | "json_type", [ v; p ] -> (
757757+ match json_at v (Some p) with
758758+ | Some j -> Text (Jsonval.type_name j)
759759+ | None -> Null)
760760+ | "json_array_length", ([ v ] | [ v; _ ]) -> (
761761+ let path_opt = match args with [ _; p ] -> Some p | _ -> None in
762762+ match json_at v path_opt with
763763+ | Some (Jsonval.Arr xs) -> Int (Int64.of_int (List.length xs))
764764+ | Some _ -> Int 0L
765765+ | None -> Null)
766766+ (* Scalar max/min (two or more arguments): NULL if any argument is NULL, else
767767+ the largest/smallest by SQLite value ordering. *)
768768+ | "max", (_ :: _ :: _ as args) -> fn_extreme (fun c -> c > 0) args
769769+ | "min", (_ :: _ :: _ as args) -> fn_extreme (fun c -> c < 0) args
770770+ | _ -> Fmt.failwith "unknown function %s()" name
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2026 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** SQLite's built-in scalar and table-valued functions over {!Catalog.Value.t}.
77+88+ A pure value-level layer (no AST, no evaluator) shared by the tree-walker
99+ and the register VM, so a function applies the same way in both. *)
1010+1111+val apply : string -> Catalog.Value.t list -> Catalog.Value.t
1212+(** [apply name args] applies the scalar function [name] to its evaluated
1313+ argument values (length/substr/abs/round/coalesce/json_extract/printf/...).
1414+ @raise Failure on an unknown function or a domain error (e.g. a bad ESCAPE).
1515+*)
1616+1717+val json_arrow :
1818+ as_text:bool -> Catalog.Value.t -> Catalog.Value.t -> Catalog.Value.t
1919+(** [json_arrow ~as_text doc path] is the JSON [->] ([~as_text:false]) / [->>]
2020+ ([~as_text:true]) accessor: extract [path] from the JSON [doc]. *)
2121+2222+val table_function :
2323+ string -> Catalog.Value.t list -> string list * Catalog.Value.t list list
2424+(** [table_function name args] materialises a table-valued function (e.g.
2525+ [generate_series]) to its column names and rows. *)
2626+2727+val table_function_columns : string -> string list
2828+(** [table_function_columns name] is a table-valued function's output column
2929+ names, without materialising its rows. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(* SQLite's json1 value dialect (json.html). Parsing and validation reuse the
77+ {!Json} library (nox-json); this module adds what is sqlite-specific: the
88+ minified text serialisation sqlite3 emits (number and string formatting
99+ byte-compatible) and [$.a.b[2]] path navigation. The SQL functions in
1010+ {!Query} build on it. *)
1111+1212+type t =
1313+ | Null
1414+ | Bool of bool
1515+ | Int of int64
1616+ | Real of string (** the number's text, formatted by {!real} *)
1717+ | Str of string
1818+ | Arr of t list
1919+ | Obj of (string * t) list
2020+2121+exception Error of string
2222+2323+let type_name = function
2424+ | Null -> "null"
2525+ | Bool true -> "true"
2626+ | Bool false -> "false"
2727+ | Int _ -> "integer"
2828+ | Real _ -> "real"
2929+ | Str _ -> "text"
3030+ | Arr _ -> "array"
3131+ | Obj _ -> "object"
3232+3333+(* ── Serialisation ───────────────────────────────────────────── *)
3434+3535+(* A real as sqlite3 renders it in JSON: C [%.15g] (shortest within 15
3636+ significant digits, trailing zeros stripped), then a forced decimal point --
3737+ "10000000000.0", "1.0e+15", "0.1" -- so it always reads back as a real. *)
3838+let real_text f =
3939+ let s = Fmt.str "%.15g" f in
4040+ match String.index_opt s 'e' with
4141+ | Some i ->
4242+ let mant = String.sub s 0 i
4343+ and ex = String.sub s i (String.length s - i) in
4444+ let mant = if String.contains mant '.' then mant else mant ^ ".0" in
4545+ mant ^ ex
4646+ | None -> if String.contains s '.' then s else s ^ ".0"
4747+4848+(* Build a real node from a SQL double, formatted as sqlite3 would. *)
4949+let real f = Real (real_text f)
5050+5151+let escape_into buf s =
5252+ Buffer.add_char buf '"';
5353+ String.iter
5454+ (fun c ->
5555+ match c with
5656+ | '"' -> Buffer.add_string buf "\\\""
5757+ | '\\' -> Buffer.add_string buf "\\\\"
5858+ | '\n' -> Buffer.add_string buf "\\n"
5959+ | '\r' -> Buffer.add_string buf "\\r"
6060+ | '\t' -> Buffer.add_string buf "\\t"
6161+ | '\b' -> Buffer.add_string buf "\\b"
6262+ | '\012' -> Buffer.add_string buf "\\f"
6363+ | c when Char.code c < 0x20 ->
6464+ (* control characters as \u00XX; built directly to avoid a formatter
6565+ allocation per character *)
6666+ let hex = "0123456789abcdef" in
6767+ Buffer.add_string buf "\\u00";
6868+ Buffer.add_char buf hex.[(Char.code c lsr 4) land 0xf];
6969+ Buffer.add_char buf hex.[Char.code c land 0xf]
7070+ | c -> Buffer.add_char buf c)
7171+ s;
7272+ Buffer.add_char buf '"'
7373+7474+let to_text j =
7575+ let buf = Buffer.create 64 in
7676+ let rec go = function
7777+ | Null -> Buffer.add_string buf "null"
7878+ | Bool b -> Buffer.add_string buf (if b then "true" else "false")
7979+ | Int i -> Buffer.add_string buf (Int64.to_string i)
8080+ | Real s -> Buffer.add_string buf s
8181+ | Str s -> escape_into buf s
8282+ | Arr xs ->
8383+ Buffer.add_char buf '[';
8484+ List.iteri
8585+ (fun i x ->
8686+ if i > 0 then Buffer.add_char buf ',';
8787+ go x)
8888+ xs;
8989+ Buffer.add_char buf ']'
9090+ | Obj kvs ->
9191+ Buffer.add_char buf '{';
9292+ List.iteri
9393+ (fun i (k, v) ->
9494+ if i > 0 then Buffer.add_char buf ',';
9595+ escape_into buf k;
9696+ Buffer.add_char buf ':';
9797+ go v)
9898+ kvs;
9999+ Buffer.add_char buf '}'
100100+ in
101101+ go j;
102102+ Buffer.contents buf
103103+104104+let pp ppf j = Fmt.string ppf (to_text j)
105105+106106+(* ── Parsing (delegated to nox-json) ─────────────────────────── *)
107107+108108+(* Map a parsed nox-json value into the sqlite dialect tree. A number with a
109109+ fraction/exponent (nox-json [Float]) is re-formatted via {!real}; sqlite3
110110+ preserves the original lexeme of such a number, which this does not (the one
111111+ intentional divergence -- integers, strings, and structure are exact). *)
112112+let rec of_json (j : Json.t) : t =
113113+ match j with
114114+ | Json.Null _ -> Null
115115+ | Json.Bool (b, _) -> Bool b
116116+ | Json.Number (n, _) -> (
117117+ match n with
118118+ | Json.Number.Int i | Json.Number.Uint i -> Int i
119119+ | Json.Number.Float f -> real f)
120120+ | Json.String (s, _) -> Str s
121121+ | Json.Array (xs, _) -> Arr (List.map of_json xs)
122122+ | Json.Object (members, _) ->
123123+ Obj (List.map (fun ((k, _), v) -> (k, of_json v)) members)
124124+125125+let parse s =
126126+ match Json.Value.of_string s with Ok v -> Some (of_json v) | Error _ -> None
127127+128128+let parse_exn s =
129129+ match parse s with Some j -> j | None -> raise (Error "malformed JSON")
130130+131131+(* ── Path navigation ($.a.b[2]) ──────────────────────────────── *)
132132+133133+type step = Key of string | Index of int
134134+135135+(* Parse a path expression like [$.a.b[2]] or [$[0]] into steps. *)
136136+let parse_path p =
137137+ let n = String.length p in
138138+ if n = 0 || p.[0] <> '$' then None
139139+ else begin
140140+ let i = ref 1 and steps = ref [] and ok = ref true in
141141+ while !ok && !i < n do
142142+ match p.[!i] with
143143+ | '.' ->
144144+ incr i;
145145+ let start = !i in
146146+ while !i < n && match p.[!i] with '.' | '[' -> false | _ -> true do
147147+ incr i
148148+ done;
149149+ steps := Key (String.sub p start (!i - start)) :: !steps
150150+ | '[' ->
151151+ incr i;
152152+ let start = !i in
153153+ while !i < n && p.[!i] <> ']' do
154154+ incr i
155155+ done;
156156+ (match int_of_string_opt (String.sub p start (!i - start)) with
157157+ | Some k -> steps := Index k :: !steps
158158+ | None -> ok := false);
159159+ if !i < n then incr i (* past ']' *)
160160+ | _ -> ok := false
161161+ done;
162162+ if !ok then Some (List.rev !steps) else None
163163+ end
164164+165165+let rec navigate j = function
166166+ | [] -> Some j
167167+ | Key k :: rest -> (
168168+ match j with
169169+ | Obj kvs -> (
170170+ match List.assoc_opt k kvs with
171171+ | Some v -> navigate v rest
172172+ | None -> None)
173173+ | _ -> None)
174174+ | Index k :: rest -> (
175175+ match j with
176176+ | Arr xs -> (
177177+ match List.nth_opt xs k with
178178+ | Some v -> navigate v rest
179179+ | None -> None)
180180+ | _ -> None)
181181+182182+let lookup j path =
183183+ match parse_path path with None -> None | Some steps -> navigate j steps
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** SQLite's json1 value dialect (json.html). Parsing reuses the {!Json} library
77+ (nox-json); this module adds the minified text serialisation sqlite3 emits
88+ (number and string formatting byte-compatible) and [$.a.b[2]] path lookup.
99+*)
1010+1111+type t =
1212+ | Null
1313+ | Bool of bool
1414+ | Int of int64
1515+ | Real of string (** the number's text, formatted by {!real} *)
1616+ | Str of string
1717+ | Arr of t list
1818+ | Obj of (string * t) list
1919+2020+exception Error of string
2121+2222+val real : float -> t
2323+(** A real node from a SQL double, formatted as sqlite3 renders JSON reals. *)
2424+2525+val type_name : t -> string
2626+(** [type_name j] is the sqlite3 json_type name of [j]: ["null"], ["true"],
2727+ ["false"], ["integer"], ["real"], ["text"], ["array"], or ["object"]. *)
2828+2929+val pp : Format.formatter -> t -> unit
3030+(** Pretty-print as minified JSON text (same as {!to_text}). *)
3131+3232+val to_text : t -> string
3333+(** Minified JSON text, byte-compatible with sqlite3. *)
3434+3535+val parse : string -> t option
3636+(** [parse s] parses the JSON text [s] via nox-json, or [None] if it is
3737+ malformed. *)
3838+3939+val parse_exn : string -> t
4040+(** Parse JSON text. @raise Error on malformed input. *)
4141+4242+val lookup : t -> string -> t option
4343+(** [lookup j path] evaluates a path like [$.a.b[2]] against [j], or [None] if
4444+ the path is malformed or does not resolve. *)
···11+(** ocamllex lexer for SQLite CREATE TABLE statements. *)
22+33+exception Error of string
44+(** Raised on lexer errors (unterminated strings, unexpected input). *)
55+66+val token : Lexing.lexbuf -> Parser.token
77+(** [token lexbuf] returns the next token. *)
88+99+val parse : string -> (Ast.create_table, string) result
1010+(** [parse sql] parses a single CREATE TABLE statement. *)
1111+1212+val parse_statements : string -> (Ast.stmt list, string) result
1313+(** [parse_statements sql] parses a [;]-separated sequence of statements. *)
1414+1515+val split_statements : string -> string list
1616+(** [split_statements sql] returns the raw text of each top-level statement,
1717+ trimmed, with empty statements dropped. A [;] inside a string literal is not
1818+ a separator. *)
···11+{
22+(*---------------------------------------------------------------------------
33+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
44+ SPDX-License-Identifier: MIT
55+66+ ocamllex lexer for the SQLite SQL subset (CREATE TABLE plus queries).
77+88+ The grammar is ASCII-only at the punctuation / keyword level; quoted
99+ identifiers and string literals pass through the body bytes verbatim,
1010+ which means UTF-8 content round-trips untouched.
1111+ ---------------------------------------------------------------------------*)
1212+1313+exception Error of string
1414+1515+(* Keyword constructors — all carry the original text for case preservation *)
1616+let keywords =
1717+ [
1818+ ("CREATE", fun s -> Parser.CREATE s);
1919+ ("TABLE", fun s -> Parser.TABLE s);
2020+ ("RENAME", fun s -> Parser.RENAME s);
2121+ ("VIEW", fun s -> Parser.VIEW s);
2222+ ("TRIGGER", fun s -> Parser.TRIGGER s);
2323+ ("BEFORE", fun s -> Parser.BEFORE s);
2424+ ("AFTER", fun s -> Parser.AFTER s);
2525+ ("INSTEAD", fun s -> Parser.INSTEAD s);
2626+ ("FOR", fun s -> Parser.FOR s);
2727+ ("EACH", fun s -> Parser.EACH s);
2828+ ("ROW", fun s -> Parser.ROW s);
2929+ ("OF", fun s -> Parser.OF s);
3030+ ("CROSS", fun s -> Parser.CROSS s);
3131+ ("SAVEPOINT", fun s -> Parser.SAVEPOINT s);
3232+ ("RELEASE", fun s -> Parser.RELEASE s);
3333+ ("TO", fun s -> Parser.TO s);
3434+ ("CASCADE", fun s -> Parser.CASCADE s);
3535+ ("RESTRICT", fun s -> Parser.RESTRICT s);
3636+ ("NO", fun s -> Parser.NO s);
3737+ ("ACTION", fun s -> Parser.ACTION s);
3838+ ("ALTER", fun s -> Parser.ALTER s);
3939+ ("DROP", fun s -> Parser.DROP s);
4040+ ("ADD", fun s -> Parser.ADD s);
4141+ ("COLUMN", fun s -> Parser.COLUMN s);
4242+ ("IF", fun s -> Parser.IF s);
4343+ ("NOT", fun s -> Parser.NOT s);
4444+ ("EXISTS", fun s -> Parser.EXISTS s);
4545+ ("PRIMARY", fun s -> Parser.PRIMARY s);
4646+ ("KEY", fun s -> Parser.KEY s);
4747+ ("UNIQUE", fun s -> Parser.UNIQUE s);
4848+ ("NULL", fun s -> Parser.NULL s);
4949+ ("TRUE", fun s -> Parser.TRUE s);
5050+ ("FALSE", fun s -> Parser.FALSE s);
5151+ ("DEFAULT", fun s -> Parser.DEFAULT s);
5252+ ("CHECK", fun s -> Parser.CHECK s);
5353+ ("REFERENCES", fun s -> Parser.REFERENCES s);
5454+ ("COLLATE", fun s -> Parser.COLLATE s);
5555+ ("GENERATED", fun s -> Parser.GENERATED s);
5656+ ("ALWAYS", fun s -> Parser.ALWAYS s);
5757+ ("AS", fun s -> Parser.AS s);
5858+ ("AUTOINCREMENT", fun s -> Parser.AUTOINCREMENT s);
5959+ ("FOREIGN", fun s -> Parser.FOREIGN s);
6060+ ("CONSTRAINT", fun s -> Parser.CONSTRAINT s);
6161+ ("ON", fun s -> Parser.ON s);
6262+ ("ASC", fun s -> Parser.ASC s);
6363+ ("DESC", fun s -> Parser.DESC s);
6464+ (* Query keywords *)
6565+ ("SELECT", fun s -> Parser.SELECT s);
6666+ ("DISTINCT", fun s -> Parser.DISTINCT s);
6767+ ("FROM", fun s -> Parser.FROM s);
6868+ ("WHERE", fun s -> Parser.WHERE s);
6969+ ("JOIN", fun s -> Parser.JOIN s);
7070+ ("INNER", fun s -> Parser.INNER s);
7171+ ("LEFT", fun s -> Parser.LEFT s);
7272+ ("OUTER", fun s -> Parser.OUTER s);
7373+ ("ORDER", fun s -> Parser.ORDER s);
7474+ ("GROUP", fun s -> Parser.GROUP s);
7575+ ("HAVING", fun s -> Parser.HAVING s);
7676+ ("FILTER", fun s -> Parser.FILTER s);
7777+ ("OVER", fun s -> Parser.OVER s);
7878+ ("PARTITION", fun s -> Parser.PARTITION s);
7979+ ("ROWS", fun s -> Parser.ROWS s);
8080+ ("RANGE", fun s -> Parser.RANGE s);
8181+ ("PRECEDING", fun s -> Parser.PRECEDING s);
8282+ ("FOLLOWING", fun s -> Parser.FOLLOWING s);
8383+ ("UNBOUNDED", fun s -> Parser.UNBOUNDED s);
8484+ ("CURRENT", fun s -> Parser.CURRENT s);
8585+ ("BY", fun s -> Parser.BY s);
8686+ ("OFFSET", fun s -> Parser.OFFSET s);
8787+ ("RETURNING", fun s -> Parser.RETURNING s);
8888+ ("CONFLICT", fun s -> Parser.CONFLICT s);
8989+ ("DO", fun s -> Parser.DO s);
9090+ ("NOTHING", fun s -> Parser.NOTHING s);
9191+ ("CASE", fun s -> Parser.CASE s);
9292+ ("WHEN", fun s -> Parser.WHEN s);
9393+ ("THEN", fun s -> Parser.THEN s);
9494+ ("ELSE", fun s -> Parser.ELSE s);
9595+ ("END", fun s -> Parser.END s);
9696+ ("WITH", fun s -> Parser.WITH s);
9797+ ("RECURSIVE", fun s -> Parser.RECURSIVE s);
9898+ ("UNION", fun s -> Parser.UNION s);
9999+ ("ALL", fun s -> Parser.ALL s);
100100+ ("EXCEPT", fun s -> Parser.EXCEPT s);
101101+ ("INTERSECT", fun s -> Parser.INTERSECT s);
102102+ ("LIMIT", fun s -> Parser.LIMIT s);
103103+ ("AND", fun s -> Parser.AND s);
104104+ ("OR", fun s -> Parser.OR s);
105105+ ("IN", fun s -> Parser.IN s);
106106+ ("IS", fun s -> Parser.IS s);
107107+ ("ISNULL", fun s -> Parser.ISNULL s);
108108+ ("NOTNULL", fun s -> Parser.NOTNULL s);
109109+ ("MATERIALIZED", fun s -> Parser.MATERIALIZED s);
110110+ ("WITHOUT", fun s -> Parser.WITHOUT s);
111111+ ("LIKE", fun s -> Parser.LIKE s);
112112+ ("ESCAPE", fun s -> Parser.ESCAPE s);
113113+ ("GLOB", fun s -> Parser.GLOB s);
114114+ ("NATURAL", fun s -> Parser.NATURAL s);
115115+ ("RIGHT", fun s -> Parser.RIGHT s);
116116+ ("FULL", fun s -> Parser.FULL s);
117117+ ("USING", fun s -> Parser.USING s);
118118+ ("BETWEEN", fun s -> Parser.BETWEEN s);
119119+ ("CAST", fun s -> Parser.CAST s);
120120+ (* Write keywords *)
121121+ ("INSERT", fun s -> Parser.INSERT s);
122122+ ("INTO", fun s -> Parser.INTO s);
123123+ ("VALUES", fun s -> Parser.VALUES s);
124124+ ("REPLACE", fun s -> Parser.REPLACE s);
125125+ ("DELETE", fun s -> Parser.DELETE s);
126126+ ("UPDATE", fun s -> Parser.UPDATE s);
127127+ ("SET", fun s -> Parser.SET s);
128128+ ("INDEX", fun s -> Parser.INDEX s);
129129+ ("PRAGMA", fun s -> Parser.PRAGMA s);
130130+ ("BEGIN", fun s -> Parser.BEGIN s);
131131+ ("COMMIT", fun s -> Parser.COMMIT s);
132132+ ("ROLLBACK", fun s -> Parser.ROLLBACK s);
133133+ ("TRANSACTION", fun s -> Parser.TRANSACTION s);
134134+ ("VACUUM", fun s -> Parser.VACUUM s);
135135+ ("ANALYZE", fun s -> Parser.ANALYZE s);
136136+ ]
137137+138138+let classify s =
139139+ match List.assoc_opt (String.uppercase_ascii s) keywords with
140140+ | Some mk -> mk s
141141+ | None -> Parser.IDENT s
142142+}
143143+144144+let digit = ['0'-'9']
145145+let alpha = ['a'-'z' 'A'-'Z']
146146+let ident_start = alpha | '_'
147147+let ident_char = ident_start | digit
148148+let ident = ident_start ident_char*
149149+(* No leading sign: [-] / [+] are operators (unary or binary), so [x+1] and
150150+ [-2.0] lex as operator-then-number, not as a single signed literal. *)
151151+let number = digit+ ('.' digit+)? (['e' 'E'] ['+' '-']? digit+)?
152152+let hex_int = '0' ['x' 'X'] ['0'-'9' 'a'-'f' 'A'-'F']+
153153+let ws = [' ' '\t' '\n' '\r']+
154154+155155+rule token = parse
156156+ | ws { token lexbuf }
157157+ | "--" [^ '\n']* { token lexbuf }
158158+ | '(' { Parser.LPAREN }
159159+ | ')' { Parser.RPAREN }
160160+ | ',' { Parser.COMMA }
161161+ | ';' { Parser.SEMI }
162162+ | '.' { Parser.DOT }
163163+ | '*' { Parser.STAR }
164164+ | '+' { Parser.PLUS }
165165+ | '-' { Parser.MINUS }
166166+ | '/' { Parser.SLASH }
167167+ | '%' { Parser.PERCENT }
168168+ | '?' { Parser.QUESTION }
169169+ | "<<" { Parser.SHL }
170170+ | ">>" { Parser.SHR }
171171+ | '&' { Parser.BITAND }
172172+ | "<=" { Parser.LE }
173173+ | ">=" { Parser.GE }
174174+ | "<>" { Parser.NEQ }
175175+ | "!=" { Parser.NEQ }
176176+ | '<' { Parser.LT }
177177+ | '>' { Parser.GT }
178178+ | "==" { Parser.EQ }
179179+ | '=' { Parser.EQ }
180180+ | "||" { Parser.CONCAT }
181181+ | "->>" { Parser.ARROW2 }
182182+ | "->" { Parser.ARROW }
183183+ | '|' { Parser.BITOR }
184184+ (* A blob literal x'48...' / X'48...': hex digit pairs between single quotes,
185185+ adjacent to the leading x (a space would lex x as an identifier). *)
186186+ | ('x' | 'X') '\'' { Parser.BLOB (read_blob (Buffer.create 16) lexbuf) }
187187+ | hex_int as n { Parser.NUMBER n }
188188+ | number as n { Parser.NUMBER n }
189189+ | ident as s { classify s }
190190+ | '"'
191191+ { Parser.IDENT (read_until '"' "\"" (Buffer.create 32) lexbuf) }
192192+ | '['
193193+ { Parser.IDENT (read_until ']' "]" (Buffer.create 32) lexbuf) }
194194+ | '`'
195195+ { Parser.IDENT (read_until '`' "`" (Buffer.create 32) lexbuf) }
196196+ | '\''
197197+ { Parser.STRING (read_single_quoted (Buffer.create 32) lexbuf) }
198198+ | eof { Parser.EOF }
199199+ | _ as c { Parser.NUMBER (String.make 1 c) }
200200+201201+and read_until stop label buf = parse
202202+ | eof
203203+ { raise (Error ("unterminated " ^ label ^ "-quoted identifier")) }
204204+ | _ as c
205205+ {
206206+ if c = stop then Buffer.contents buf
207207+ else begin
208208+ Buffer.add_char buf c;
209209+ read_until stop label buf lexbuf
210210+ end
211211+ }
212212+213213+and read_single_quoted buf = parse
214214+ | "''"
215215+ {
216216+ Buffer.add_char buf '\'';
217217+ read_single_quoted buf lexbuf
218218+ }
219219+ | '\'' { Buffer.contents buf }
220220+ | eof { raise (Error "unterminated string literal") }
221221+ (* Consume a whole run of non-quote bytes in a single engine call rather than
222222+ one character at a time -- the bulk of lexer allocation on literal-heavy
223223+ scripts. *)
224224+ | [^ '\'' ]+ as s
225225+ {
226226+ Buffer.add_string buf s;
227227+ read_single_quoted buf lexbuf
228228+ }
229229+230230+(* Read the hex body of a blob literal up to the closing quote, decoding each
231231+ pair of hex digits into a byte. An odd digit count or a non-hex digit is a
232232+ malformed blob, as in sqlite3. *)
233233+and read_blob buf = parse
234234+ | '\''
235235+ {
236236+ let hex = Buffer.contents buf in
237237+ let n = String.length hex in
238238+ if n mod 2 <> 0 then raise (Error "odd number of digits in blob literal");
239239+ let b = Buffer.create (n / 2) in
240240+ for i = 0 to (n / 2) - 1 do
241241+ Buffer.add_char b
242242+ (Char.chr (int_of_string ("0x" ^ String.sub hex (2 * i) 2)))
243243+ done;
244244+ Buffer.contents b
245245+ }
246246+ | eof { raise (Error "unterminated blob literal") }
247247+ | (['0'-'9' 'a'-'f' 'A'-'F']) as c
248248+ {
249249+ Buffer.add_char buf c;
250250+ read_blob buf lexbuf
251251+ }
252252+ | _ { raise (Error "malformed hex in blob literal") }
253253+254254+{
255255+let parse sql =
256256+ let lexbuf = Lexing.from_string sql in
257257+ try Ok (Parser.create_table token lexbuf) with
258258+ | Parser.Error -> Error "parse error"
259259+ | Error msg -> Error msg
260260+261261+let parse_statements sql =
262262+ let lexbuf = Lexing.from_string sql in
263263+ try Ok (Parser.statements token lexbuf) with
264264+ | Parser.Error -> Error "parse error"
265265+ | Error msg -> Error msg
266266+267267+let split_statements sql =
268268+ (* Cut [sql] into the raw text of each top-level statement. The lexer reads a
269269+ quoted string as one token, so a [;] inside a literal is never a separator.
270270+ A CREATE TRIGGER body holds its own [;]-separated statements between
271271+ BEGIN and END, so while inside one a [;] is not a separator: track the
272272+ BEGIN/CASE..END nesting (CASE also closes with END) and only treat a [;] as
273273+ a separator once that nesting is back to zero. *)
274274+ let lexbuf = Lexing.from_string sql in
275275+ let rec go start acc ~in_trigger ~depth ~after_create =
276276+ let next = go start acc in
277277+ match token lexbuf with
278278+ | Parser.EOF ->
279279+ let piece = String.sub sql start (String.length sql - start) in
280280+ List.rev (piece :: acc)
281281+ | Parser.TRIGGER _ when after_create ->
282282+ next ~in_trigger:true ~depth ~after_create:false
283283+ | (Parser.BEGIN _ | Parser.CASE _) when in_trigger ->
284284+ next ~in_trigger ~depth:(depth + 1) ~after_create:false
285285+ | Parser.END _ when in_trigger ->
286286+ next ~in_trigger ~depth:(depth - 1) ~after_create:false
287287+ | Parser.SEMI when in_trigger && depth > 0 ->
288288+ next ~in_trigger ~depth ~after_create:false
289289+ | Parser.SEMI ->
290290+ let stop = Lexing.lexeme_start lexbuf in
291291+ let piece = String.sub sql start (stop - start) in
292292+ go (Lexing.lexeme_end lexbuf) (piece :: acc) ~in_trigger:false ~depth:0
293293+ ~after_create:false
294294+ | Parser.CREATE _ -> next ~in_trigger ~depth ~after_create:true
295295+ | _ -> next ~in_trigger ~depth ~after_create:false
296296+ in
297297+ go 0 [] ~in_trigger:false ~depth:0 ~after_create:false
298298+ |> List.map String.trim
299299+ |> List.filter (fun s -> s <> "")
300300+}
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2026 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+module Ir = Catalog.Ir
77+module V = Catalog.Value
88+99+let unsupported () = raise Ir.Unsupported
1010+1111+(* The correlated subqueries found while lowering a select, in id order. [expr]
1212+ registers each scalar / [EXISTS] / [IN (SELECT)] here as it emits the matching
1313+ [Ir.Subquery] node, and [query] reads the table back so the caller can wire a
1414+ runner (the engine has no SQL; it calls the injected runner by id). A
1515+ reset-per-[query] accumulator is safe: lowering does not recurse into a
1616+ subquery's body (it keeps the [Ast.select] for the runner), and lowering
1717+ finishes -- the [query] result captures this list -- before the engine runs
1818+ and fires any nested subquery lowering. [allow] is cleared while lowering an
1919+ aggregate select, where the grouped path has no runner, so a subquery there
2020+ falls back. *)
2121+type subkind = Scalar | Exists | In
2222+2323+let sub_acc : (subkind * Ast.select) list ref = ref []
2424+let sub_count = ref 0
2525+let sub_allow = ref true
2626+2727+let register_sub kind sub =
2828+ if not !sub_allow then unsupported ();
2929+ let id = !sub_count in
3030+ sub_acc := (kind, sub) :: !sub_acc;
3131+ incr sub_count;
3232+ id
3333+3434+(* Compare two values under a named collation -- the tree-walker's own collated
3535+ comparison, for an [ORDER BY] key that carries a [COLLATE]. *)
3636+let collate = Value_ops.compare_values_coll
3737+3838+(* The IR's value layer is the tree-walker's own functions, called directly: the
3939+ frontend and the engine share {!Catalog.Value.t}, so no value conversion sits
4040+ between them and a compiled query agrees with an interpreted one by sharing
4141+ the code. Only the operator enums differ, mapped by {!ast_cmp}/{!ast_logic}. *)
4242+let ast_cmp : Ir.binop -> Ast.binop = function
4343+ | Eq -> Eq
4444+ | Ne -> Neq
4545+ | Lt -> Lt
4646+ | Le -> Le
4747+ | Gt -> Gt
4848+ | Ge -> Ge
4949+ | _ -> assert false
5050+5151+let ast_logic : Ir.logicop -> Ast.binop = function
5252+ | And -> And
5353+ | Or -> Or
5454+ | Like -> Like
5555+ | Glob -> Glob
5656+ | Concat -> Concat
5757+ | Json_get -> Json_get
5858+ | Json_get_text -> Json_get_text
5959+6060+let builtins : Ir.builtins =
6161+ {
6262+ call = Func.apply;
6363+ compare =
6464+ (fun op coll a_col b_col va vb ->
6565+ Value_op.compare_with_affinity ?coll ~a_col ~b_col (ast_cmp op) va vb);
6666+ logic = (fun op va vb -> Value_op.binop_value (ast_logic op) va vb);
6767+ not_ = Value_ops.not_value;
6868+ is_eq = Value_ops.is_value;
6969+ in_list =
7070+ (fun coll e_col v cands -> Value_op.in_list_value ?coll ~e_col v cands);
7171+ cast = Value_ops.cast;
7272+ subquery = (fun _ _ _ -> unsupported ());
7373+ }
7474+7575+type layout = (string * string list) list
7676+7777+(* The comparison, opcode-free, and arithmetic operator groups of [Ast.binop]. *)
7878+let is_cmp : Ast.binop -> bool = function
7979+ | Eq | Neq | Lt | Le | Gt | Ge -> true
8080+ | _ -> false
8181+8282+let is_logic : Ast.binop -> bool = function
8383+ | And | Or | Like | Glob | Concat | Json_get | Json_get_text -> true
8484+ | _ -> false
8585+8686+let ir_cmp : Ast.binop -> Ir.binop = function
8787+ | Eq -> Eq
8888+ | Neq -> Ne
8989+ | Lt -> Lt
9090+ | Le -> Le
9191+ | Gt -> Gt
9292+ | Ge -> Ge
9393+ | _ -> unsupported ()
9494+9595+let ir_logic : Ast.binop -> Ir.logicop = function
9696+ | And -> And
9797+ | Or -> Or
9898+ | Like -> Like
9999+ | Glob -> Glob
100100+ | Concat -> Concat
101101+ | Json_get -> Json_get
102102+ | Json_get_text -> Json_get_text
103103+ | _ -> unsupported ()
104104+105105+let ir_arith : Ast.binop -> Ir.binop = function
106106+ | Add -> Add
107107+ | Sub -> Sub
108108+ | Mul -> Mul
109109+ | Div -> Div
110110+ | Mod -> Mod
111111+ | Bit_and -> Band
112112+ | Bit_or -> Bor
113113+ | Shl -> Shl
114114+ | Shr -> Shr
115115+ | _ -> unsupported ()
116116+117117+(* The position of the column named [name] in [cols] (case-insensitive). *)
118118+let col_index name cols =
119119+ let uname = String.uppercase_ascii name in
120120+ let rec go j = function
121121+ | c :: _ when String.uppercase_ascii c = uname -> Some j
122122+ | _ :: t -> go (j + 1) t
123123+ | [] -> None
124124+ in
125125+ go 0 cols
126126+127127+(* Resolve [alias.name] (or a bare [name]) to its [(cursor, column)] position: a
128128+ qualified name picks the source with that alias, a bare name takes the first
129129+ source that has it. *)
130130+let resolve_col (layout : layout) alias name =
131131+ match alias with
132132+ | Some a ->
133133+ let ua = String.uppercase_ascii a in
134134+ let rec go ci = function
135135+ | (al, cols) :: t ->
136136+ if String.uppercase_ascii al = ua then
137137+ match col_index name cols with
138138+ | Some j -> (ci, j)
139139+ | None -> unsupported ()
140140+ else go (ci + 1) t
141141+ | [] -> unsupported ()
142142+ in
143143+ go 0 layout
144144+ | None ->
145145+ let rec go ci = function
146146+ | (_, cols) :: t -> (
147147+ match col_index name cols with
148148+ | Some j -> (ci, j)
149149+ | None -> go (ci + 1) t)
150150+ | [] -> unsupported ()
151151+ in
152152+ go 0 layout
153153+154154+(* A comparison's collation -- sqlite3's left-operand rule, made explicit by
155155+ [Query.resolve_collations] as a [Collate] wrapper on an operand -- and the bare
156156+ operand under it. [is_col_ast] treats a [Collate] wrapper as not a column, so a
157157+ collated comparison carries no affinity on that side, matching the tree-walker. *)
158158+let is_col_ast = function Ast.Col _ -> true | _ -> false
159159+let collation_of = function Ast.Collate (_, c) -> Some c | _ -> None
160160+161161+let comparison_collation a b =
162162+ match collation_of a with Some _ as c -> c | None -> collation_of b
163163+164164+(* The collation an operand carries for membership/comparison, the default
165165+ [BINARY] (unary plus, which only strips affinity) carrying none. *)
166166+let nonbinary_collation e =
167167+ match collation_of e with Some c when c <> "BINARY" -> Some c | _ -> None
168168+169169+let strip_collate = function Ast.Collate (e, _) -> e | e -> e
170170+171171+let rec expr (layout : layout) (e : Ast.expr) : Ir.expr =
172172+ match e with
173173+ | Lit v -> Lit v
174174+ | Param i -> Param i
175175+ | Col (alias, name) ->
176176+ let c, j = resolve_col layout alias name in
177177+ Col (c, j)
178178+ | Unop_not e -> Not (expr layout e)
179179+ | Binary (op, a, b) ->
180180+ if is_cmp op then cmp_expr layout op a b
181181+ else if is_logic op then Logic (ir_logic op, expr layout a, expr layout b)
182182+ else Arith (ir_arith op, expr layout a, expr layout b)
183183+ | Is_null e -> Null_value (true, expr layout e)
184184+ | Is_not_null e -> Null_value (false, expr layout e)
185185+ | Is (a, b) -> Is (expr layout a, expr layout b)
186186+ | In_list (e, cands) ->
187187+ In
188188+ ( expr layout (strip_collate e),
189189+ nonbinary_collation e,
190190+ is_col_ast e,
191191+ List.map (expr layout) cands )
192192+ (* [iif(c, t, f)] short-circuits, so it is the searched [CASE WHEN c THEN t ELSE
193193+ f] -- lower it as one rather than an eager function call. An aggregate
194194+ reaching [expr] is in a WHERE/ON/ORDER BY/scan position (a select-list
195195+ aggregate makes the query aggregate, lowered elsewhere) -- always an error,
196196+ raised here so the engine reports it without deferring to the tree-walker. *)
197197+ | Func ("iif", [ c; t; f ]) -> expr layout (Case (None, [ (c, t) ], Some f))
198198+ | Func (name, args) when not (Query.is_agg name args) ->
199199+ Func (name, List.map (expr layout) args)
200200+ | Func (name, _) ->
201201+ Fmt.failwith
202202+ "%s() is an aggregate function and is only valid as a select item, not \
203203+ in WHERE, ON, or ORDER BY"
204204+ name
205205+ | Case (base, branches, els) ->
206206+ let branch (c, r) =
207207+ let p =
208208+ match base with
209209+ | None -> predicate layout c
210210+ | Some b -> Compare (Eq, expr layout b, expr layout c)
211211+ in
212212+ (p, expr layout r)
213213+ in
214214+ Case (List.map branch branches, Option.map (expr layout) els)
215215+ | Cast (e, ty) -> Cast (expr layout e, ty)
216216+ (* A correlated subquery becomes an opaque [Subquery] node: register the
217217+ [Ast.select] and its kind, and (for [IN]) lower the left operand whose value
218218+ the membership test reads. An uncorrelated one is already folded to a
219219+ constant before lowering ([Query.hoist_subqueries]), so what reaches here is
220220+ correlated, run per outer row by the injected runner. *)
221221+ | Scalar_select sub -> Subquery (register_sub Scalar sub, None)
222222+ | Exists sub -> Subquery (register_sub Exists sub, None)
223223+ | In_select (e, sub) -> Subquery (register_sub In sub, Some (expr layout e))
224224+ | Star | Qualified_star _ | Distinct_agg _ | Filter _ | Ordered_agg _
225225+ | Window _ | Collate _ ->
226226+ unsupported ()
227227+228228+(* Lower a [WHERE]/[ON]/[HAVING] expression to a filter predicate: an [AND]/[OR]
229229+ of comparisons, null tests, and bare truthy expressions. *)
230230+and predicate (layout : layout) (e : Ast.expr) : Ir.predicate =
231231+ match e with
232232+ | Binary (And, a, b) -> And (predicate layout a, predicate layout b)
233233+ | Binary (Or, a, b) -> Or (predicate layout a, predicate layout b)
234234+ | Binary (op, a, b) when is_cmp op -> cmp_pred layout op a b
235235+ | Is_null e -> Null_test (true, expr layout e)
236236+ | Is_not_null e -> Null_test (false, expr layout e)
237237+ (* a bare [WHERE expr] keeps the truthy rows; [expr] itself rejects a shape the
238238+ IR cannot model (a subquery, a window), falling the whole predicate back *)
239239+ | e -> Truthy (expr layout e)
240240+241241+(* Lower a comparison. A [COLLATE]-wrapped operand -- a declared collation, or a
242242+ [BINARY] one (which is unary plus, stripping affinity) -- routes through the
243243+ value-level {!Ir.Cmp_coll}: the binary VM compare cannot honour a non-binary
244244+ collation, and stripping the wrapper for the plain path would let the IR
245245+ re-derive affinity from the bare column the wrapper suppressed. The explicit
246246+ affinity flags read the original operands ([is_col_ast] of a wrapper is
247247+ [false]), matching the tree-walker; an unwrapped comparison takes the binary
248248+ value comparison / filter jump. *)
249249+and cmp_expr layout op a b : Ir.expr =
250250+ let a' = expr layout (strip_collate a)
251251+ and b' = expr layout (strip_collate b) in
252252+ match comparison_collation a b with
253253+ | Some c -> Cmp_coll (ir_cmp op, c, is_col_ast a, is_col_ast b, a', b')
254254+ | None -> Cmp (ir_cmp op, a', b')
255255+256256+and cmp_pred layout op a b : Ir.predicate =
257257+ let a' = expr layout (strip_collate a)
258258+ and b' = expr layout (strip_collate b) in
259259+ match comparison_collation a b with
260260+ | Some c ->
261261+ Truthy (Cmp_coll (ir_cmp op, c, is_col_ast a, is_col_ast b, a', b'))
262262+ | None -> Compare (ir_cmp op, a', b')
263263+264264+let ir_dir = function Ast.Asc -> Ir.Asc | Ast.Desc -> Ir.Desc
265265+266266+(* An [ORDER BY] key's explicit collation, if any -- a [BINARY] wrapper is the
267267+ default, so it carries no collation. *)
268268+let order_collation e =
269269+ match collation_of e with Some c when c <> "BINARY" -> Some c | _ -> None
270270+271271+(* Lower an [ORDER BY] key with [lower_key] (over the scan, or the synthetic
272272+ group row): its expression, direction, and collation. *)
273273+let order_key lower_key (e, d) =
274274+ (lower_key (strip_collate e), ir_dir d, order_collation e)
275275+276276+(* The per-output-column collations a DISTINCT dedups under (one per result
277277+ column; [[]] when not DISTINCT, so the engine dedups by binary value). *)
278278+let distinct_collations (s : Ast.select) =
279279+ if s.distinct then
280280+ List.map (fun (rc : Ast.result_col) -> order_collation rc.expr) s.cols
281281+ else []
282282+283283+(* Expand a select item into its projected expressions: [*] and [t.*] become one
284284+ column reference per source column. *)
285285+let project_item (layout : layout) (rc : Ast.result_col) : Ir.expr list =
286286+ match rc.expr with
287287+ | Star ->
288288+ List.concat
289289+ (List.mapi
290290+ (fun ci (_, cols) -> List.mapi (fun j _ -> Ir.Col (ci, j)) cols)
291291+ layout)
292292+ | Qualified_star q ->
293293+ let uq = String.uppercase_ascii q in
294294+ let rec go ci = function
295295+ | (al, cols) :: t ->
296296+ if String.uppercase_ascii al = uq then
297297+ List.mapi (fun j _ -> Ir.Col (ci, j)) cols
298298+ else go (ci + 1) t
299299+ | [] -> unsupported ()
300300+ in
301301+ go 0 layout
302302+ (* A projected [COLLATE] (or unary plus) produces the bare value -- the
303303+ collation governs only comparison/ordering/dedup, captured separately in the
304304+ query's ORDER BY keys and [distinct_colls]. *)
305305+ | e -> [ expr layout (strip_collate e) ]
306306+307307+(* The [int64] value of a literal / bound-parameter [LIMIT]/[OFFSET] expression,
308308+ or [None] for any other shape (a column, an expression) -- which falls back. *)
309309+let lit_int64 params (e : Ast.expr) =
310310+ match e with
311311+ | Ast.Lit (Ast.Int n) -> Some n
312312+ | Param i when i >= 0 && i < Array.length params -> (
313313+ match params.(i) with Ast.Int n -> Some n | _ -> None)
314314+ | _ -> None
315315+316316+(* A non-negative row count clamped to a native int: a value beyond the native
317317+ range exceeds any materialised row count, so [max_int] is exact (keep all for a
318318+ limit, skip all for an offset) without truncating to a wrong small number. *)
319319+let clamp_count n =
320320+ if Int64.compare n (Int64.of_int max_int) >= 0 then max_int
321321+ else Int64.to_int n
322322+323323+(* The [ON] of an inner join, folded into the filter; an outer/natural/using join
324324+ falls back. *)
325325+let join_on (j : Ast.join) =
326326+ if j.type_ <> Inner || j.natural || j.using <> [] then unsupported ();
327327+ j.on
328328+329329+(* The scan filter: the [WHERE] conjoined with every inner-join [ON]. *)
330330+let filter_of (layout : layout) (s : Ast.select) =
331331+ let fexprs =
332332+ (match s.where with Some w -> [ w ] | None -> [])
333333+ @ List.map join_on s.joins
334334+ in
335335+ match fexprs with
336336+ | [] -> Ir.Always
337337+ | e :: rest ->
338338+ List.fold_left
339339+ (fun acc e -> Ir.And (acc, predicate layout e))
340340+ (predicate layout e) rest
341341+342342+(* A negative LIMIT is unbounded ([None]); a huge one clamps to [max_int] (every
343343+ row kept); a non-literal one falls back (sqlite3). *)
344344+let limit_of params (s : Ast.select) =
345345+ match s.limit with
346346+ | None -> None
347347+ | Some e -> (
348348+ match lit_int64 params e with
349349+ | None -> unsupported ()
350350+ | Some n when n < 0L -> None
351351+ | Some n -> Some (clamp_count n))
352352+353353+(* A negative OFFSET is zero; a huge one clamps to [max_int] (every row skipped);
354354+ a non-literal one falls back (sqlite3). *)
355355+let offset_of params (s : Ast.select) =
356356+ match s.offset with
357357+ | None -> 0
358358+ | Some e -> (
359359+ match lit_int64 params e with
360360+ | None -> unsupported ()
361361+ | Some n when n < 0L -> 0
362362+ | Some n -> clamp_count n)
363363+364364+(* A single trailing [LEFT JOIN] (one [FROM] table, one left-joined table, no
365365+ [NATURAL]/[USING]): its [ON] predicate, the [WHERE] kept separate from it so
366366+ the engine can null-fill. Any other join shape returns [None], so the inner
367367+ path folds every [ON] into one filter. *)
368368+let left_join (layout : layout) (s : Ast.select) =
369369+ match s.joins with
370370+ | [ j ] when j.type_ = Left && (not j.natural) && j.using = [] ->
371371+ let where =
372372+ match s.where with Some w -> predicate layout w | None -> Ir.Always
373373+ in
374374+ Some (predicate layout j.on, where)
375375+ | _ -> None
376376+377377+let query ?(params = [||]) (layout : layout) (s : Ast.select) :
378378+ Ir.query * (subkind * Ast.select) list =
379379+ if s.group_by <> [] || s.having <> None || s.compound <> [] then
380380+ unsupported ();
381381+ sub_acc := [];
382382+ sub_count := 0;
383383+ sub_allow := true;
384384+ let arities = List.map (fun (_, cols) -> List.length cols) layout in
385385+ let left, filter =
386386+ match left_join layout s with
387387+ | Some (on, where) -> (Some on, where)
388388+ | None -> (None, filter_of layout s)
389389+ in
390390+ let project = List.concat_map (project_item layout) s.cols in
391391+ let select = { Ir.arities; left; filter; project } in
392392+ let order_by = List.map (order_key (expr layout)) s.order_by in
393393+ let q =
394394+ {
395395+ Ir.select;
396396+ order_by;
397397+ distinct = s.distinct;
398398+ distinct_colls = distinct_collations s;
399399+ limit = limit_of params s;
400400+ offset = offset_of params s;
401401+ }
402402+ in
403403+ (q, List.rev !sub_acc)
404404+405405+(* The aggregates {!agg_fold} folds: a star [count], the numeric aggregates, and
406406+ the string aggregate (group_concat / string_agg). A FILTER / ORDER-BY
407407+ aggregate and a window are not here, so they fall back to the tree-walker. *)
408408+let numeric_aggs = [ "count"; "sum"; "min"; "max"; "avg"; "total" ]
409409+let string_aggs = [ "group_concat"; "string_agg" ]
410410+411411+(* A plain aggregate call: a star [count], a numeric aggregate of an argument, or
412412+ a string aggregate ([group_concat]/[string_agg], with an optional separator
413413+ argument), optionally [DISTINCT] -- its name, distinctness, and argument
414414+ expressions ([] for a star count). [None] for anything else. *)
415415+let core_aggregate : Ast.expr -> (string * bool * Ast.expr list) option =
416416+ function
417417+ | Func ("count", [ Star ]) -> Some ("count", false, [])
418418+ | Func (name, args)
419419+ when (List.mem name numeric_aggs || List.mem name string_aggs)
420420+ && Query.is_agg name args ->
421421+ Some (name, false, args)
422422+ | Distinct_agg (name, arg)
423423+ when List.mem name numeric_aggs || List.mem name string_aggs ->
424424+ Some (name, true, [ arg ])
425425+ | _ -> None
426426+427427+(* A plain aggregate call, peeling any [FILTER (WHERE p)] and [ORDER BY] around
428428+ the core call: its name, distinctness, arguments, optional FILTER predicate,
429429+ and ORDER BY keys. [None] for anything that is not an aggregate. *)
430430+let as_aggregate e =
431431+ let rec peel filter order = function
432432+ | Ast.Filter (inner, p) -> peel (Some p) order inner
433433+ | Ast.Ordered_agg (inner, ob) -> peel filter ob inner
434434+ | core -> (
435435+ match core_aggregate core with
436436+ | Some (name, distinct, args) ->
437437+ Some (name, distinct, args, filter, order)
438438+ | None -> None)
439439+ in
440440+ peel None [] e
441441+442442+(* The value-level aggregate fold injected into {!Catalog.Ir.run_grouped}: one
443443+ {!Catalog.Value.t} list per group row (empty for a star [count]; each
444444+ argument's value otherwise), already deduped under [DISTINCT]. [count] counts
445445+ rows (star) or non-NULL first arguments; [group_concat]/[string_agg] join the
446446+ non-NULL first arguments by the separator (the second argument, default ",");
447447+ the rest fold the non-NULL first arguments through {!Value_ops.numeric_agg} -- the
448448+ same folds the tree-walker uses. *)
449449+let agg_fold name _distinct (rows : V.t list list) : V.t =
450450+ let first_args =
451451+ List.filter_map
452452+ (function v :: _ when v <> V.Null -> Some v | _ -> None)
453453+ rows
454454+ in
455455+ match rows with
456456+ | r :: _ when r = [] -> Int (Int64.of_int (List.length rows))
457457+ | _ when List.mem name string_aggs -> (
458458+ match first_args with
459459+ | [] -> Null
460460+ | vs ->
461461+ let sep =
462462+ match rows with
463463+ | (_ :: s :: _) :: _ -> Value_ops.text_of_value s
464464+ | _ -> ","
465465+ in
466466+ Text (String.concat sep (List.map Value_ops.text_of_value vs)))
467467+ | _ when name = "count" -> Int (Int64.of_int (List.length first_args))
468468+ | _ -> Value_ops.numeric_agg name first_args
469469+470470+(* Map an output expression over the synthetic per-group row, whose columns are
471471+ the group keys ([0..nkeys-1]) then the aggregate results. A group key or an
472472+ aggregate becomes a column reference; a literal or parameter stays itself; a
473473+ compound expression (arithmetic, a comparison, a scalar function, CAST)
474474+ recurses on its operands, so an aggregate expression like [count() + 1] or a
475475+ scalar function of a key is computed over the group row. Anything else (a bare
476476+ non-key column, a window) falls back. [collect] registers an aggregate and
477477+ returns its slot. *)
478478+let rec out_operand ~nkeys ~ncols ~key_index ~collect ~resolve_bare
479479+ (e : Ast.expr) : Ir.expr =
480480+ let recur = out_operand ~nkeys ~ncols ~key_index ~collect ~resolve_bare in
481481+ match key_index e with
482482+ | Some i -> Col (0, i)
483483+ | None -> (
484484+ match as_aggregate e with
485485+ (* the synthetic row is [keys, representative columns, aggregate results],
486486+ so an aggregate sits past the [ncols] representative columns *)
487487+ | Some info -> Col (0, nkeys + ncols + collect e info)
488488+ | None -> (
489489+ match e with
490490+ | Lit v -> Lit v
491491+ | Param i -> Param i
492492+ | Unop_not e -> Not (recur e)
493493+ | Binary (op, a, b) ->
494494+ if is_cmp op then Cmp (ir_cmp op, recur a, recur b)
495495+ else if is_logic op then Logic (ir_logic op, recur a, recur b)
496496+ else Arith (ir_arith op, recur a, recur b)
497497+ | Is_null e -> Null_value (true, recur e)
498498+ | Is_not_null e -> Null_value (false, recur e)
499499+ | Is (a, b) -> Is (recur a, recur b)
500500+ | In_list (e, cands) ->
501501+ In
502502+ ( recur (strip_collate e),
503503+ nonbinary_collation e,
504504+ is_col_ast e,
505505+ List.map recur cands )
506506+ | Func (name, args) when name <> "iif" && not (Query.is_agg name args)
507507+ ->
508508+ Func (name, List.map recur args)
509509+ | Cast (e, ty) -> Cast (recur e, ty)
510510+ (* a bare (non-key) column reads from the group's representative row *)
511511+ | Col _ -> resolve_bare e
512512+ | _ -> unsupported ()))
513513+514514+let rec out_pred ~op_of (e : Ast.expr) : Ir.predicate =
515515+ match e with
516516+ | Binary (And, a, b) -> And (out_pred ~op_of a, out_pred ~op_of b)
517517+ | Binary (Or, a, b) -> Or (out_pred ~op_of a, out_pred ~op_of b)
518518+ | Binary (op, a, b) when is_cmp op -> Compare (ir_cmp op, op_of a, op_of b)
519519+ | Is_null e -> Null_test (true, op_of e)
520520+ | Is_not_null e -> Null_test (false, op_of e)
521521+ | _ -> unsupported ()
522522+523523+(* The position of [e] among the group keys (by structural expression, ignoring a
524524+ [COLLATE] wrapper on either side: [GROUP BY a COLLATE NOCASE] groups by [a], so
525525+ a projected bare [a] is that key). *)
526526+let key_index keys e =
527527+ let e = strip_collate e in
528528+ let rec go i = function
529529+ | k :: _ when strip_collate k = e -> Some i
530530+ | _ :: t -> go (i + 1) t
531531+ | [] -> None
532532+ in
533533+ go 0 keys
534534+535535+(* Whether [s]'s output (select list, HAVING, ORDER BY) reads a bare (non-key,
536536+ non-aggregate) column or a [*] -- the cases that need a representative group
537537+ row materialised. A column matching a group key (ignoring a [COLLATE] wrapper)
538538+ or sitting inside an aggregate does not. *)
539539+let references_bare_or_star keys (s : Ast.select) =
540540+ let is_key e = key_index keys e <> None in
541541+ let rec bare (e : Ast.expr) =
542542+ if is_key e then false
543543+ else if as_aggregate e <> None then false
544544+ else
545545+ match e with
546546+ | Star | Qualified_star _ | Col _ -> true
547547+ | Lit _ | Param _ -> false
548548+ | Unop_not e
549549+ | Is_null e
550550+ | Is_not_null e
551551+ | Cast (e, _)
552552+ | Collate (e, _)
553553+ | Distinct_agg (_, e) ->
554554+ bare e
555555+ | Binary (_, a, b) | Is (a, b) | Filter (a, b) -> bare a || bare b
556556+ | In_list (e, l) -> bare e || List.exists bare l
557557+ | Func (_, args) -> List.exists bare args
558558+ | Ordered_agg (a, ob) -> bare a || List.exists (fun (e, _) -> bare e) ob
559559+ | Case (base, brs, els) ->
560560+ Option.fold ~none:false ~some:bare base
561561+ || List.exists (fun (c, x) -> bare c || bare x) brs
562562+ || Option.fold ~none:false ~some:bare els
563563+ | Window _ | In_select _ | Scalar_select _ | Exists _ -> false
564564+ in
565565+ List.exists (fun (rc : Ast.result_col) -> bare rc.expr) s.cols
566566+ || (match s.having with Some h -> bare h | None -> false)
567567+ || List.exists (fun (e, _) -> bare e) s.order_by
568568+569569+let column_bases arities =
570570+ let a = Array.of_list arities in
571571+ let b = Array.make (Array.length a) 0 in
572572+ for c = 1 to Array.length a - 1 do
573573+ b.(c) <- b.(c - 1) + a.(c - 1)
574574+ done;
575575+ b
576576+577577+let representative_shape layout keys s =
578578+ let arities = List.map (fun (_, cols) -> List.length cols) layout in
579579+ let bases = column_bases arities in
580580+ let needs_rep = references_bare_or_star keys s in
581581+ let ncols = if needs_rep then List.fold_left ( + ) 0 arities else 0 in
582582+ (arities, bases, needs_rep, ncols)
583583+584584+let representative_choice layout needs_rep (s : Ast.select) =
585585+ if not needs_rep then None
586586+ else
587587+ List.find_map
588588+ (fun (rc : Ast.result_col) ->
589589+ match rc.expr with
590590+ | Func ("min", [ a ]) -> Some (false, expr layout a)
591591+ | Func ("max", [ a ]) -> Some (true, expr layout a)
592592+ | _ -> None)
593593+ s.cols
594594+595595+let bare_resolver layout ~nkeys ~bases = function
596596+ | Ast.Col (alias, name) ->
597597+ let c, j = resolve_col layout alias name in
598598+ Ir.Col (0, nkeys + bases.(c) + j)
599599+ | _ -> unsupported ()
600600+601601+let aggregate_collector layout =
602602+ let aggs = ref [] and nagg = ref 0 in
603603+ let collect e info =
604604+ let rec go i = function
605605+ | (e', _) :: _ when e' = e -> Some i
606606+ | _ :: t -> go (i + 1) t
607607+ | [] -> None
608608+ in
609609+ match go 0 (List.rev !aggs) with
610610+ | Some i -> i
611611+ | None ->
612612+ let name, distinct, args, filter, order = info in
613613+ let agg =
614614+ {
615615+ Ir.name;
616616+ distinct;
617617+ args = List.map (expr layout) args;
618618+ filter = Option.map (expr layout) filter;
619619+ order = List.map (fun (e, d) -> (expr layout e, ir_dir d)) order;
620620+ }
621621+ in
622622+ aggs := (e, agg) :: !aggs;
623623+ let slot = !nagg in
624624+ incr nagg;
625625+ slot
626626+ in
627627+ (collect, (fun () -> !nagg), fun () -> List.rev_map snd !aggs)
628628+629629+let aggregate_star_cols layout ~nkeys ~bases qopt =
630630+ List.concat
631631+ (List.mapi
632632+ (fun c (al, cols) ->
633633+ match qopt with
634634+ | Some q when String.uppercase_ascii q <> String.uppercase_ascii al ->
635635+ []
636636+ | _ -> List.mapi (fun j _ -> Ir.Col (0, nkeys + bases.(c) + j)) cols)
637637+ layout)
638638+639639+let aggregate_project layout ~nkeys ~bases op (s : Ast.select) =
640640+ let star_cols = aggregate_star_cols layout ~nkeys ~bases in
641641+ List.concat_map
642642+ (fun (rc : Ast.result_col) ->
643643+ match rc.expr with
644644+ | Star -> star_cols None
645645+ | Qualified_star q ->
646646+ if
647647+ List.exists
648648+ (fun (al, _) ->
649649+ String.uppercase_ascii al = String.uppercase_ascii q)
650650+ layout
651651+ then star_cols (Some q)
652652+ else unsupported ()
653653+ | e -> [ op e ])
654654+ s.cols
655655+656656+let aggregate_output ?(params = [||]) ~nkeys ~ncols ~nagg op project
657657+ (s : Ast.select) =
658658+ (* HAVING and ORDER BY register their aggregates through [op] too, so [nagg]
659659+ (the live count) is read only after both are lowered -- an aggregate that
660660+ appears solely in HAVING / ORDER BY still gets a synthetic-row slot. *)
661661+ let filter =
662662+ match s.having with Some h -> out_pred ~op_of:op h | None -> Always
663663+ in
664664+ let order_by = List.map (order_key op) s.order_by in
665665+ let arities = [ nkeys + ncols + nagg () ] in
666666+ {
667667+ Ir.select = { arities; left = None; filter; project };
668668+ order_by;
669669+ distinct = s.distinct;
670670+ distinct_colls = distinct_collations s;
671671+ limit = limit_of params s;
672672+ offset = offset_of params s;
673673+ }
674674+675675+let aggregate_scan layout keys (s : Ast.select) =
676676+ let left, filter =
677677+ match left_join layout s with
678678+ | Some (on, where) -> (Some on, where)
679679+ | None -> (None, filter_of layout s)
680680+ in
681681+ (left, filter, List.map (fun e -> expr layout (strip_collate e)) keys)
682682+683683+let aggregate_query ?(params = [||]) (layout : layout) (s : Ast.select) :
684684+ Ir.grouped * (subkind * Ast.select) list =
685685+ if s.compound <> [] then unsupported ();
686686+ (* a correlated scalar / EXISTS / IN subquery in the scan filter (WHERE / ON)
687687+ registers here, for the caller to wire a runner -- the per-source-row scope
688688+ the engine passes matches what the grouped scan sees *)
689689+ sub_acc := [];
690690+ sub_count := 0;
691691+ sub_allow := true;
692692+ let keys = s.group_by in
693693+ let nkeys = List.length keys in
694694+ let key_index = key_index keys in
695695+ let arities, bases, needs_rep, ncols = representative_shape layout keys s in
696696+ (* sqlite3's companion rule: a bare column takes its value from the row holding
697697+ a lone min/max in the select list (else an arbitrary, here the first, row) *)
698698+ let rep = representative_choice layout needs_rep s in
699699+ let resolve_bare = bare_resolver layout ~nkeys ~bases in
700700+ (* aggregates in first-seen order, structurally deduped so a repeated aggregate
701701+ shares one slot *)
702702+ let collect, nagg, aggs = aggregate_collector layout in
703703+ let op = out_operand ~nkeys ~ncols ~key_index ~collect ~resolve_bare in
704704+ (* [*] (or [t.*]) expands to the representative row's scanned columns, of the
705705+ matching cursor for a qualified star; an unmatched qualified star falls back
706706+ so the tree-walker reports "no such table". *)
707707+ (* project / HAVING / ORDER BY all register their aggregates through [op] *)
708708+ let project = aggregate_project layout ~nkeys ~bases op s in
709709+ let output = aggregate_output ~params ~nkeys ~ncols ~nagg op project s in
710710+ (* A single trailing LEFT JOIN null-fills before grouping; its [ON] is the
711711+ [left] predicate, the [WHERE] the scan filter. Otherwise every [ON] folds
712712+ into the filter, as for an inner join. *)
713713+ let left, filter, group_by = aggregate_scan layout keys s in
714714+ let g =
715715+ {
716716+ Ir.arities;
717717+ left;
718718+ filter;
719719+ group_by;
720720+ group_colls = List.map order_collation keys;
721721+ ncols;
722722+ rep;
723723+ aggs = aggs ();
724724+ output;
725725+ }
726726+ in
727727+ (g, List.rev !sub_acc)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2026 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Lower a resolved SQL {!Ast} to the backend-agnostic {!Catalog.Ir}.
77+88+ This is the SQL frontend's half of the engine migration: it resolves every
99+ column reference to a positional [(cursor, column)] index against the
1010+ scanned sources, turns every literal into a {!Catalog.Value.t}, and maps the
1111+ AST's operators onto the IR's, so {!Catalog.Ir} can compile and run the
1212+ query over any backend cursor. A node the IR does not model raises
1313+ {!Catalog.Ir.Unsupported}, so the caller falls back to the tree-walker. *)
1414+1515+val collate : string -> Catalog.Value.t -> Catalog.Value.t -> int
1616+(** [collate name a b] compares two values under the named collation -- the
1717+ tree-walker's own collated comparison, to inject into {!Catalog.Ir.run} for
1818+ a collated [ORDER BY] key. *)
1919+2020+val builtins : Catalog.Ir.builtins
2121+(** The IR's injected value layer, wired directly to the shared value semantics
2222+ ({!Query.func}, {!Query.compare_with_affinity}, {!Query.binop_value},
2323+ {!Value_ops.not_value}, {!Value_ops.is_value}, {!Query.in_list_value}), so a
2424+ compiled query and an interpreted one agree on a shared {!Catalog.Value.t}.
2525+*)
2626+2727+type layout = (string * string list) list
2828+(** The scanned sources in nested-loop order: each [(alias, column names)], the
2929+ [FROM] table then every inner-joined table. A column resolves to the cursor
3030+ index of its source and the position of its name within that source. *)
3131+3232+val agg_fold : string -> bool -> Catalog.Value.t list list -> Catalog.Value.t
3333+(** The value-level aggregate fold injected into {!Catalog.Ir.run_grouped}:
3434+ [count] counts rows (a star [count], whose tuples are empty) or non-NULL
3535+ arguments, and [sum]/[min]/[max]/[avg]/[total] fold the non-NULL arguments
3636+ through the shared {!Value_ops.numeric_agg}. *)
3737+3838+type subkind =
3939+ | Scalar
4040+ | Exists
4141+ | In
4242+ (** The kind of a registered correlated subquery: a scalar value, an
4343+ [EXISTS] test, or the [IN (SELECT)] membership whose left operand the
4444+ [Ir.Subquery] node carries. *)
4545+4646+val query :
4747+ ?params:Ast.value array ->
4848+ layout ->
4949+ Ast.select ->
5050+ Catalog.Ir.query * (subkind * Ast.select) list
5151+(** [query layout s] lowers a plain (non-aggregate, non-window, non-compound)
5252+ select to a {!Catalog.Ir.query} and the correlated subqueries it references,
5353+ in id order (the {!Catalog.Ir.constructor-Subquery} ids index this list):
5454+ the scan over [layout]'s cursors, the [WHERE] filter, the projection
5555+ (expanding [*] and [t.*]), [ORDER BY], [DISTINCT], and a literal/parameter
5656+ [LIMIT]/[OFFSET]. [params] resolves a bound [LIMIT ?]/[OFFSET ?]. The caller
5757+ wires a runner for each subquery into {!Catalog.Ir.field-subquery}. Raises
5858+ {!Catalog.Ir.Unsupported} for a select outside the subset (grouping, a
5959+ window, a compound arm, a non-literal limit). *)
6060+6161+val aggregate_query :
6262+ ?params:Ast.value array ->
6363+ layout ->
6464+ Ast.select ->
6565+ Catalog.Ir.grouped * (subkind * Ast.select) list
6666+(** [aggregate_query layout s] lowers an aggregate / [GROUP BY] select to a
6767+ {!Catalog.Ir.grouped} and the correlated subqueries its scan filter ([WHERE]
6868+ / [ON]) references, in id order (the caller wires a runner for each): the
6969+ group keys, the aggregates (each select / [HAVING] / [ORDER BY] item that is
7070+ a plain aggregate call), a representative-row section for any bare column or
7171+ [*], and an output query over the synthetic per-group row (the keys, the
7272+ representative columns, then the aggregate results). Raises
7373+ {!Catalog.Ir.Unsupported} for a shape outside the subset: a select item that
7474+ is neither a group key, a bare column, a plain aggregate, nor a literal (an
7575+ expression mixing keys and aggregates), a window, or a compound arm. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+55+ Menhir grammar for the SQLite SQL subset.
66+77+ CREATE TABLE column bodies are collected as token lists and classified in
88+ Ast.classify_column — the grammar handles structure (parens, commas,
99+ table-vs-column dispatch), OCaml handles the type/constraint semantics.
1010+ Queries (SELECT) get a real expression grammar with precedence.
1111+ ---------------------------------------------------------------------------*)
1212+1313+%{ open Ast
1414+1515+let number_to_value s =
1616+ match Int64.of_string_opt s with
1717+ | Some i -> Int i
1818+ | None -> (
1919+ match float_of_string_opt s with Some f -> Float f | None -> Text s)
2020+2121+(* Wrap an aggregate call [base] in its optional FILTER then OVER decorations. *)
2222+let decorate base filter window =
2323+ let base = match filter with Some p -> Filter (base, p) | None -> base in
2424+ match window with Some w -> Window (base, w) | None -> base
2525+2626+(* [e d NULLS FIRST|LAST]: NULLS is a soft keyword (a plain identifier elsewhere),
2727+ matched contextually here. The default null ordering already matches sqlite3
2828+ (NULL sorts lowest, so ASC is nulls-first and DESC nulls-last); an explicit
2929+ clause overrides it with a leading [e IS NULL] key. *)
3030+let nulls_order e d kw pos =
3131+ if String.uppercase_ascii kw <> "NULLS" then
3232+ failwith "syntax error: expected NULLS in ORDER BY term";
3333+ match String.uppercase_ascii pos with
3434+ | "FIRST" -> [ (Is_null e, Desc); (e, d) ]
3535+ | "LAST" -> [ (Is_null e, Asc); (e, d) ]
3636+ | _ -> failwith "syntax error: expected NULLS FIRST or NULLS LAST"
3737+3838+(* [a IS b] / [a IS NOT b]: NULL-safe (not-)distinct. A NULL right operand folds
3939+ to the dedicated Is_null/Is_not_null nodes; otherwise it is the general
4040+ NULL-safe equality node (IS NOT is its boolean complement). *)
4141+let mk_is a = function Lit Null -> Is_null a | b -> Is (a, b)
4242+let mk_is_not a = function
4343+ | Lit Null -> Is_not_null a
4444+ | b -> Unop_not (Is (a, b))
4545+4646+(* [a IS TRUE]/[IS FALSE] (and their NOT forms) test truthiness, not equality:
4747+ sqlite3 defines [a IS TRUE] as [a<>0] and [a IS FALSE] as [a=0], each mapping
4848+ a NULL [a] to 0 (false). Desugar to a CASE so [a] is evaluated once and the
4949+ result is always 0/1, never NULL. *)
5050+(* [x IN (list)]: a list whose sole element is a parenthesised subquery (so the
5151+ source wrote [x IN ((SELECT ...))], any depth of extra parens collapsing to a
5252+ scalar subquery) is the subquery membership test [x IN (SELECT ...)], matching
5353+ every result row -- not a one-element value list. *)
5454+let mk_in e = function [ Scalar_select s ] -> In_select (e, s) | l -> In_list (e, l)
5555+5656+let zero = Lit (Int 0L)
5757+let one = Lit (Int 1L)
5858+let is_true a = Case (None, [ (Binary (Neq, a, zero), one) ], Some zero)
5959+let is_false a = Case (None, [ (Binary (Eq, a, zero), one) ], Some zero)
6060+let is_not_true a = Case (None, [ (Binary (Neq, a, zero), zero) ], Some one)
6161+let is_not_false a = Case (None, [ (Binary (Eq, a, zero), zero) ], Some one)
6262+6363+(* Build a [create_table] from a parsed column/constraint list (shared by the
6464+ standalone CREATE TABLE parser and the statement form). *)
6565+let make_create_table ?(without_rowid = false) name if_not_exists defs =
6666+ let mk_fk cols (parent, pcols, od, ou) =
6767+ { cols = cols; parent = parent; parent_cols = pcols;
6868+ on_delete = od; on_update = ou }
6969+ in
7070+ let columns, tcs, checks, defaults, gens, fks =
7171+ List.fold_left (fun (cs, ts, ks, ds, gs, fs) -> function
7272+ | `Col (cname, (toks, col_checks, def, gen)) ->
7373+ let ds = match def with Some e -> (cname, e) :: ds | None -> ds in
7474+ let gs = match gen with Some e -> (cname, e) :: gs | None -> gs in
7575+ let fs =
7676+ match column_foreign_key cname toks with
7777+ | Some fk -> fk :: fs
7878+ | None -> fs
7979+ in
8080+ (classify_column cname toks :: cs, ts,
8181+ List.rev_append col_checks ks, ds, gs, fs)
8282+ | `Tbl t -> (cs, t :: ts, ks, ds, gs, fs)
8383+ | `Check k -> (cs, ts, k :: ks, ds, gs, fs)
8484+ | `Fk (cols, r) -> (cs, ts, ks, ds, gs, mk_fk cols r :: fs))
8585+ ([], [], [], [], [], []) defs
8686+ in
8787+ { tbl_name = name;
8888+ ct_if_not_exists = if_not_exists;
8989+ columns = List.rev columns;
9090+ table_constraints = List.rev tcs;
9191+ checks = List.rev checks;
9292+ defaults = List.rev defaults;
9393+ generated = List.rev gens;
9494+ foreign_keys = List.rev fks;
9595+ ct_without_rowid = without_rowid }
9696+%}
9797+9898+(* Payload tokens *)
9999+%token <string> IDENT
100100+%token <string> NUMBER
101101+%token <string> STRING
102102+103103+(* SQL keywords — all carry original text for case preservation *)
104104+%token <string> CREATE TABLE ALTER ADD COLUMN IF NOT EXISTS DROP VIEW RENAME
105105+%token <string> TRIGGER BEFORE AFTER INSTEAD FOR EACH ROW OF
106106+%token <string> CASCADE RESTRICT NO ACTION CROSS SAVEPOINT RELEASE TO
107107+%token <string> PRIMARY KEY UNIQUE NULL DEFAULT CHECK REFERENCES TRUE FALSE
108108+%token <string> COLLATE GENERATED ALWAYS AS AUTOINCREMENT
109109+%token <string> FOREIGN CONSTRAINT ON ASC DESC
110110+%token <string> SELECT DISTINCT FROM WHERE JOIN INNER LEFT OUTER ORDER GROUP BY LIMIT
111111+%token <string> NATURAL RIGHT FULL USING
112112+%token <string> HAVING OFFSET CASE WHEN THEN ELSE END RETURNING FILTER
113113+%token <string> OVER PARTITION ROWS RANGE PRECEDING FOLLOWING UNBOUNDED CURRENT
114114+%token <string> CONFLICT DO NOTHING
115115+%token <string> WITH RECURSIVE UNION ALL EXCEPT INTERSECT
116116+%token <string> AND OR IN IS LIKE GLOB BETWEEN CAST ESCAPE ISNULL NOTNULL
117117+%token <string> MATERIALIZED WITHOUT
118118+%token <string> INSERT INTO VALUES REPLACE DELETE UPDATE SET INDEX PRAGMA
119119+%token <string> BEGIN COMMIT ROLLBACK TRANSACTION VACUUM ANALYZE
120120+121121+(* Delimiters and operators *)
122122+%token LPAREN RPAREN COMMA SEMI DOT STAR QUESTION PLUS MINUS SLASH PERCENT
123123+%token EQ NEQ LT LE GT GE EOF CONCAT
124124+%token BITAND BITOR SHL SHR ARROW ARROW2
125125+%token <string> BLOB
126126+(* Phantom token: precedence marker for an absent join constraint (never lexed). *)
127127+%token NO_CONSTRAINT
128128+129129+%start <Ast.create_table> create_table
130130+%start <Ast.stmt list> statements
131131+132132+(* Precedence: lowest first. NOT binds tighter than AND/OR but looser than the
133133+ comparison operators; bitwise (& | << >>) binds tighter than comparison,
134134+ additive tighter still, then multiplicative, matching SQLite. *)
135135+(* A bare join with no [ON]/[USING] reduces at the lowest precedence, so a
136136+ following [ON] is shifted into the join constraint rather than ending the
137137+ join (which would mis-bind [INSERT ... SELECT ... JOIN b ON CONFLICT]). *)
138138+%nonassoc NO_CONSTRAINT
139139+%nonassoc ON
140140+%left OR
141141+%left AND
142142+%right NOT
143143+%nonassoc EQ NEQ LT LE GT GE LIKE GLOB IN IS BETWEEN ISNULL NOTNULL
144144+(* ESCAPE binds tighter than LIKE so [a LIKE b ESCAPE c] shifts the ESCAPE
145145+ rather than reducing [a LIKE b] first. *)
146146+%nonassoc ESCAPE
147147+%left BITAND BITOR SHL SHR
148148+%left PLUS MINUS
149149+%left STAR SLASH PERCENT
150150+%left CONCAT ARROW ARROW2
151151+%right UMINUS
152152+(* COLLATE is the highest-precedence operator (datatype3.html), binding tighter
153153+ than any arithmetic or comparison operator. *)
154154+%left COLLATE
155155+156156+%%
157157+158158+(* ── Statements ──────────────────────────────────────────────── *)
159159+160160+statements:
161161+ | EOF { [] }
162162+ | s = statement; t = stmt_tail { s :: t }
163163+ | SEMI; t = statements { t }
164164+165165+stmt_tail:
166166+ | EOF { [] }
167167+ | SEMI; t = statements { t }
168168+169169+statement:
170170+ | s = select_stmt { Select s }
171171+ | i = insert_stmt { Insert i }
172172+ (* [WITH ctes INSERT ... SELECT ...]: the CTEs are in scope for the inserted
173173+ query, so inject them into its embedded SELECT (a recursive CTE feeding an
174174+ INSERT is the common shape). *)
175175+ | l = with_clause; i = insert_stmt
176176+ { Insert (match i.source with
177177+ | Ast.Select s -> { i with source = Ast.Select { s with ctes = l } }
178178+ | _ -> i) }
179179+ | d = delete_stmt { Delete d }
180180+ | u = update_stmt { Update u }
181181+ (* [WITH ctes UPDATE/DELETE ...]: the CTEs are in scope for the statement's
182182+ WHERE and SET subqueries. *)
183183+ | l = with_clause; u = update_stmt { Update { u with ctes = l } }
184184+ | l = with_clause; d = delete_stmt { Delete { d with ctes = l } }
185185+ | s = create_table_statement { s }
186186+ | c = create_index_stmt { Create_index c }
187187+ | v = create_view_stmt { Create_view v }
188188+ | tg = create_trigger_stmt { Create_trigger tg }
189189+ | d = drop_stmt { Drop d }
190190+ | a = alter_table_stmt { Alter_table a }
191191+ | p = pragma_stmt { Pragma p }
192192+ | BEGIN; option(TRANSACTION) { Begin }
193193+ | COMMIT; option(TRANSACTION) { Commit }
194194+ | ROLLBACK; option(TRANSACTION); r = rollback_tail { r }
195195+ | SAVEPOINT; name = ident { Savepoint name }
196196+ | RELEASE; ioption(SAVEPOINT); name = ident { Release name }
197197+ | VACUUM; option(ident) { Vacuum }
198198+ | ANALYZE; ioption(analyze_target) { Analyze }
199199+200200+(* ANALYZE's optional argument: a schema, table, or index name, optionally
201201+ schema-qualified. The name is ignored -- ANALYZE is run as a no-op. *)
202202+analyze_target:
203203+ | ident { () }
204204+ | ident; DOT; ident { () }
205205+206206+(* Plain ROLLBACK, or ROLLBACK TO a savepoint. *)
207207+rollback_tail:
208208+ | { Rollback }
209209+ | TO; ioption(SAVEPOINT); name = ident { Rollback_to name }
210210+211211+(* ── INSERT ──────────────────────────────────────────────────── *)
212212+213213+(* The source is a query: a bare [VALUES (..), (..)] list or a [SELECT]. Both
214214+ are [select_stmt] cores, so [INSERT ... VALUES] and [INSERT ... SELECT] share
215215+ one production (the executor fast-paths a literal VALUES list). *)
216216+insert_stmt:
217217+ | INSERT; c = insert_conflict; INTO; name = ident;
218218+ cols = loption(insert_columns); s = select_stmt;
219219+ up = ioption(upsert_clause); ret = loption(returning_clause)
220220+ { { conflict = c; table = name; columns = cols;
221221+ source = Select s; upsert = up; returning = ret } }
222222+ | INSERT; c = insert_conflict; INTO; name = ident; DEFAULT; VALUES;
223223+ ret = loption(returning_clause)
224224+ { { conflict = c; table = name; columns = [];
225225+ source = Default_values; upsert = None; returning = ret } }
226226+ | REPLACE; INTO; name = ident;
227227+ cols = loption(insert_columns); s = select_stmt;
228228+ ret = loption(returning_clause)
229229+ { { conflict = Replace; table = name; columns = cols;
230230+ source = Select s; upsert = None; returning = ret } }
231231+232232+(* [INSERT OR <action>]; a bare INSERT defaults to ABORT. REPLACE and ROLLBACK
233233+ are keyword tokens; IGNORE/ABORT/FAIL are context-sensitive (lexed as IDENT,
234234+ so they stay usable as identifiers elsewhere). *)
235235+insert_conflict:
236236+ | { Abort }
237237+ | OR; REPLACE { Replace }
238238+ | OR; ROLLBACK { Rollback }
239239+ | OR; id = IDENT
240240+ { match String.uppercase_ascii id with
241241+ | "IGNORE" -> Ignore
242242+ | "ABORT" -> Abort
243243+ | "FAIL" -> Fail
244244+ | "REPLACE" -> Replace
245245+ | "ROLLBACK" -> Rollback
246246+ | _ -> failwith ("INSERT OR: unknown conflict action " ^ id) }
247247+248248+insert_columns:
249249+ | LPAREN; cs = names; RPAREN { cs }
250250+251251+(* ON CONFLICT [(target cols)] DO NOTHING | DO UPDATE SET ... [WHERE ...] *)
252252+upsert_clause:
253253+ | ON; CONFLICT; target = loption(conflict_target); DO; NOTHING
254254+ { { target = target; action = Nothing } }
255255+ | ON; CONFLICT; target = loption(conflict_target); DO; UPDATE; SET;
256256+ sets = separated_nonempty_list(COMMA, assignment);
257257+ w = option(where_clause)
258258+ { { target = target; action = Update (sets, w) } }
259259+260260+conflict_target:
261261+ | LPAREN; cs = names; RPAREN { cs }
262262+263263+value_tuple:
264264+ | LPAREN; es = separated_nonempty_list(COMMA, expr); RPAREN { es }
265265+266266+(* RETURNING expr, expr, ... (shared by INSERT / UPDATE / DELETE). *)
267267+returning_clause:
268268+ | RETURNING; cs = separated_nonempty_list(COMMA, result_col) { cs }
269269+270270+(* ── DELETE ──────────────────────────────────────────────────── *)
271271+272272+delete_stmt:
273273+ | DELETE; FROM; name = ident; alias = option(table_alias);
274274+ where = option(where_clause); ret = loption(returning_clause)
275275+ { { ctes = []; table = name; alias = alias; where = where;
276276+ returning = ret } }
277277+278278+(* ── UPDATE ──────────────────────────────────────────────────── *)
279279+280280+update_stmt:
281281+ | UPDATE; name = ident; SET;
282282+ sets = separated_nonempty_list(COMMA, assignment);
283283+ f = ioption(from_part);
284284+ where = option(where_clause); ret = loption(returning_clause)
285285+ { { ctes = []; table = name; sets = sets; from = f;
286286+ where = where; returning = ret } }
287287+288288+assignment:
289289+ | col = ident; EQ; e = expr { (col, e) }
290290+291291+(* ── CREATE INDEX ────────────────────────────────────────────── *)
292292+293293+create_index_stmt:
294294+ | CREATE; u = boption(UNIQUE); INDEX; e = if_not_exists_b; name = ident;
295295+ ON; table = ident;
296296+ LPAREN; cs = separated_nonempty_list(COMMA, indexed_column); RPAREN;
297297+ w = ioption(index_where)
298298+ { { if_not_exists = e; unique = u; name = name; table = table;
299299+ columns = List.filter_map (fun c -> c) cs;
300300+ has_expr = List.exists (fun c -> c = None) cs;
301301+ where = w } }
302302+303303+(* An indexed key: a plain column ([Some name]) or an expression ([None]). An
304304+ ASC/DESC qualifier is ignored (the index is unordered); COLLATE is part of the
305305+ key expression. *)
306306+indexed_column:
307307+ | e = expr; ioption(ASC); ioption(DESC)
308308+ { match e with
309309+ | Col (_, name) | Collate (Col (_, name), _) -> Some name
310310+ | _ -> None }
311311+312312+(* [WHERE p] makes a partial index; the predicate is kept on the AST and used by
313313+ the executor only to reject a partial UNIQUE index. *)
314314+index_where:
315315+ | WHERE; e = expr { e }
316316+317317+(* ── CREATE VIEW ─────────────────────────────────────────────── *)
318318+319319+create_view_stmt:
320320+ | CREATE; VIEW; e = if_not_exists_b; name = ident;
321321+ cols = loption(view_columns); AS; s = select_stmt
322322+ { { name = name; if_not_exists = e; columns = cols;
323323+ select = s } }
324324+325325+view_columns:
326326+ | LPAREN; cs = names; RPAREN { cs }
327327+328328+(* ── CREATE TRIGGER ──────────────────────────────────────────── *)
329329+330330+(* [CREATE TRIGGER name [BEFORE|AFTER|INSTEAD OF] event ON table [FOR EACH ROW]
331331+ [WHEN expr] BEGIN stmt; ... END]. Each body statement is terminated by its own
332332+ semicolon (including the last, before END). *)
333333+create_trigger_stmt:
334334+ | CREATE; TRIGGER; e = if_not_exists_b; name = ident;
335335+ timing = trigger_timing; ev = trigger_event; ON; tbl = ident;
336336+ ioption(for_each_row); w = ioption(when_clause);
337337+ BEGIN; body = trigger_body; END
338338+ { { name = name; if_not_exists = e; timing = timing;
339339+ event = ev; table = tbl; when_ = w; body = body } }
340340+341341+trigger_timing:
342342+ | BEFORE { Before }
343343+ | AFTER { After }
344344+ | INSTEAD; OF { Instead_of }
345345+ | { Before }
346346+347347+trigger_event:
348348+ (* These constructor names are shared with [stmt]; the [stmt] arm wins as the
349349+ last definition, so a [trigger_event] use needs the cast. *)
350350+ | INSERT { (Insert : trigger_event) }
351351+ | DELETE { (Delete : trigger_event) }
352352+ | UPDATE { (Update [] : trigger_event) }
353353+ | UPDATE; OF; cs = names { (Update cs : trigger_event) }
354354+355355+for_each_row:
356356+ | FOR; EACH; ROW { () }
357357+358358+when_clause:
359359+ | WHEN; e = expr { e }
360360+361361+trigger_body:
362362+ | { [] }
363363+ | s = trigger_action; SEMI; rest = trigger_body { s :: rest }
364364+365365+trigger_action:
366366+ | s = select_stmt { Select s }
367367+ | i = insert_stmt { Insert i }
368368+ | u = update_stmt { Update u }
369369+ | d = delete_stmt { Delete d }
370370+371371+(* ── DROP TABLE / INDEX / VIEW / TRIGGER ─────────────────────── *)
372372+373373+drop_stmt:
374374+ | DROP; TABLE; e = if_exists_b; name = ident
375375+ { { object_ = Table; name = name; if_exists = e } }
376376+ | DROP; INDEX; e = if_exists_b; name = ident
377377+ { { object_ = Index; name = name; if_exists = e } }
378378+ | DROP; VIEW; e = if_exists_b; name = ident
379379+ { { object_ = View; name = name; if_exists = e } }
380380+ | DROP; TRIGGER; e = if_exists_b; name = ident
381381+ { { object_ = Trigger; name = name; if_exists = e } }
382382+383383+if_exists_b:
384384+ | IF; EXISTS { true }
385385+ | { false }
386386+387387+(* ── ALTER TABLE ADD COLUMN ──────────────────────────────────── *)
388388+389389+(* [ADD [COLUMN] name [type] [DEFAULT literal]]. Column constraints
390390+ (PRIMARY KEY / UNIQUE / NOT NULL) are not accepted: the executor cannot
391391+ honour them on an existing table, so rejecting them at parse time keeps a
392392+ stub from silently dropping a constraint. *)
393393+alter_table_stmt:
394394+ | ALTER; TABLE; name = ident; ADD; ioption(COLUMN);
395395+ col = name; ty = ioption(add_type); def = ioption(default_clause)
396396+ { { table = name;
397397+ op = Add_column {
398398+ ac_name = col;
399399+ ac_affinity = (match ty with Some t -> t | None -> "");
400400+ ac_default = def } } }
401401+ | ALTER; TABLE; name = ident; RENAME; TO; nn = ident
402402+ { { table = name; op = Rename_table nn } }
403403+ | ALTER; TABLE; name = ident; RENAME; ioption(COLUMN); c = ident; TO;
404404+ nn = ident
405405+ { { table = name; op = Rename_column (c, nn) } }
406406+ | ALTER; TABLE; name = ident; DROP; ioption(COLUMN); c = ident
407407+ { { table = name; op = Drop_column c } }
408408+409409+add_type:
410410+ | name = IDENT; p = ioption(type_params)
411411+ { match p with Some s -> name ^ s | None -> name }
412412+413413+type_params:
414414+ | LPAREN; ns = separated_nonempty_list(COMMA, NUMBER); RPAREN
415415+ { "(" ^ String.concat "," ns ^ ")" }
416416+417417+default_clause:
418418+ | DEFAULT; l = literal { l }
419419+ | DEFAULT; MINUS; n = NUMBER
420420+ { match number_to_value n with
421421+ | Int i -> Int (Int64.neg i)
422422+ | Float f -> Float (-. f)
423423+ | v -> v }
424424+ | DEFAULT; PLUS; n = NUMBER { number_to_value n }
425425+426426+if_not_exists_b:
427427+ | IF; NOT; EXISTS { true }
428428+ | { false }
429429+430430+(* ── PRAGMA ──────────────────────────────────────────────────── *)
431431+432432+pragma_stmt:
433433+ | PRAGMA; name = ident { { name = name; value = None } }
434434+ | PRAGMA; name = ident; EQ; v = pragma_value { { name = name; value = Some v } }
435435+ | PRAGMA; name = ident; LPAREN; v = pragma_value; RPAREN
436436+ { { name = name; value = Some v } }
437437+438438+(* A PRAGMA value is a literal or a bareword keyword like WAL / NORMAL / ON. *)
439439+pragma_value:
440440+ | l = literal { l }
441441+ | s = IDENT { Text s }
442442+ | s = ON { Text s } (* a bareword value that is also a keyword, e.g. [= on] *)
443443+444444+(* ── SELECT ──────────────────────────────────────────────────── *)
445445+446446+(* A query: one core, then [UNION]/[EXCEPT]/[INTERSECT] arms, then the
447447+ compound-wide ORDER BY / LIMIT, optionally prefixed by WITH. The arms and the
448448+ trailing clauses are folded onto the first core's record. *)
449449+select_stmt:
450450+ | ctes = loption(with_clause); first = select_core;
451451+ arms = list(compound_arm);
452452+ order_by = loption(order_clause); lim = option(limit_clause)
453453+ { let limit, offset =
454454+ match lim with None -> (None, None) | Some (l, o) -> (Some l, o)
455455+ in
456456+ { first with ctes; compound = arms; order_by; limit; offset } }
457457+458458+select_core:
459459+ | SELECT; d = boption(DISTINCT);
460460+ cols = separated_nonempty_list(COMMA, result_col);
461461+ src = option(from_part);
462462+ where = option(where_clause);
463463+ group_by = loption(group_clause);
464464+ having = option(having_clause)
465465+ { let from, joins =
466466+ match src with Some (f, js) -> (f, js) | None -> (Unit, [])
467467+ in
468468+ { ctes = []; distinct = d; cols; from; joins; where; group_by; having;
469469+ compound = []; order_by = []; limit = None; offset = None } }
470470+ (* A bare VALUES list is a core: rows over columns column1..columnN. *)
471471+ | VALUES; rows = separated_nonempty_list(COMMA, value_tuple)
472472+ { { ctes = []; distinct = false;
473473+ cols = [ { expr = Star; alias = None } ];
474474+ from = Values rows; joins = []; where = None; group_by = [];
475475+ having = None;
476476+ compound = []; order_by = []; limit = None; offset = None } }
477477+478478+from_part:
479479+ | FROM; from = from_clause; joins = list(join_item) { (from, joins) }
480480+481481+having_clause:
482482+ | HAVING; e = expr { e }
483483+484484+compound_arm:
485485+ | UNION; ALL; c = select_core { (Union_all, c) }
486486+ | UNION; c = select_core { (Union, c) }
487487+ | EXCEPT; c = select_core { (Except, c) }
488488+ | INTERSECT; c = select_core { (Intersect, c) }
489489+490490+(* WITH [RECURSIVE] cte, cte, ... (RECURSIVE is accepted; a self-referential
491491+ body is handled by the recursive runner.) *)
492492+with_clause:
493493+ | WITH; option(RECURSIVE); l = separated_nonempty_list(COMMA, cte_def) { l }
494494+495495+cte_def:
496496+ | name = ident; cols = loption(cte_columns); AS; ioption(materialized);
497497+ LPAREN; s = select_stmt; RPAREN
498498+ { { Ast.name; columns = cols; query = s } }
499499+500500+(* An [AS [NOT] MATERIALIZED] hint on a CTE: a planner directive that does not
501501+ change results, so it is accepted and ignored. *)
502502+materialized:
503503+ | MATERIALIZED { () }
504504+ | NOT; MATERIALIZED { () }
505505+506506+cte_columns:
507507+ | LPAREN; cs = names; RPAREN { cs }
508508+509509+group_clause:
510510+ | GROUP; BY; l = separated_nonempty_list(COMMA, expr) { l }
511511+512512+from_clause:
513513+ (* [Table]/[Select]/[Values] are shared with other Ast types whose definitions
514514+ come later and so win by default; cast to pick the [from_clause] arm. *)
515515+ | t = table_ref { (Table t : from_clause) }
516516+ | LPAREN; s = select_stmt; RPAREN; a = option(table_alias)
517517+ { (Select { fs_select = s; fs_alias = a } : from_clause) }
518518+ | name = IDENT; LPAREN; args = separated_list(COMMA, expr); RPAREN;
519519+ a = option(table_alias)
520520+ { (Table_function { tf_name = String.lowercase_ascii name;
521521+ tf_args = args; tf_alias = a } : from_clause) }
522522+523523+result_col:
524524+ | STAR { { expr = Star; alias = None } }
525525+ | q = IDENT; DOT; STAR { { expr = Qualified_star q; alias = None } }
526526+ | e = expr; a = option(col_alias) { { expr = e; alias = a } }
527527+528528+col_alias:
529529+ | AS; s = IDENT { s }
530530+ | AS; s = STRING { s } (* a quoted string is accepted as a column alias *)
531531+ | s = IDENT { s }
532532+533533+table_ref:
534534+ | name = IDENT; a = option(table_alias)
535535+ { { name = name; alias = a; subquery = None } }
536536+537537+table_alias:
538538+ | AS; s = IDENT { s }
539539+ | s = IDENT { s }
540540+541541+(* A join operand: a named table, or a parenthesised subquery bound under its
542542+ alias (an unaliased one gets a unique synthetic name from its source offset,
543543+ never referenced by the user). *)
544544+join_source:
545545+ | t = table_ref { t }
546546+ | LPAREN; s = select_stmt; RPAREN; a = table_alias
547547+ { { name = a; alias = Some a; subquery = Some s } }
548548+ | LPAREN; s = select_stmt; RPAREN
549549+ { let nm = Printf.sprintf "__sub%d" $startofs in
550550+ { name = nm; alias = None; subquery = Some s } }
551551+552552+(* An extra FROM table: a join (with an optional ON/USING constraint -- an
553553+ unconstrained [A JOIN B] is allowed, like sqlite3), or a Cartesian product --
554554+ [CROSS JOIN] or a comma join. *)
555555+join_item:
556556+ | k = join_kw; t = join_source; c = join_constraint
557557+ { let on, using = c in
558558+ { table = t; on; type_ = k; natural = false; using } }
559559+ | NATURAL; k = join_kw; t = join_source
560560+ { { table = t; on = Lit (Int 1L); type_ = k; natural = true;
561561+ using = [] } }
562562+ | CROSS; JOIN; t = join_source
563563+ { { table = t; on = Lit (Int 1L); type_ = Inner; natural = false;
564564+ using = [] } }
565565+ | COMMA; t = join_source
566566+ { { table = t; on = Lit (Int 1L); type_ = Inner; natural = false;
567567+ using = [] } }
568568+569569+(* The optional join constraint: [ON expr], [USING (cols)], or nothing (an
570570+ unconstrained join, equivalent to ON true). *)
571571+join_constraint:
572572+ | ON; e = expr { (e, []) }
573573+ | USING; LPAREN; cs = names; RPAREN { (Lit (Int 1L), cs) }
574574+ | %prec NO_CONSTRAINT { (Lit (Int 1L), []) }
575575+576576+join_kw:
577577+ | JOIN { Inner }
578578+ | INNER; JOIN { Inner }
579579+ | LEFT; JOIN { Left }
580580+ | LEFT; OUTER; JOIN { Left }
581581+ | RIGHT; JOIN { Right }
582582+ | RIGHT; OUTER; JOIN { Right }
583583+ | FULL; JOIN { Full }
584584+ | FULL; OUTER; JOIN { Full }
585585+586586+where_clause:
587587+ | WHERE; e = expr { e }
588588+589589+order_clause:
590590+ | ORDER; BY; l = separated_nonempty_list(COMMA, order_term)
591591+ { List.concat l }
592592+593593+order_term:
594594+ | e = expr; d = dir { [ (e, d) ] }
595595+ | e = expr; d = dir; kw = IDENT; pos = IDENT { nulls_order e d kw pos }
596596+597597+dir:
598598+ | ASC { Asc }
599599+ | DESC { Desc }
600600+ | { Asc }
601601+602602+(* Returns (limit, offset option). Note the comma form [LIMIT off, cnt] puts the
603603+ OFFSET first -- the reverse of the [OFFSET] keyword form. *)
604604+limit_clause:
605605+ | LIMIT; n = limit_value { (n, None) }
606606+ | LIMIT; n = limit_value; OFFSET; o = limit_value { (n, Some o) }
607607+ | LIMIT; o = limit_value; COMMA; n = limit_value { (n, Some o) }
608608+609609+limit_value:
610610+ | n = NUMBER { Lit (number_to_value n) }
611611+ | MINUS; n = NUMBER
612612+ { Lit (match number_to_value n with
613613+ | Int i -> Int (Int64.neg i)
614614+ | Float f -> Float (-. f)
615615+ | v -> v) }
616616+ | QUESTION { Param 0 }
617617+618618+(* ── Expressions ─────────────────────────────────────────────── *)
619619+620620+expr:
621621+ | e1 = expr; OR; e2 = expr { Binary (Or, e1, e2) }
622622+ | e1 = expr; AND; e2 = expr { Binary (And, e1, e2) }
623623+ | NOT; e = expr { Unop_not e }
624624+ | e1 = expr; EQ; e2 = expr { Binary (Eq, e1, e2) }
625625+ | e1 = expr; NEQ; e2 = expr { Binary (Neq, e1, e2) }
626626+ | e1 = expr; LT; e2 = expr { Binary (Lt, e1, e2) }
627627+ | e1 = expr; LE; e2 = expr { Binary (Le, e1, e2) }
628628+ | e1 = expr; GT; e2 = expr { Binary (Gt, e1, e2) }
629629+ | e1 = expr; GE; e2 = expr { Binary (Ge, e1, e2) }
630630+ | e1 = expr; LIKE; e2 = expr { Binary (Like, e1, e2) }
631631+ | e1 = expr; LIKE; e2 = expr; ESCAPE; e3 = expr
632632+ { Func ("like", [ e2; e1; e3 ]) }
633633+ | e1 = expr; NOT; LIKE; e2 = expr %prec LIKE
634634+ { Unop_not (Binary (Like, e1, e2)) }
635635+ | e1 = expr; NOT; LIKE; e2 = expr; ESCAPE; e3 = expr
636636+ { Unop_not (Func ("like", [ e2; e1; e3 ])) }
637637+ | e1 = expr; GLOB; e2 = expr { Binary (Glob, e1, e2) }
638638+ | e1 = expr; NOT; GLOB; e2 = expr %prec GLOB
639639+ { Unop_not (Binary (Glob, e1, e2)) }
640640+ | e1 = expr; PLUS; e2 = expr { Binary (Add, e1, e2) }
641641+ | e1 = expr; MINUS; e2 = expr { Binary (Sub, e1, e2) }
642642+ | e1 = expr; STAR; e2 = expr { Binary (Mul, e1, e2) }
643643+ | e1 = expr; SLASH; e2 = expr { Binary (Div, e1, e2) }
644644+ | e1 = expr; PERCENT; e2 = expr { Binary (Mod, e1, e2) }
645645+ | e1 = expr; BITAND; e2 = expr { Binary (Bit_and, e1, e2) }
646646+ | e1 = expr; BITOR; e2 = expr { Binary (Bit_or, e1, e2) }
647647+ | e1 = expr; SHL; e2 = expr { Binary (Shl, e1, e2) }
648648+ | e1 = expr; SHR; e2 = expr { Binary (Shr, e1, e2) }
649649+ | e1 = expr; CONCAT; e2 = expr { Binary (Concat, e1, e2) }
650650+ | e1 = expr; ARROW; e2 = expr { Binary (Json_get, e1, e2) }
651651+ | e1 = expr; ARROW2; e2 = expr { Binary (Json_get_text, e1, e2) }
652652+ (* [e COLLATE name]: highest precedence, so it binds to the nearest operand. *)
653653+ | e = expr; COLLATE; c = IDENT { Collate (e, String.uppercase_ascii c) }
654654+ (* Unary [-e] is [0 - e]; unary [+e] is [e]. *)
655655+ | MINUS; e = expr %prec UMINUS { Binary (Sub, Lit (Int 0L), e) }
656656+ (* Unary [+] is a value no-op but strips the operand's affinity and collation
657657+ (lang_expr.html): wrapping in [COLLATE BINARY] preserves the value, removes
658658+ the column-affinity coercion (which only fires on a bare column), and pins
659659+ the default binary collation -- so [a = +b] does not coerce like [a = b]. *)
660660+ | PLUS; e = expr %prec UMINUS
661661+ (* Unary [+] strips affinity but is a no-op for collation, so it must not
662662+ override an explicit inner COLLATE ([+x COLLATE nocase] stays nocase);
663663+ the wrapper itself already suppresses affinity coercion. *)
664664+ { match e with Collate _ -> e | _ -> Collate (e, "BINARY") }
665665+ (* [x BETWEEN lo AND hi] desugars to [x >= lo AND x <= hi]; bounds are atoms
666666+ (optionally with a trailing COLLATE) so the inner AND can't be mistaken for
667667+ a boolean conjunction. *)
668668+ | e = expr; BETWEEN; lo = between_bound; AND; hi = between_bound
669669+ { Binary (And, Binary (Ge, e, lo), Binary (Le, e, hi)) }
670670+ | e = expr; NOT; BETWEEN; lo = between_bound; AND; hi = between_bound
671671+ { Unop_not (Binary (And, Binary (Ge, e, lo), Binary (Le, e, hi))) }
672672+ (* The right operand is an [atom] (not a full [expr]): an atom cannot begin
673673+ with NOT, so [IS NOT] is unambiguous, and atoms bind tighter than IS so
674674+ there is no associativity conflict. A complex RHS is parenthesised. This
675675+ covers [IS NULL], [IS NOT NULL], [col IS col], [x IS <literal>]. *)
676676+ (* [IS TRUE]/[IS FALSE] test truthiness, distinct from [IS 1]/[IS 0] equality,
677677+ so they take the keyword tokens; the general RHS is a [nonbool_atom], which
678678+ excludes the bare TRUE/FALSE literals to keep these unambiguous. *)
679679+ | e1 = expr; IS; TRUE { is_true e1 }
680680+ | e1 = expr; IS; FALSE { is_false e1 }
681681+ | e1 = expr; IS; NOT; TRUE { is_not_true e1 }
682682+ | e1 = expr; IS; NOT; FALSE { is_not_false e1 }
683683+ | e1 = expr; IS; e2 = nonbool_atom { mk_is e1 e2 }
684684+ | e1 = expr; IS; NOT; e2 = nonbool_atom { mk_is_not e1 e2 }
685685+ (* [x ISNULL] / [x NOTNULL]: the one-word postfix synonyms for IS [NOT] NULL. *)
686686+ | e = expr; ISNULL { Is_null e }
687687+ | e = expr; NOTNULL { Is_not_null e }
688688+ | e = expr; IN; LPAREN; l = separated_nonempty_list(COMMA, expr); RPAREN
689689+ { mk_in e l }
690690+ | e = expr; IN; LPAREN; RPAREN
691691+ { In_list (e, []) }
692692+ | e = expr; IN; LPAREN; s = select_stmt; RPAREN
693693+ { In_select (e, s) }
694694+ | e = expr; NOT; IN; LPAREN; l = separated_nonempty_list(COMMA, expr); RPAREN
695695+ { Unop_not (mk_in e l) }
696696+ | e = expr; NOT; IN; LPAREN; RPAREN
697697+ { Unop_not (In_list (e, [])) }
698698+ | e = expr; NOT; IN; LPAREN; s = select_stmt; RPAREN
699699+ { Unop_not (In_select (e, s)) }
700700+ | a = atom { a }
701701+702702+(* A BETWEEN bound: an atom, optionally with a trailing COLLATE (which binds
703703+ tighter than the comparison the bound feeds into). *)
704704+between_bound:
705705+ (* [%prec BETWEEN] (below COLLATE) makes a trailing COLLATE bind to the bound,
706706+ [b COLLATE c], rather than to the whole BETWEEN expression. *)
707707+ | a = atom %prec BETWEEN { a }
708708+ | a = atom; COLLATE; c = IDENT { Collate (a, String.uppercase_ascii c) }
709709+710710+(* [atom] is a [nonbool_atom] plus the bare TRUE/FALSE literals. The IS rules use
711711+ [nonbool_atom] so [IS TRUE]/[IS FALSE] (truthiness) stay distinct from
712712+ [IS <literal>] (equality) without a grammar conflict; every other context uses
713713+ [atom], where TRUE/FALSE are the integers 1/0 (lang_expr.html). *)
714714+atom:
715715+ | a = nonbool_atom { a }
716716+ | TRUE { Lit (Int 1L) }
717717+ | FALSE { Lit (Int 0L) }
718718+719719+nonbool_atom:
720720+ | LPAREN; e = expr; RPAREN { e }
721721+ (* A parenthesised subquery used as a value: SELECT/WITH/VALUES after LPAREN
722722+ are not in [expr]'s first set, so this never conflicts with [( expr )]. *)
723723+ | LPAREN; s = select_stmt; RPAREN { Scalar_select s }
724724+ | EXISTS; LPAREN; s = select_stmt; RPAREN { Exists s }
725725+ | l = literal { Lit l }
726726+ | QUESTION { Param 0 }
727727+ | CAST; LPAREN; e = expr; AS; ty = IDENT; RPAREN
728728+ { Cast (e, ty) }
729729+ (* CASE [base] WHEN c THEN r ... [ELSE e] END. The optional base before the
730730+ first WHEN gives the simple form; without it, the searched form. *)
731731+ | CASE; base = ioption(expr); branches = nonempty_list(case_when);
732732+ els = ioption(case_else); END
733733+ { Case (base, branches, els) }
734734+ | name = IDENT; LPAREN; DISTINCT; e = expr; RPAREN; f = ioption(filter_clause);
735735+ w = ioption(window_clause)
736736+ { decorate (Distinct_agg (String.lowercase_ascii name, e)) f w }
737737+ | name = IDENT; LPAREN; args = func_args; RPAREN; f = ioption(filter_clause);
738738+ w = ioption(window_clause)
739739+ { decorate (Func (String.lowercase_ascii name, args)) f w }
740740+ | name = IDENT; LPAREN; args = func_args; ob = order_clause; RPAREN;
741741+ f = ioption(filter_clause)
742742+ { let a = Ordered_agg (Func (String.lowercase_ascii name, args), ob) in
743743+ match f with Some p -> Filter (a, p) | None -> a }
744744+ (* [replace] is also a function, but REPLACE is a keyword token (INSERT OR
745745+ REPLACE), so it does not arrive as IDENT. *)
746746+ | REPLACE; LPAREN; args = func_args; RPAREN
747747+ { Func ("replace", args) }
748748+ (* [like]/[glob] are also functions, but LIKE/GLOB are keyword tokens (the
749749+ binary operators), so they do not arrive as IDENT. *)
750750+ | LIKE; LPAREN; args = func_args; RPAREN { Func ("like", args) }
751751+ | GLOB; LPAREN; args = func_args; RPAREN { Func ("glob", args) }
752752+ (* KEY is a non-reserved keyword in sqlite3: [key] is an ordinary column
753753+ name outside PRIMARY/FOREIGN KEY. *)
754754+ | q = IDENT; DOT; name = IDENT { Col (Some q, name) }
755755+ | q = IDENT; DOT; name = KEY { Col (Some q, name) }
756756+ | name = IDENT { Col (None, name) }
757757+ | name = KEY { Col (None, name) }
758758+759759+filter_clause:
760760+ | FILTER; LPAREN; WHERE; p = expr; RPAREN { p }
761761+762762+(* OVER (PARTITION BY ... ORDER BY ...): an explicit frame clause is not parsed;
763763+ the default frame applies. *)
764764+window_clause:
765765+ | OVER; LPAREN; pb = loption(partition_clause); ob = loption(order_clause);
766766+ fr = ioption(frame_clause); RPAREN
767767+ { { partition = pb; order = ob; frame = fr } }
768768+769769+partition_clause:
770770+ | PARTITION; BY; l = separated_nonempty_list(COMMA, expr) { l }
771771+772772+(* A frame: [ROWS|RANGE] BETWEEN lo AND hi, or a single bound [ROWS|RANGE] lo
773773+ (shorthand for BETWEEN lo AND CURRENT ROW). *)
774774+frame_clause:
775775+ | k = frame_kind; BETWEEN; lo = frame_bound; AND; hi = frame_bound
776776+ { { rows = k; start = lo; end_ = hi } }
777777+ | k = frame_kind; lo = frame_bound
778778+ { { rows = k; start = lo; end_ = Current_row } }
779779+780780+frame_kind:
781781+ | ROWS { true }
782782+ | RANGE { false }
783783+784784+frame_bound:
785785+ | UNBOUNDED; PRECEDING { Unbounded_preceding }
786786+ | UNBOUNDED; FOLLOWING { Unbounded_following }
787787+ | CURRENT; ROW { Current_row }
788788+ | n = expr; PRECEDING { Preceding n }
789789+ | n = expr; FOLLOWING { Following n }
790790+791791+case_when:
792792+ | WHEN; c = expr; THEN; r = expr { (c, r) }
793793+794794+case_else:
795795+ | ELSE; e = expr { e }
796796+797797+func_args:
798798+ | STAR { [ Star ] }
799799+ | l = separated_list(COMMA, expr) { l }
800800+801801+literal:
802802+ | n = NUMBER { number_to_value n }
803803+ | s = STRING { Text s }
804804+ | b = BLOB { Blob b }
805805+ | NULL { Null }
806806+807807+(* ── CREATE TABLE ────────────────────────────────────────────── *)
808808+809809+create_table:
810810+ | b = create_table_body; EOF { b }
811811+812812+create_table_body:
813813+ | CREATE; TABLE; e = if_not_exists_b; name = ident;
814814+ LPAREN; defs = separated_nonempty_list(COMMA, column_or_constraint); RPAREN;
815815+ wr = table_options
816816+ { make_create_table ~without_rowid:wr name e defs }
817817+818818+(* The statement form left-factors the column-def and [AS query] tails so the
819819+ shared [CREATE TABLE name] prefix has no conflict. *)
820820+create_table_statement:
821821+ | CREATE; TABLE; e = if_not_exists_b; name = ident; r = create_table_tail
822822+ { match r with
823823+ | `Cols (defs, wr) ->
824824+ Create_table (make_create_table ~without_rowid:wr name e defs)
825825+ | `As s ->
826826+ Create_table_as
827827+ { name = name; if_not_exists = e; query = s } }
828828+829829+create_table_tail:
830830+ | LPAREN; defs = separated_nonempty_list(COMMA, column_or_constraint); RPAREN;
831831+ opts = table_options
832832+ { `Cols (defs, opts) }
833833+ | AS; s = select_stmt { `As s }
834834+835835+(* Trailing table options after the column list: [WITHOUT ROWID] (kept) and
836836+ [STRICT] (accepted, ignored), comma-separated in any order. *)
837837+table_options:
838838+ | (* empty *) { false }
839839+ | WITHOUT; ident; ioption(other_table_options) { true }
840840+ | IDENT; ioption(other_table_options) { false }
841841+842842+other_table_options:
843843+ | COMMA; WITHOUT; ident; ioption(other_table_options) { () }
844844+ | COMMA; IDENT; ioption(other_table_options) { () }
845845+846846+(* Dispatch: table constraints start with PRIMARY, UNIQUE, FOREIGN,
847847+ CHECK, or CONSTRAINT. Everything else is a column definition. *)
848848+column_or_constraint:
849849+ | t = table_constraint { `Tbl t }
850850+ | k = table_check { `Check k }
851851+ | f = table_fk { `Fk f }
852852+ | name = name; body = col_body { `Col (name, body) }
853853+854854+(* Column names: IDENT plus keywords that never start a table constraint *)
855855+name:
856856+ | s = IDENT { s }
857857+ | s = TABLE { s }
858858+ | s = CREATE { s }
859859+ | s = KEY { s }
860860+ | s = NOT { s }
861861+ | s = NULL { s }
862862+ | s = DEFAULT { s }
863863+ | s = REFERENCES { s }
864864+ | s = COLLATE { s }
865865+ | s = GENERATED { s }
866866+ | s = ALWAYS { s }
867867+ | s = AS { s }
868868+ | s = AUTOINCREMENT { s }
869869+ | s = EXISTS { s }
870870+ | s = IF { s }
871871+ | s = ON { s }
872872+ | s = ASC { s }
873873+ | s = DESC { s }
874874+ | s = SELECT { s }
875875+ | s = DISTINCT { s }
876876+ | s = FROM { s }
877877+ | s = WHERE { s }
878878+ | s = JOIN { s }
879879+ | s = INNER { s }
880880+ | s = LEFT { s }
881881+ | s = OUTER { s }
882882+ | s = ORDER { s }
883883+ | s = BY { s }
884884+ | s = LIMIT { s }
885885+ | s = AND { s }
886886+ | s = OR { s }
887887+ | s = IN { s }
888888+ | s = IS { s }
889889+ | s = LIKE { s }
890890+891891+(* ── Table-level constraints ─────────────────────────────────── *)
892892+893893+table_constraint:
894894+ | UNIQUE; LPAREN; cs = indexed_names; RPAREN
895895+ { Unique cs }
896896+ | PRIMARY; KEY; LPAREN; cs = indexed_names; RPAREN
897897+ { Primary_key cs }
898898+ | CONSTRAINT; ident; UNIQUE; LPAREN; cs = indexed_names; RPAREN
899899+ { Unique cs }
900900+ | CONSTRAINT; ident; PRIMARY; KEY; LPAREN; cs = indexed_names; RPAREN
901901+ { Primary_key cs }
902902+903903+(* A PRIMARY KEY / UNIQUE column list: each column may carry a COLLATE and an
904904+ ASC/DESC qualifier, of which only the column name is kept. *)
905905+indexed_names:
906906+ | separated_nonempty_list(COMMA, indexed_name) { $1 }
907907+908908+indexed_name:
909909+ | c = ident; ioption(constraint_collate); ioption(ASC); ioption(DESC) { c }
910910+911911+constraint_collate:
912912+ | COLLATE; ident { () }
913913+914914+(* A CHECK constraint, column- or table-level. The predicate is parsed as a real
915915+ expression and its verbatim source offsets are captured so a violation can
916916+ quote it exactly. CONSTRAINT names are accepted but unused (SQLite reports the
917917+ predicate text, not the name). *)
918918+table_check:
919919+ | CHECK; LPAREN; e = expr; RPAREN
920920+ { { expr = e; start = $startofs(e); stop = $endofs(e) } }
921921+ | CONSTRAINT; ident; CHECK; LPAREN; e = expr; RPAREN
922922+ { { expr = e; start = $startofs(e); stop = $endofs(e) } }
923923+924924+(* A table-level FOREIGN KEY. The child columns pair with a [fk_clause]
925925+ ([REFERENCES parent [(cols)] [ON DELETE/UPDATE action]]); the result carries
926926+ the child columns so [create_table_body] can build the {!Ast.foreign_key}. *)
927927+table_fk:
928928+ | FOREIGN; KEY; LPAREN; cs = names; RPAREN; r = fk_clause
929929+ { (cs, r) }
930930+ | CONSTRAINT; ident; FOREIGN; KEY; LPAREN; cs = names; RPAREN; r = fk_clause
931931+ { (cs, r) }
932932+933933+(* The shared REFERENCES tail, also used for a column-level [col REFERENCES ...].
934934+ Returns [(parent, parent_cols, on_delete, on_update)]; parent_cols [] means
935935+ the parent's primary key. *)
936936+fk_clause:
937937+ | REFERENCES; parent = ident; pcols = loption(paren_names);
938938+ acts = list(fk_ref_action)
939939+ { let pick sel =
940940+ List.fold_left (fun a x -> match sel x with Some v -> v | None -> a)
941941+ No_action acts
942942+ in
943943+ (parent, pcols,
944944+ pick (function `Del d -> Some d | `Upd _ -> None),
945945+ pick (function `Upd u -> Some u | `Del _ -> None)) }
946946+947947+fk_ref_action:
948948+ | ON; DELETE; a = fk_action { `Del a }
949949+ | ON; UPDATE; a = fk_action { `Upd a }
950950+951951+fk_action:
952952+ | SET; NULL { Set_null }
953953+ | SET; DEFAULT { Set_default }
954954+ | CASCADE { Cascade }
955955+ | RESTRICT { Restrict }
956956+ | NO; ACTION { No_action }
957957+958958+paren_names:
959959+ | LPAREN; cs = names; RPAREN { cs }
960960+961961+names:
962962+ | separated_nonempty_list(COMMA, ident) { $1 }
963963+964964+(* ── Column body: flat token list ────────────────────────────── *)
965965+966966+col_body:
967967+ | items = list(col_body_item)
968968+ { let toks =
969969+ List.filter_map
970970+ (function `T t -> Some t | `K _ | `D _ | `G _ -> None) items
971971+ in
972972+ let checks =
973973+ List.filter_map
974974+ (function `K k -> Some k | `T _ | `D _ | `G _ -> None) items
975975+ in
976976+ (* A column has at most one DEFAULT and one generation expr; keep the
977977+ first if repeated. *)
978978+ let default =
979979+ List.find_map (function `D e -> Some e | `T _ | `K _ | `G _ -> None) items
980980+ in
981981+ let generated =
982982+ List.find_map (function `G e -> Some e | `T _ | `K _ | `D _ -> None) items
983983+ in
984984+ (toks, checks, default, generated) }
985985+986986+col_body_item:
987987+ | t = col_token { `T t }
988988+ | k = col_check { `K k }
989989+ | e = col_default { `D e }
990990+ | e = col_generated { `G e }
991991+992992+(* A generated (computed) column: [[GENERATED ALWAYS] AS (expr) [VIRTUAL|STORED]].
993993+ A trailing STORED/VIRTUAL keyword is left to be collected as an opaque column
994994+ token and ignored -- both behave as stored here. *)
995995+col_generated:
996996+ | GENERATED; ALWAYS; AS; LPAREN; e = expr; RPAREN { e }
997997+ | AS; LPAREN; e = expr; RPAREN { e }
998998+999999+(* A column-level CHECK. Like the table-level form, the predicate is a real
10001000+ expression whose verbatim source offsets are captured for the error text. *)
10011001+col_check:
10021002+ | CHECK; LPAREN; e = expr; RPAREN
10031003+ { { expr = e; start = $startofs(e); stop = $endofs(e) } }
10041004+10051005+(* A column DEFAULT: a constant literal, a signed number, or a parenthesised
10061006+ expression. The parenthesised form is checked for constancy when the table is
10071007+ created (a column reference there is an error). *)
10081008+col_default:
10091009+ | DEFAULT; l = literal { Lit l }
10101010+ | DEFAULT; MINUS; n = NUMBER
10111011+ { Lit (match number_to_value n with
10121012+ | Int i -> Int (Int64.neg i)
10131013+ | Float f -> Float (-. f)
10141014+ | v -> v) }
10151015+ | DEFAULT; PLUS; n = NUMBER { Lit (number_to_value n) }
10161016+ | DEFAULT; LPAREN; e = expr; RPAREN { e }
10171017+10181018+%inline col_token:
10191019+ | s = IDENT { Word s }
10201020+ | s = NUMBER { Number s }
10211021+ | s = STRING { Number ("'" ^ s ^ "'") }
10221022+ | k = keyword { Word k }
10231023+ | p = punct { Word p }
10241024+ | LPAREN; ts = list(paren_token); RPAREN { Parens ts }
10251025+10261026+paren_token:
10271027+ | s = IDENT { Word s }
10281028+ | s = NUMBER { Number s }
10291029+ | s = STRING { Number ("'" ^ s ^ "'") }
10301030+ | k = keyword { Word k }
10311031+ | p = punct { Word p }
10321032+ | COMMA { Word "," }
10331033+ | LPAREN; ts = list(paren_token); RPAREN { Parens ts }
10341034+10351035+(* Operators that may appear inside CHECK / DEFAULT / GENERATED expressions.
10361036+ They are opaque to column classification, but must be accepted so the
10371037+ DDL parser does not choke on them. *)
10381038+%inline punct:
10391039+ | EQ { "=" }
10401040+ | NEQ { "<>" }
10411041+ | LT { "<" }
10421042+ | LE { "<=" }
10431043+ | GT { ">" }
10441044+ | GE { ">=" }
10451045+ | STAR { "*" }
10461046+ | DOT { "." }
10471047+ | PLUS { "+" }
10481048+ | MINUS { "-" }
10491049+ | SLASH { "/" }
10501050+ | PERCENT { "%" }
10511051+ | BITAND { "&" }
10521052+ | BITOR { "|" }
10531053+ | SHL { "<<" }
10541054+ | SHR { ">>" }
10551055+ | CONCAT { "||" }
10561056+10571057+(* ── Keywords usable as column body tokens ───────────────────── *)
10581058+10591059+%inline keyword:
10601060+ | s = PRIMARY { s }
10611061+ | s = KEY { s }
10621062+ | s = NOT { s }
10631063+ | s = NULL { s }
10641064+ | s = UNIQUE { s }
10651065+ | s = REFERENCES { s }
10661066+ | s = COLLATE { s }
10671067+ | s = AUTOINCREMENT { s }
10681068+ | s = CONSTRAINT { s }
10691069+ (* [ON CONFLICT <action>] on a column constraint: collected as opaque tokens
10701070+ so the column still parses (the action keywords ABORT/FAIL/IGNORE lex as
10711071+ IDENT; REPLACE/ROLLBACK/CONFLICT are keyword tokens). *)
10721072+ | s = CONFLICT { s }
10731073+ | s = REPLACE { s }
10741074+ | s = ROLLBACK { s }
10751075+ | s = EXISTS { s }
10761076+ | s = CREATE { s }
10771077+ | s = TABLE { s }
10781078+ | s = IF { s }
10791079+ | s = ON { s }
10801080+ | s = ASC { s }
10811081+ | s = DESC { s }
10821082+ | s = FOREIGN { s }
10831083+ | s = SELECT { s }
10841084+ | s = DISTINCT { s }
10851085+ | s = FROM { s }
10861086+ | s = WHERE { s }
10871087+ | s = JOIN { s }
10881088+ | s = INNER { s }
10891089+ | s = LEFT { s }
10901090+ | s = OUTER { s }
10911091+ | s = ORDER { s }
10921092+ | s = BY { s }
10931093+ | s = LIMIT { s }
10941094+ | s = AND { s }
10951095+ | s = OR { s }
10961096+ | s = IN { s }
10971097+ | s = IS { s }
10981098+ | s = LIKE { s }
10991099+11001100+(* ── Identifiers (including quoted) ──────────────────────────── *)
11011101+11021102+ident:
11031103+ | s = IDENT { s }
11041104+ | s = KEY { s }
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+open Ast
77+88+(* SQLite's value-level semantics (comparison order, collation, text conversion,
99+ aggregate folds) live in {!Value_ops}, shared with the register-VM paths;
1010+ open it so the tree-walker uses the same folds and orders by the same rule. *)
1111+open Value_ops
1212+1313+(* The built-in scalar / table-valued functions live in {!Func} (also shared
1414+ with the VM); open it so the tree-walker dispatches them through the same
1515+ value-level implementations. *)
1616+open Func
1717+1818+(* The value-level binary operators (comparison with affinity, AND/OR/LIKE/GLOB/
1919+ JSON, IN-list) live in {!Value_op}; open it so the tree-walker evaluates an
2020+ operator through the same shared implementation as the VM. *)
2121+open Value_op
2222+2323+type catalog = {
2424+ columns : string -> string list;
2525+ rows : string -> Catalog.Value.t list list;
2626+ rows_rowid : string -> (int64 option * Catalog.Value.t list) list;
2727+ (** Each row paired with its rowid, or [None] for a row source that has no
2828+ rowid (a view, a derived table, [VALUES]). Lets the executor resolve
2929+ the implicit [rowid]/[oid]/[_rowid_] column of a full table scan. *)
3030+ index_scan :
3131+ string ->
3232+ (string * Catalog.Value.t) list ->
3333+ (string * Catalog.Value.t list list) option;
3434+ (** [index_scan table eqs] returns [Some (index_name, rows)] if an index
3535+ on [table] serves the column-equality bindings [eqs] -- the rows whose
3636+ indexed columns match, by which the caller narrows a scan -- or [None]
3737+ to fall back to a full {!rows} scan. *)
3838+ index_for : string -> string list -> string option;
3939+ (** [index_for table cols] is the name of a unique index on [table] all of
4040+ whose columns are among [cols], if any -- the structural counterpart
4141+ of {!index_scan} (no values), used to explain the plan. *)
4242+ collation : string -> string -> string option;
4343+ (** [collation table col] is [col]'s declared [COLLATE] sequence
4444+ (uppercase) on [table], or [None] for the default binary / a derived
4545+ source. *)
4646+}
4747+4848+let of_catalog cat =
4949+ {
5050+ columns = Catalog.Spi.columns cat;
5151+ rows = Catalog.Spi.rows cat;
5252+ rows_rowid = Catalog.Spi.rows_rowid cat;
5353+ index_scan = Catalog.Spi.index_scan cat;
5454+ index_for = Catalog.Spi.index_for cat;
5555+ collation = Catalog.Spi.collation cat;
5656+ }
5757+5858+(* The runner [eval] reaches for a scalar / [IN] / [EXISTS] subquery, given the
5959+ subquery's enclosing-row [cat], its outer scope, the subquery, and the params.
6060+ Held in a private ref behind {!set_cat_subquery_runner} (the catalog VM that
6161+ fulfils it depends on this module, so it is injected, not called directly).
6262+ The default -- and a pure storage catalog -- is the tree-walker {!exec_select},
6363+ tied off once that is defined; the engine overrides it to run the subquery
6464+ over the {e same} [cat]'s rows on the catalog VM, falling back here. *)
6565+let cat_subquery_runner :
6666+ (catalog ->
6767+ (string * (string * Catalog.Value.t) list) list ->
6868+ Ast.select ->
6969+ Catalog.Value.t array ->
7070+ Catalog.Value.t list list)
7171+ ref =
7272+ ref (fun _ _ _ _ -> failwith "Query.cat_subquery_runner: not initialised")
7373+7474+let set_cat_subquery_runner f = cat_subquery_runner := f
7575+7676+(* ── Parameter numbering ─────────────────────────────────────── *)
7777+7878+(* Renumber [?] placeholders left-to-right. [next] is a monotonic counter; the
7979+ explicit [let] bindings force left-to-right traversal so placeholders are
8080+ numbered in source order regardless of OCaml's argument evaluation. *)
8181+let rec renum_expr next = function
8282+ | Param _ -> Param (next ())
8383+ | (Lit _ | Col _ | Star | Qualified_star _) as e -> e
8484+ | Unop_not e -> Unop_not (renum_expr next e)
8585+ | Binary (op, a, b) ->
8686+ let a = renum_expr next a in
8787+ let b = renum_expr next b in
8888+ Binary (op, a, b)
8989+ | Is_null e -> Is_null (renum_expr next e)
9090+ | Is_not_null e -> Is_not_null (renum_expr next e)
9191+ | Is (a, b) ->
9292+ let a = renum_expr next a in
9393+ Is (a, renum_expr next b)
9494+ | In_list (e, l) ->
9595+ let e = renum_expr next e in
9696+ In_list (e, renum_exprs next l)
9797+ | In_select (e, s) ->
9898+ let e = renum_expr next e in
9999+ In_select (e, renum_select next s)
100100+ | Scalar_select s -> Scalar_select (renum_select next s)
101101+ | Exists s -> Exists (renum_select next s)
102102+ | Func (n, args) -> Func (n, renum_exprs next args)
103103+ | Distinct_agg (n, e) -> Distinct_agg (n, renum_expr next e)
104104+ | Filter (a, p) -> Filter (renum_expr next a, renum_expr next p)
105105+ | Ordered_agg (a, ob) ->
106106+ Ordered_agg
107107+ (renum_expr next a, List.map (fun (e, d) -> (renum_expr next e, d)) ob)
108108+ | Window (a, w) ->
109109+ let bound = function
110110+ | (Unbounded_preceding | Current_row | Unbounded_following) as b -> b
111111+ | Preceding e -> Preceding (renum_expr next e)
112112+ | Following e -> Following (renum_expr next e)
113113+ in
114114+ Window
115115+ ( renum_expr next a,
116116+ {
117117+ partition = List.map (renum_expr next) w.partition;
118118+ order = List.map (fun (e, d) -> (renum_expr next e, d)) w.order;
119119+ frame =
120120+ Option.map
121121+ (fun (f : frame) ->
122122+ { f with start = bound f.start; end_ = bound f.end_ })
123123+ w.frame;
124124+ } )
125125+ | Cast (e, ty) -> Cast (renum_expr next e, ty)
126126+ | Collate (e, c) -> Collate (renum_expr next e, c)
127127+ | Case (base, branches, els) ->
128128+ let base = Option.map (renum_expr next) base in
129129+ let branches =
130130+ List.map
131131+ (fun (c, r) ->
132132+ let c = renum_expr next c in
133133+ (c, renum_expr next r))
134134+ branches
135135+ in
136136+ let els = Option.map (renum_expr next) els in
137137+ Case (base, branches, els)
138138+139139+and renum_exprs next = function
140140+ | [] -> []
141141+ | e :: rest ->
142142+ let e = renum_expr next e in
143143+ e :: renum_exprs next rest
144144+145145+(* Source order: SELECT cols, FROM (subquery), JOINs, WHERE, ORDER BY, LIMIT. *)
146146+and renum_select next (s : select) =
147147+ let ctes =
148148+ List.map
149149+ (fun (c : cte) -> { c with query = renum_select next c.query })
150150+ s.ctes
151151+ in
152152+ let cols =
153153+ List.map
154154+ (fun (rc : result_col) -> { rc with expr = renum_expr next rc.expr })
155155+ s.cols
156156+ in
157157+ let from =
158158+ match s.from with
159159+ | (Unit | Table _) as f -> f
160160+ | Select fs -> Select { fs with fs_select = renum_select next fs.fs_select }
161161+ | Values rows -> Values (List.map (List.map (renum_expr next)) rows)
162162+ | Table_function tf ->
163163+ Table_function
164164+ { tf with tf_args = List.map (renum_expr next) tf.tf_args }
165165+ in
166166+ let joins =
167167+ List.map
168168+ (fun (j : join) ->
169169+ let table =
170170+ match j.table.subquery with
171171+ | Some q -> { j.table with subquery = Some (renum_select next q) }
172172+ | None -> j.table
173173+ in
174174+ { j with table; on = renum_expr next j.on })
175175+ s.joins
176176+ in
177177+ let where = Option.map (renum_expr next) s.where in
178178+ let group_by = List.map (renum_expr next) s.group_by in
179179+ let having = Option.map (renum_expr next) s.having in
180180+ let compound =
181181+ List.map (fun (op, arm) -> (op, renum_select next arm)) s.compound
182182+ in
183183+ let order_by = List.map (fun (e, d) -> (renum_expr next e, d)) s.order_by in
184184+ let limit = Option.map (renum_expr next) s.limit in
185185+ let offset = Option.map (renum_expr next) s.offset in
186186+ {
187187+ s with
188188+ ctes;
189189+ cols;
190190+ from;
191191+ joins;
192192+ where;
193193+ group_by;
194194+ having;
195195+ compound;
196196+ order_by;
197197+ limit;
198198+ offset;
199199+ }
200200+201201+let number_params stmt =
202202+ let counter = ref 0 in
203203+ let next () =
204204+ let i = !counter in
205205+ incr counter;
206206+ i
207207+ in
208208+ let e = renum_expr next and es = renum_exprs next in
209209+ let rcs l =
210210+ List.map (fun (rc : result_col) -> { rc with expr = e rc.expr }) l
211211+ in
212212+ match stmt with
213213+ | Select s -> Select (renum_select next s)
214214+ | Insert (i : insert) ->
215215+ let source =
216216+ match i.source with
217217+ | Values rows -> Values (List.map es rows)
218218+ | Select s -> Select (renum_select next s)
219219+ | Default_values -> Default_values
220220+ in
221221+ let upsert =
222222+ Option.map
223223+ (fun (u : upsert) ->
224224+ match u.action with
225225+ | Nothing -> u
226226+ | Update (sets, w) ->
227227+ let sets = List.map (fun (c, x) -> (c, e x)) sets in
228228+ { u with action = Update (sets, Option.map e w) })
229229+ i.upsert
230230+ in
231231+ let returning = rcs i.returning in
232232+ Insert { i with source; upsert; returning }
233233+ | Delete d ->
234234+ let where = Option.map e d.where in
235235+ Delete { d with where; returning = rcs d.returning }
236236+ | Update u ->
237237+ let sets = List.map (fun (c, x) -> (c, e x)) u.sets in
238238+ let where = Option.map e u.where in
239239+ Update { u with sets; where; returning = rcs u.returning }
240240+ | Create_table_as cta ->
241241+ Create_table_as { cta with query = renum_select next cta.query }
242242+ | ( Create_table _ | Create_index _ | Create_view _ | Create_trigger _
243243+ | Drop _ | Alter_table _ | Pragma _ | Begin | Commit | Rollback
244244+ | Savepoint _ | Release _ | Rollback_to _ | Vacuum | Analyze ) as s ->
245245+ s
246246+247247+(* ── Values ──────────────────────────────────────────────────── *)
248248+249249+(* ── Three-valued logic ──────────────────────────────────────── *)
250250+251251+(* The collating sequence an operand assigns by a top-level [COLLATE], if any.
252252+ In a comparison the left operand's explicit collation wins, else the right's
253253+ (datatype3.html sec.7.1). Column-declared collation is not yet threaded here. *)
254254+let explicit_collation = function Collate (_, c) -> Some c | _ -> None
255255+256256+(* SQLite integer coercion (as the bitwise/modulo operators apply it): an
257257+ integer stays itself, a real truncates toward zero, text parses its leading
258258+ numeric prefix (else 0), NULL is handled by the caller. *)
259259+260260+(* [a << b] / [a >> b]: a negative shift count shifts the other way; a count of
261261+ 64 or more clears the value, matching sqlite3. *)
262262+let shift dir x n =
263263+ let n = Int64.to_int n in
264264+ let lsl_ x n = if n >= 64 then 0L else Int64.shift_left x n in
265265+ (* arithmetic right shift, so a negative x keeps its sign, as in sqlite3 *)
266266+ let asr_ x n =
267267+ if n >= 64 then if x < 0L then -1L else 0L else Int64.shift_right x n
268268+ in
269269+ match dir with
270270+ | `Left -> if n >= 0 then lsl_ x n else asr_ x (-n)
271271+ | `Right -> if n >= 0 then asr_ x n else lsl_ x (-n)
272272+273273+(* SQLite arithmetic: NULL is contagious; integer op integer stays integer
274274+ (with [/] truncating and division-by-zero giving NULL); any real operand, or
275275+ a text operand coerced to a number, makes the result real. The bitwise and
276276+ modulo operators always work on integers (operands coerced via {!intify}). *)
277277+(* Does this operand carry REAL affinity? A real literal, or text that parses as
278278+ a real but not an integer. Mod is computed on integers but returns a real when
279279+ either operand is real, as sqlite3 does. *)
280280+(* Add/Sub/Mul/Div: integer when both operands are integers ([/] truncates, /0
281281+ is NULL), otherwise real (text operands coerced to a number). *)
282282+(* SQLite computes an integer Add/Sub/Mul/Div that would overflow [int64] as
283283+ floating point instead of wrapping (e.g. [9223372036854775807 + 1] is a real).
284284+ Each op detects overflow on the native result and falls back to a float. *)
285285+let add_checked x y =
286286+ let r = Int64.add x y in
287287+ (* overflow iff the operands share a sign and the result's sign flips *)
288288+ if x >= 0L = (y >= 0L) && r >= 0L <> (x >= 0L) then
289289+ Float (Int64.to_float x +. Int64.to_float y)
290290+ else Int r
291291+292292+let sub_checked x y =
293293+ let r = Int64.sub x y in
294294+ if x >= 0L <> (y >= 0L) && r >= 0L <> (x >= 0L) then
295295+ Float (Int64.to_float x -. Int64.to_float y)
296296+ else Int r
297297+298298+let mul_checked x y =
299299+ let r = Int64.mul x y in
300300+ (* check the special [min_int * -1] first so the div below never hits it *)
301301+ let overflow =
302302+ x <> 0L && ((x = -1L && y = Int64.min_int) || Int64.div r x <> y)
303303+ in
304304+ if overflow then Float (Int64.to_float x *. Int64.to_float y) else Int r
305305+306306+let arith_int op x y =
307307+ match op with
308308+ | Add -> add_checked x y
309309+ | Sub -> sub_checked x y
310310+ | Mul -> mul_checked x y
311311+ | Div ->
312312+ if y = 0L then Null
313313+ else if x = Int64.min_int && y = -1L then
314314+ Float (Int64.to_float x /. Int64.to_float y)
315315+ else Int (Int64.div x y)
316316+ | _ -> assert false
317317+318318+let arith_float op a b =
319319+ let f = function
320320+ | Int i -> Int64.to_float i
321321+ | Float f -> f
322322+ | v -> (
323323+ match float_of_string_opt (String.trim (text_of_value v)) with
324324+ | Some f -> f
325325+ | None -> 0.)
326326+ in
327327+ let x = f a and y = f b in
328328+ match op with
329329+ | Add -> Float (x +. y)
330330+ | Sub -> Float (x -. y)
331331+ | Mul -> Float (x *. y)
332332+ | Div -> if y = 0. then Null else Float (x /. y)
333333+ | _ -> assert false
334334+335335+let arith op a b =
336336+ if a = Null || b = Null then Null
337337+ else
338338+ match op with
339339+ | Mod ->
340340+ let y = intify b in
341341+ if y = 0L then Null
342342+ else
343343+ let r = Int64.rem (intify a) y in
344344+ if is_real a || is_real b then Float (Int64.to_float r) else Int r
345345+ | Bit_and -> Int (Int64.logand (intify a) (intify b))
346346+ | Bit_or -> Int (Int64.logor (intify a) (intify b))
347347+ | Shl -> Int (shift `Left (intify a) (intify b))
348348+ | Shr -> Int (shift `Right (intify a) (intify b))
349349+ | _ -> (
350350+ (* Apply numeric affinity to text operands first, so '5'+2 is integer
351351+ arithmetic (7) while '5.5'+2 is real (7.5), matching sqlite3. *)
352352+ match (numeric_affinity a, numeric_affinity b) with
353353+ | Int x, Int y -> arith_int op x y
354354+ | a, b -> arith_float op a b)
355355+356356+(* ── Environments and lookup ─────────────────────────────────── *)
357357+358358+(* A [binding] is one named row fragment: an (alias, [(column, value)]) pair --
359359+ the public form used for an outer [scope] (a correlated subquery's enclosing
360360+ row, or a trigger's OLD/NEW rows). *)
361361+type binding = string * (string * value) list
362362+363363+(* Internally a row is a list of [frame]s -- one per table joined in, plus the
364364+ scope at the tail so inner tables shadow it for unqualified names. A table row
365365+ is a [Pos] frame: the alias, the shared column-name list (one allocation,
366366+ reused across every row of that table), and the row's values -- so building a
367367+ row's environment costs a 3-word record, not an n-pair association list. A
368368+ scope binding becomes an [Assoc] frame. *)
369369+(* A [Pos] frame carries the optional rowid of a real-table row (for the
370370+ implicit [rowid]/[oid]/[_rowid_] column); [None] for an index-narrowed scan,
371371+ a derived table, or a scope binding, where rowid is not resolvable. *)
372372+type frame =
373373+ | Pos of string * string list * value list * int64 option
374374+ | Assoc of binding
375375+376376+type env = frame list
377377+378378+(* The [rowid] pseudo-column and its two aliases, matched case-insensitively. A
379379+ real column of the same name shadows the implicit one, so this is consulted
380380+ only after the declared columns. *)
381381+let is_rowid_name n =
382382+ match String.lowercase_ascii n with
383383+ | "rowid" | "_rowid_" | "oid" -> true
384384+ | _ -> false
385385+386386+(* Parallel-list lookup: the value paired with [name] in [cols]/[vals] (short
387387+ [vals] reads as trailing NULLs), or [None]. Column names match exactly, as the
388388+ association-list lookup it replaces did. *)
389389+let rec assoc2 name cols vals =
390390+ match (cols, vals) with
391391+ | c :: cs, v :: vs -> if c = name then Some v else assoc2 name cs vs
392392+ | c :: cs, [] -> if c = name then Some Null else assoc2 name cs []
393393+ | [], _ -> None
394394+395395+let frame_alias = function Pos (a, _, _, _) | Assoc (a, _) -> a
396396+397397+let frame_lookup name = function
398398+ | Pos (_, cols, vals, rid) -> (
399399+ match assoc2 name cols vals with
400400+ | Some _ as r -> r
401401+ | None -> (
402402+ match rid with
403403+ | Some r when is_rowid_name name -> Some (Int r)
404404+ | _ -> None))
405405+ | Assoc (_, row) -> List.assoc_opt name row
406406+407407+(* The frame's values in column order -- the expansion of [alias.*]. The rowid is
408408+ a hidden column and never part of [*]. *)
409409+let frame_values = function
410410+ | Pos (_, _, vals, _) -> vals
411411+ | Assoc (_, row) -> List.map snd row
412412+413413+(* The same frame with every value NULL -- the inner side of an unmatched outer
414414+ join row. The synthetic row has no rowid. *)
415415+let null_pad_frame = function
416416+ | Pos (a, cols, vals, _) -> Pos (a, cols, List.map (fun _ -> Null) vals, None)
417417+ | Assoc (a, row) -> Assoc (a, List.map (fun (c, _) -> (c, Null)) row)
418418+419419+(* Drop the [drop] columns from a frame -- the NATURAL/USING common columns,
420420+ which survive on the left side and so are listed once. *)
421421+let trim_frame drop = function
422422+ | Pos (a, cols, vals, rid) ->
423423+ let keep =
424424+ List.filter
425425+ (fun (c, _) -> not (List.mem c drop))
426426+ (List.combine cols vals)
427427+ in
428428+ Pos (a, List.map fst keep, List.map snd keep, rid)
429429+ | Assoc (a, row) ->
430430+ Assoc (a, List.filter (fun (c, _) -> not (List.mem c drop)) row)
431431+432432+let frames_of_scope (scope : binding list) : env =
433433+ List.map (fun b -> Assoc b) scope
434434+435435+(* Materialise an environment as public [binding]s -- used only to hand the
436436+ current row to a correlated subquery as its outer scope. Re-pairs a [Pos]
437437+ frame's columns with its values (short values read as trailing NULLs); this is
438438+ the one place a row's association list is rebuilt, and only per outer row, not
439439+ per scanned row. *)
440440+let bindings_of_frames (env : env) : binding list =
441441+ let rec pairs cols vals =
442442+ match (cols, vals) with
443443+ | c :: cs, v :: vs -> (c, v) :: pairs cs vs
444444+ | c :: cs, [] -> (c, Null) :: pairs cs []
445445+ | [], _ -> []
446446+ in
447447+ List.map
448448+ (function Pos (a, cols, vals, _) -> (a, pairs cols vals) | Assoc b -> b)
449449+ env
450450+451451+(* These walkers are top-level (capturing nothing) so a column lookup -- run once
452452+ per [Col] per row -- allocates neither a closure nor a [lazy] cell. *)
453453+let rec aliased q = function
454454+ | [] -> None
455455+ | f :: rest -> if frame_alias f = q then Some f else aliased q rest
456456+457457+let rec aliased_ci qu = function
458458+ | [] -> None
459459+ | f :: rest ->
460460+ if String.uppercase_ascii (frame_alias f) = qu then Some f
461461+ else aliased_ci qu rest
462462+463463+let rec lookup_unqual name = function
464464+ | [] -> Fmt.failwith "unknown column %S" name
465465+ | f :: rest -> (
466466+ match frame_lookup name f with
467467+ | Some v -> v
468468+ | None -> lookup_unqual name rest)
469469+470470+let lookup env qual name =
471471+ match qual with
472472+ | Some q -> (
473473+ (* Exact alias first; then a case-insensitive match (the uppercase form is
474474+ built only if the exact match fails), since SQL identifiers (and the
475475+ OLD/NEW pseudo-tables, written in any case) are case-insensitive. *)
476476+ let frame =
477477+ match aliased q env with
478478+ | Some _ as r -> r
479479+ | None -> aliased_ci (String.uppercase_ascii q) env
480480+ in
481481+ match frame with
482482+ | None -> Fmt.failwith "unknown table %S in %s.%s" q q name
483483+ | Some f -> (
484484+ match frame_lookup name f with
485485+ | Some v -> v
486486+ | None -> Fmt.failwith "unknown column %s.%s" q name))
487487+ | None -> lookup_unqual name env
488488+489489+(* ── Evaluation ──────────────────────────────────────────────── *)
490490+491491+let frag cat (tr : table_ref) =
492492+ let alias = Option.value tr.alias ~default:tr.name in
493493+ let cols = cat.columns tr.name in
494494+ (alias, cols)
495495+496496+(* An all-NULL environment carrying the FROM tables' column names. It resolves a
497497+ bare column over an EMPTY aggregate group (no rows): the aggregate is still
498498+ folded over zero rows, but a bare column reads as NULL -- as sqlite3 does for
499499+ an aggregate select list (or a HAVING) that names a column on an empty
500500+ table. *)
501501+let schema_env cat (s : select) : env =
502502+ (* Best-effort: a derived/subquery operand whose columns are not in the base
503503+ catalog is skipped (its bare columns simply stay unresolved, as before). *)
504504+ let frame tr =
505505+ match frag cat tr with
506506+ | alias, cols ->
507507+ Some (Pos (alias, cols, List.map (fun _ -> Null) cols, None))
508508+ | exception Failure _ -> None
509509+ in
510510+ let from_frames =
511511+ match s.from with Table tr -> Option.to_list (frame tr) | _ -> []
512512+ in
513513+ from_frames @ List.filter_map (fun (j : join) -> frame j.table) s.joins
514514+515515+(* [max]/[min] are aggregates only in their one-argument form; [max(a,b,...)]
516516+ and [min(a,b,...)] are scalar functions over their arguments. *)
517517+let is_agg name args =
518518+ match name with
519519+ | "count" | "sum" | "avg" | "total" | "group_concat" | "string_agg" -> true
520520+ | "min" | "max" -> List.length args <= 1
521521+ | _ -> false
522522+523523+(* Is [e] an aggregate call, possibly wrapped in FILTER/ORDER BY decorations? *)
524524+let rec is_agg_expr = function
525525+ | Func (n, a) -> is_agg n a
526526+ | Distinct_agg _ -> true
527527+ | Filter (e, _) | Ordered_agg (e, _) -> is_agg_expr e
528528+ | _ -> false
529529+530530+(* Does [e] contain an aggregate of {e this} query anywhere? Unlike
531531+ {!is_agg_expr} this looks inside ordinary sub-expressions ([f(a, max(b))],
532532+ [a + count(c)]) but NOT into a subquery -- an aggregate there is the inner
533533+ query's, so it does not make the enclosing query an aggregate query. *)
534534+let rec contains_agg = function
535535+ | Func (n, args) -> is_agg n args || List.exists contains_agg args
536536+ | Distinct_agg _ | Filter _ | Ordered_agg _ -> true
537537+ | Binary (_, a, b) | Is (a, b) -> contains_agg a || contains_agg b
538538+ | Unop_not a | Is_null a | Is_not_null a | Cast (a, _) | Collate (a, _) ->
539539+ contains_agg a
540540+ | In_list (a, l) -> contains_agg a || List.exists contains_agg l
541541+ | In_select (a, _) -> contains_agg a
542542+ | Case (base, brs, els) ->
543543+ Option.fold ~none:false ~some:contains_agg base
544544+ || List.exists (fun (c, r) -> contains_agg c || contains_agg r) brs
545545+ || Option.fold ~none:false ~some:contains_agg els
546546+ | Window _ | Scalar_select _ | Exists _ | Lit _ | Col _ | Param _ | Star
547547+ | Qualified_star _ ->
548548+ false
549549+550550+let is_aggregate (rc : Ast.result_col) = contains_agg rc.expr
551551+552552+let dedup rows =
553553+ let seen = Hashtbl.create 64 in
554554+ List.filter
555555+ (fun r ->
556556+ if Hashtbl.mem seen r then false
557557+ else (
558558+ Hashtbl.add seen r ();
559559+ true))
560560+ rows
561561+562562+(* DISTINCT keyed by each output column's collation: a text value is reduced to
563563+ its [collate_key] so two rows differing only in case (under NOCASE) or
564564+ trailing spaces (under RTRIM) count as one. [colls] is one entry per output
565565+ column ([None] = the default BINARY). The first row of each class is kept. *)
566566+let dedup_coll colls rows =
567567+ let key row =
568568+ List.map2
569569+ (fun coll v ->
570570+ match (coll, v) with
571571+ | Some c, Ast.Text s -> collation_key c (Ast.Text s)
572572+ | _ -> v)
573573+ colls row
574574+ in
575575+ let seen = Hashtbl.create 64 in
576576+ List.filter
577577+ (fun r ->
578578+ let k = key r in
579579+ if Hashtbl.mem seen k then false
580580+ else (
581581+ Hashtbl.add seen k ();
582582+ true))
583583+ rows
584584+585585+(* DISTINCT over the output [cols]: when a column carries an explicit [COLLATE]
586586+ the dedup honours it ({!dedup_coll}); the common all-binary case (and a [*]
587587+ whose width does not line up with [cols]) takes the plain {!dedup}. *)
588588+let distinct_dedup cols rows =
589589+ let colls =
590590+ List.map (fun (rc : Ast.result_col) -> explicit_collation rc.expr) cols
591591+ in
592592+ let is_star (rc : Ast.result_col) =
593593+ match rc.expr with Star | Qualified_star _ -> true | _ -> false
594594+ in
595595+ let width_ok =
596596+ match rows with r :: _ -> List.length r = List.length colls | [] -> true
597597+ in
598598+ if
599599+ List.exists is_star cols
600600+ || (not (List.exists Option.is_some colls))
601601+ || not width_ok
602602+ then dedup rows
603603+ else dedup_coll colls rows
604604+605605+let take n rows =
606606+ let rec go n = function
607607+ | _ when n <= 0 -> []
608608+ | [] -> []
609609+ | x :: rest -> x :: go (n - 1) rest
610610+ in
611611+ go n rows
612612+613613+let rec drop n = function
614614+ | rows when n <= 0 -> rows
615615+ | [] -> []
616616+ | _ :: rest -> drop (n - 1) rest
617617+618618+(* ── Index access ────────────────────────────────────────────── *)
619619+620620+(* Top-level ANDed equalities [col = const] on table alias [alias], with the
621621+ right-hand side resolved to a constant (a literal or a bound parameter).
622622+ These are the predicates an index can drive; anything else is left to the
623623+ row filter. *)
624624+let collect_eqs params alias where =
625625+ let const = function
626626+ | Lit v -> Some v
627627+ | Param i -> if i < Array.length params then Some params.(i) else None
628628+ | _ -> None
629629+ in
630630+ let here = function None -> true | Some q -> q = alias in
631631+ let rec go acc = function
632632+ | Binary (And, a, b) -> go (go acc a) b
633633+ | Binary (Eq, Col (q, c), rhs) when here q -> (
634634+ match const rhs with Some v -> (c, v) :: acc | None -> acc)
635635+ | Binary (Eq, lhs, Col (q, c)) when here q -> (
636636+ match const lhs with Some v -> (c, v) :: acc | None -> acc)
637637+ | _ -> acc
638638+ in
639639+ match where with None -> [] | Some w -> go [] w
640640+641641+(* The columns of [alias] equated to a constant (literal or parameter) at top
642642+ level -- the structural view of {!collect_eqs}, independent of any parameter
643643+ values, used to explain the plan. *)
644644+let eq_columns alias where =
645645+ let is_const = function Lit _ | Param _ -> true | _ -> false in
646646+ let here = function None -> true | Some q -> q = alias in
647647+ let rec go acc = function
648648+ | Binary (And, a, b) -> go (go acc a) b
649649+ | Binary (Eq, Col (q, c), e) when here q && is_const e -> c :: acc
650650+ | Binary (Eq, e, Col (q, c)) when here q && is_const e -> c :: acc
651651+ | _ -> acc
652652+ in
653653+ match where with None -> [] | Some w -> go [] w
654654+655655+(* Does [e] reference table alias [a]? A join's inner table can be looked up by
656656+ index only when the other side of the equality does not. *)
657657+let rec mentions a = function
658658+ | Col (Some q, _) | Qualified_star q -> q = a
659659+ | Col (None, _) | Lit _ | Param _ | Star -> false
660660+ | Unop_not e | Is_null e | Is_not_null e -> mentions a e
661661+ | Binary (_, x, y) | Is (x, y) -> mentions a x || mentions a y
662662+ | In_list (e, l) -> mentions a e || List.exists (mentions a) l
663663+ | In_select (e, _) -> mentions a e
664664+ | Scalar_select _ | Exists _ -> false
665665+ | Collate (e, _) -> mentions a e
666666+ | Func (_, args) -> List.exists (mentions a) args
667667+ | Distinct_agg (_, e) -> mentions a e
668668+ | Filter (x, p) -> mentions a x || mentions a p
669669+ | Ordered_agg (x, ob) ->
670670+ mentions a x || List.exists (fun (e, _) -> mentions a e) ob
671671+ | Window (x, w) ->
672672+ mentions a x
673673+ || List.exists (mentions a) w.partition
674674+ || List.exists (fun (e, _) -> mentions a e) w.order
675675+ | Cast (e, _) -> mentions a e
676676+ | Case (base, branches, els) ->
677677+ Option.fold ~none:false ~some:(mentions a) base
678678+ || List.exists (fun (c, r) -> mentions a c || mentions a r) branches
679679+ || Option.fold ~none:false ~some:(mentions a) els
680680+681681+(* Columns of the join's inner table [ialias] equated, in [on], to something
682682+ that does not reference the inner table -- the structural view used to
683683+ explain an index-driven join. *)
684684+let join_eq_columns ialias on =
685685+ let rec go acc = function
686686+ | Binary (And, a, b) -> go (go acc a) b
687687+ | Binary (Eq, Col (Some q, c), e) when q = ialias && not (mentions ialias e)
688688+ ->
689689+ c :: acc
690690+ | Binary (Eq, e, Col (Some q, c)) when q = ialias && not (mentions ialias e)
691691+ ->
692692+ c :: acc
693693+ | _ -> acc
694694+ in
695695+ go [] on
696696+697697+(* The inner column -> outer expression map of a join's ON equalities: like
698698+ {!join_eq_columns} but keeping the other side of each equality, so a hash
699699+ probe key can be precomputed once and only evaluated per outer row. *)
700700+let join_probe_map ialias on =
701701+ let rec go acc = function
702702+ | Binary (And, a, b) -> go (go acc a) b
703703+ | Binary (Eq, Col (Some q, c), e) when q = ialias && not (mentions ialias e)
704704+ ->
705705+ (c, e) :: acc
706706+ | Binary (Eq, e, Col (Some q, c)) when q = ialias && not (mentions ialias e)
707707+ ->
708708+ (c, e) :: acc
709709+ | _ -> acc
710710+ in
711711+ go [] on
712712+713713+(* Every table alias a join's ON clause references. A join can only run once the
714714+ relations it mentions are already in scope, so this bounds reordering. *)
715715+let rec on_aliases = function
716716+ | Col (Some q, _) | Qualified_star q -> [ q ]
717717+ | Col (None, _) | Lit _ | Param _ | Star -> []
718718+ | Unop_not e | Is_null e | Is_not_null e | Cast (e, _) -> on_aliases e
719719+ | Binary (_, a, b) | Is (a, b) -> on_aliases a @ on_aliases b
720720+ | In_list (e, l) -> on_aliases e @ List.concat_map on_aliases l
721721+ | In_select (e, _) -> on_aliases e
722722+ | Scalar_select _ | Exists _ -> []
723723+ | Collate (e, _) -> on_aliases e
724724+ | Func (_, args) -> List.concat_map on_aliases args
725725+ | Distinct_agg (_, e) -> on_aliases e
726726+ | Filter (x, p) -> on_aliases x @ on_aliases p
727727+ | Ordered_agg (x, ob) ->
728728+ on_aliases x @ List.concat_map (fun (e, _) -> on_aliases e) ob
729729+ | Window (x, w) ->
730730+ on_aliases x
731731+ @ List.concat_map on_aliases w.partition
732732+ @ List.concat_map (fun (e, _) -> on_aliases e) w.order
733733+ | Case (b, brs, el) ->
734734+ Option.fold ~none:[] ~some:on_aliases b
735735+ @ List.concat_map (fun (c, r) -> on_aliases c @ on_aliases r) brs
736736+ @ Option.fold ~none:[] ~some:on_aliases el
737737+738738+let join_alias (j : join) = Option.value j.table.alias ~default:j.table.name
739739+740740+(* sqlite3 lets a [WHERE] or [ORDER BY] reference a select-list alias ([SELECT a
741741+ AS x ... WHERE x>1 ORDER BY x] resolves x to a). Substitute each such alias
742742+ with the expression it names, unless a real column of a scanned table shadows
743743+ it (a real column wins). Subqueries are not descended into -- they have their
744744+ own scope. An aggregate alias substituted into WHERE then raises, as sqlite3
745745+ does (an aggregate is not allowed in WHERE). *)
746746+let resolve_where_aliases cat (s : select) =
747747+ match (s.where, s.order_by) with
748748+ | None, [] -> s
749749+ | _ ->
750750+ let table_cols (tr : table_ref) =
751751+ match tr.subquery with None -> cat.columns tr.name | Some _ -> []
752752+ in
753753+ let real_cols =
754754+ (match s.from with Table tr -> table_cols tr | _ -> [])
755755+ @ List.concat_map (fun (j : join) -> table_cols j.table) s.joins
756756+ in
757757+ let aliases =
758758+ List.filter_map
759759+ (fun (rc : result_col) ->
760760+ match rc.alias with
761761+ | Some a when not (List.mem a real_cols) -> Some (a, rc.expr)
762762+ | _ -> None)
763763+ s.cols
764764+ in
765765+ if aliases = [] then s
766766+ else
767767+ let rec sub e =
768768+ match e with
769769+ | Col (None, name) -> (
770770+ match List.assoc_opt name aliases with Some e' -> e' | None -> e)
771771+ | Binary (op, a, b) -> Binary (op, sub a, sub b)
772772+ | Unop_not a -> Unop_not (sub a)
773773+ | Is_null a -> Is_null (sub a)
774774+ | Is_not_null a -> Is_not_null (sub a)
775775+ | Is (a, b) -> Is (sub a, sub b)
776776+ | In_list (a, l) -> In_list (sub a, List.map sub l)
777777+ | Cast (a, t) -> Cast (sub a, t)
778778+ | Collate (a, c) -> Collate (sub a, c)
779779+ | Func (n, args) -> Func (n, List.map sub args)
780780+ | Case (b, brs, el) ->
781781+ Case
782782+ ( Option.map sub b,
783783+ List.map (fun (c, r) -> (sub c, sub r)) brs,
784784+ Option.map sub el )
785785+ | Distinct_agg (n, a) -> Distinct_agg (n, sub a)
786786+ | Filter (a, b) -> Filter (sub a, sub b)
787787+ | Ordered_agg (a, ob) ->
788788+ Ordered_agg (sub a, List.map (fun (e, d) -> (sub e, d)) ob)
789789+ | Lit _ | Col _ | Param _ | Star | Qualified_star _ | In_select _
790790+ | Scalar_select _ | Exists _ | Window _ ->
791791+ e
792792+ in
793793+ {
794794+ s with
795795+ where = Option.map sub s.where;
796796+ order_by = List.map (fun (e, d) -> (sub e, d)) s.order_by;
797797+ }
798798+799799+(* Cost-based join ordering (Leis et al., VLDB 2015: join ordering dominates).
800800+ Inner joins commute, so reorder them cheapest-first by an exact-cardinality
801801+ cost model -- an index-covered ON is a lookup (cost 0), otherwise the inner
802802+ table's row count -- subject to each join's ON dependencies being in scope.
803803+ Outer joins are not reorderable, so a query containing one keeps FROM order. *)
804804+let plan_joins cat from_alias joins =
805805+ if joins = [] || List.exists (fun j -> j.type_ <> Inner || j.natural) joins
806806+ then joins
807807+ else
808808+ let cost j =
809809+ let eq = join_eq_columns (join_alias j) j.on in
810810+ if eq <> [] && cat.index_for j.table.name eq <> None then 0
811811+ else List.length (cat.rows j.table.name)
812812+ in
813813+ let ready avail j =
814814+ List.for_all
815815+ (fun q -> q = join_alias j || List.mem q avail)
816816+ (on_aliases j.on)
817817+ in
818818+ let rec go avail remaining acc =
819819+ match remaining with
820820+ | [] -> List.rev acc
821821+ | _ ->
822822+ let candidates =
823823+ match List.filter (ready avail) remaining with
824824+ | [] -> remaining (* a cross/unsatisfiable join: do not stall *)
825825+ | r -> r
826826+ in
827827+ let best =
828828+ List.fold_left
829829+ (fun b j -> if cost j < cost b then j else b)
830830+ (List.hd candidates) (List.tl candidates)
831831+ in
832832+ go (join_alias best :: avail)
833833+ (List.filter (fun j -> j != best) remaining)
834834+ (best :: acc)
835835+ in
836836+ go [ from_alias ] joins []
837837+838838+(* The chosen access path for the [from] table: an index narrowing when one
839839+ covers the equalities, else a full scan. The narrowed rows are still passed
840840+ through the row filter, so the choice is a pure optimisation. *)
841841+let from_access cat params (tr : table_ref) where =
842842+ let alias = Option.value tr.alias ~default:tr.name in
843843+ match collect_eqs params alias where with
844844+ | [] -> None
845845+ | eqs -> cat.index_scan tr.name eqs
846846+847847+(* The output column names of a select, for use as a derived table's columns:
848848+ the explicit alias, else the bare column name, else a positional name. A
849849+ [SELECT *] expands to every column in scope -- the FROM table plus each
850850+ joined table, recursing into a derived (subquery) operand. *)
851851+let rec select_columns cat s =
852852+ let table_cols (tr : table_ref) =
853853+ match tr.subquery with
854854+ | Some q -> select_columns cat q
855855+ | None -> cat.columns tr.name
856856+ in
857857+ let star_cols () =
858858+ let from_cols =
859859+ match s.from with
860860+ | Unit -> []
861861+ | Table tr -> table_cols tr
862862+ | Select { fs_select; _ } -> select_columns cat fs_select
863863+ | Values (r :: _) -> List.mapi (fun j _ -> Fmt.str "column%d" (j + 1)) r
864864+ | Values [] -> []
865865+ | Table_function { tf_name; _ } -> table_function_columns tf_name
866866+ in
867867+ from_cols @ List.concat_map (fun (j : join) -> table_cols j.table) s.joins
868868+ in
869869+ List.concat
870870+ (List.mapi
871871+ (fun i (rc : result_col) ->
872872+ match rc.expr with
873873+ | Star -> star_cols ()
874874+ | Qualified_star q -> cat.columns q
875875+ | _ -> (
876876+ match rc.alias with
877877+ | Some a -> [ a ]
878878+ | None -> (
879879+ match rc.expr with
880880+ | Col (_, c) -> [ c ]
881881+ | _ -> [ Fmt.str "column%d" (i + 1) ])))
882882+ s.cols)
883883+884884+(* The output expressions of [s] in order, expanding [*]/[t.*] to a column
885885+ reference per underlying column. Used to resolve an ORDER BY ordinal to the
886886+ expression of that output column. *)
887887+let output_exprs cat (s : select) =
888888+ let star () =
889889+ select_columns cat { s with cols = [ { expr = Star; alias = None } ] }
890890+ in
891891+ List.concat_map
892892+ (fun (rc : result_col) ->
893893+ match rc.expr with
894894+ | Star -> List.map (fun c -> Col (None, c)) (star ())
895895+ | Qualified_star q -> List.map (fun c -> Col (Some q, c)) (cat.columns q)
896896+ | e -> [ e ])
897897+ s.cols
898898+899899+(* sqlite3 lets ORDER BY and GROUP BY use a 1-based output-column ordinal
900900+ ([ORDER BY 2] / [GROUP BY 2] use the second result column). Replace each
901901+ integer-literal key with the expression of that output column, so the normal
902902+ machinery uses it; an out-of-range or non-integer key is left as is. *)
903903+let resolve_ordinals cat (s : select) =
904904+ if s.order_by = [] && s.group_by = [] then s
905905+ else
906906+ let outs = Array.of_list (output_exprs cat s) in
907907+ let n = Array.length outs in
908908+ (* A 1-based ordinal becomes its output expression; [ORDER BY k COLLATE c]
909909+ parses as a [Collate] around the ordinal, so look through it (the
910910+ collation stays, wrapping the resolved column). *)
911911+ let rec nth = function
912912+ | Lit (Int i) when i >= 1L && Int64.to_int i <= n ->
913913+ outs.(Int64.to_int i - 1)
914914+ | Collate (e, c) -> Collate (nth e, c)
915915+ | e -> e
916916+ in
917917+ (* An ORDER BY key may name an explicit output-column alias; the alias takes
918918+ precedence over an input column (sqlite3), so resolve it to that output
919919+ expression -- this is how [ORDER BY n] reaches an aggregate aliased [n]. *)
920920+ let alias_expr name =
921921+ List.find_map
922922+ (fun (rc : result_col) ->
923923+ match rc.alias with Some a when a = name -> Some rc.expr | _ -> None)
924924+ s.cols
925925+ in
926926+ let rec order_key = function
927927+ | Collate (e, c) -> Collate (order_key e, c)
928928+ | Col (None, name) as e -> (
929929+ match alias_expr name with Some x -> x | None -> nth e)
930930+ | e -> nth e
931931+ in
932932+ {
933933+ s with
934934+ order_by = List.map (fun (e, d) -> (order_key e, d)) s.order_by;
935935+ group_by = List.map nth s.group_by;
936936+ }
937937+938938+(* Apply each base-table column's declared COLLATE sequence to the comparisons of
939939+ [s]'s WHERE/ON/HAVING and to its ORDER BY keys, by wrapping the column in an
940940+ explicit [Collate] node -- which the existing collation machinery then honours.
941941+ sqlite3's rule: an explicit COLLATE on either operand wins; otherwise the left
942942+ operand's column collation, else the right's. Only bare columns of a scanned
943943+ base table carry a declared collation (a subquery/derived source has none). *)
944944+let resolve_collations cat (s : select) =
945945+ let bases =
946946+ let of_ref (tr : table_ref) =
947947+ match tr.subquery with
948948+ | None -> [ (Option.value tr.alias ~default:tr.name, tr.name) ]
949949+ | Some _ -> []
950950+ in
951951+ (match s.from with Table tr -> of_ref tr | _ -> [])
952952+ @ List.concat_map (fun (j : join) -> of_ref j.table) s.joins
953953+ in
954954+ if bases = [] then s
955955+ else
956956+ let col_coll = function
957957+ | Col (Some q, name) -> (
958958+ match List.assoc_opt q bases with
959959+ | Some tbl -> cat.collation tbl name
960960+ | None -> None)
961961+ | Col (None, name) -> (
962962+ (* [cat.columns] raises for a non-base source (a CTE not yet bound on
963963+ this catalog); such a source has no declared collation anyway. *)
964964+ let cols_of tbl = try cat.columns tbl with Failure _ -> [] in
965965+ match
966966+ List.find_opt (fun (_, tbl) -> List.mem name (cols_of tbl)) bases
967967+ with
968968+ | Some (_, tbl) -> cat.collation tbl name
969969+ | None -> None)
970970+ | _ -> None
971971+ in
972972+ let explicit = function Collate _ -> true | _ -> false in
973973+ let is_col = function Col _ -> true | _ -> false in
974974+ (* A comparison's operands with a column's declared collation made explicit:
975975+ an explicit COLLATE wins; else the LEFT column's collation governs, else
976976+ the right's. Only a non-default sequence is wrapped (stays VM-compilable). *)
977977+ let cmp_operands a b =
978978+ if explicit a || explicit b then (a, b)
979979+ else if is_col a then
980980+ match col_coll a with Some c -> (Collate (a, c), b) | None -> (a, b)
981981+ else if is_col b then
982982+ match col_coll b with Some c -> (a, Collate (b, c)) | None -> (a, b)
983983+ else (a, b)
984984+ in
985985+ (* A left operand with its declared collation explicit (for [IN]/ORDER BY). *)
986986+ let lhs_coll e =
987987+ if explicit e then e
988988+ else match col_coll e with Some c -> Collate (e, c) | None -> e
989989+ in
990990+ let rec rw = function
991991+ | Binary (((And | Or) as op), a, b) -> Binary (op, rw a, rw b)
992992+ | Unop_not a -> Unop_not (rw a)
993993+ | Binary (((Eq | Neq | Lt | Le | Gt | Ge) as op), a, b) ->
994994+ let a, b = cmp_operands a b in
995995+ Binary (op, a, b)
996996+ (* [x IN ..] uses the left operand's collation, like [x = ..]. *)
997997+ | In_list (e, l) -> In_list (lhs_coll e, l)
998998+ | In_select (e, sub) -> In_select (lhs_coll e, sub)
999999+ | e -> e
10001000+ in
10011001+ let order_key (e, dir) = (lhs_coll e, dir) in
10021002+ {
10031003+ s with
10041004+ where = Option.map rw s.where;
10051005+ having = Option.map rw s.having;
10061006+ group_by = List.map lhs_coll s.group_by;
10071007+ order_by = List.map order_key s.order_by;
10081008+ joins = List.map (fun (j : join) -> { j with on = rw j.on }) s.joins;
10091009+ }
10101010+10111011+(* Does select [s] read from a table named [name] (directly, via a join, a
10121012+ FROM-subquery, or a compound arm)? Used to spot a self-referential CTE. *)
10131013+let rec references_table name (s : select) =
10141014+ (match s.from with
10151015+ | Unit -> false
10161016+ | Table tr -> tr.name = name
10171017+ | Select { fs_select; _ } -> references_table name fs_select
10181018+ | Values _ | Table_function _ -> false)
10191019+ || List.exists
10201020+ (fun (j : join) ->
10211021+ j.table.name = name
10221022+ ||
10231023+ match j.table.subquery with
10241024+ | Some q -> references_table name q
10251025+ | None -> false)
10261026+ s.joins
10271027+ || List.exists (fun (_, arm) -> references_table name arm) s.compound
10281028+10291029+(* Combine compound arms by their set operator. UNION/EXCEPT/INTERSECT yield
10301030+ distinct rows; UNION ALL keeps duplicates. *)
10311031+let set_op op left right =
10321032+ match op with
10331033+ | Union_all -> left @ right
10341034+ | Union -> dedup (left @ right)
10351035+ | Except -> dedup (List.filter (fun r -> not (List.mem r right)) left)
10361036+ | Intersect -> dedup (List.filter (fun r -> List.mem r right) left)
10371037+10381038+(* ── Evaluation and SELECT execution ─────────────────────────── *)
10391039+10401040+(* Pick how to find the inner rows for a join: a full scan, an index lookup, or
10411041+ a hash table keyed on the equijoin columns. *)
10421042+let join_strategy cat ialias env_of eq_cols (j : Ast.join) =
10431043+ if eq_cols = [] then `Scan
10441044+ else
10451045+ match cat.index_for (j.table : table_ref).name eq_cols with
10461046+ | Some _ -> `Index
10471047+ | None ->
10481048+ let tbl : (value list, value list) Hashtbl.t = Hashtbl.create 256 in
10491049+ List.iter
10501050+ (fun vals ->
10511051+ let row = [ env_of vals ] in
10521052+ let key = List.map (fun c -> lookup row (Some ialias) c) eq_cols in
10531053+ Hashtbl.add tbl key vals)
10541054+ (cat.rows j.table.name);
10551055+ `Hash tbl
10561056+10571057+(* Resolve each NATURAL join's implicit ON -- equality on the columns common to
10581058+ the declared-order left and the joined table -- and return, per join in
10591059+ declared order, a triple of: the resolved join, the common columns to drop
10601060+ from its frame (so [SELECT *] lists each common column once), and the left
10611061+ frame template (alias, columns) accumulated before it (used to NULL-pad the
10621062+ left of an unmatched right row in a RIGHT/FULL join). *)
10631063+let resolve_natural cat base_frame joins =
10641064+ let _, acc =
10651065+ List.fold_left
10661066+ (fun (lframes, acc) (j : Ast.join) ->
10671067+ let ialias, icols = frag cat j.table in
10681068+ let entry =
10691069+ if (not j.natural) && j.using = [] then (j, [], lframes)
10701070+ else
10711071+ let owner c =
10721072+ List.find_map
10731073+ (fun (a, cs) -> if List.mem c cs then Some a else None)
10741074+ lframes
10751075+ in
10761076+ let common =
10771077+ if j.natural then List.filter (fun c -> owner c <> None) icols
10781078+ else j.using
10791079+ in
10801080+ let eqs =
10811081+ List.filter_map
10821082+ (fun c ->
10831083+ Option.map
10841084+ (fun a ->
10851085+ Binary (Eq, Col (Some a, c), Col (Some ialias, c)))
10861086+ (owner c))
10871087+ common
10881088+ in
10891089+ let on =
10901090+ match eqs with
10911091+ | [] -> Lit (Int 1L)
10921092+ | e :: r -> List.fold_left (fun acc x -> Binary (And, acc, x)) e r
10931093+ in
10941094+ ({ j with on }, common, lframes)
10951095+ in
10961096+ (lframes @ [ (ialias, icols) ], acc @ [ entry ]))
10971097+ ([ base_frame ], []) joins
10981098+ in
10991099+ acc
11001100+11011101+(* The distinct [Window] nodes appearing anywhere in [cols] (window functions do
11021102+ not nest, so a Window is not descended into). Pure syntax -- no executor
11031103+ dependencies, so it sits outside the evaluation rec group. *)
11041104+let window_exprs cols =
11051105+ let acc = ref [] in
11061106+ let rec go e =
11071107+ match e with
11081108+ | Window _ -> if not (List.mem e !acc) then acc := !acc @ [ e ]
11091109+ | Binary (_, a, b) | Filter (a, b) | Is (a, b) ->
11101110+ go a;
11111111+ go b
11121112+ | Unop_not a
11131113+ | Is_null a
11141114+ | Is_not_null a
11151115+ | Cast (a, _)
11161116+ | Distinct_agg (_, a)
11171117+ | Collate (a, _) ->
11181118+ go a
11191119+ | In_list (a, l) ->
11201120+ go a;
11211121+ List.iter go l
11221122+ | Func (_, args) -> List.iter go args
11231123+ | Ordered_agg (a, ob) ->
11241124+ go a;
11251125+ List.iter (fun (e, _) -> go e) ob
11261126+ | Case (b, brs, el) ->
11271127+ Option.iter go b;
11281128+ List.iter
11291129+ (fun (c, r) ->
11301130+ go c;
11311131+ go r)
11321132+ brs;
11331133+ Option.iter go el
11341134+ | In_select _ | Scalar_select _ | Exists _ | Lit _ | Col _ | Param _ | Star
11351135+ | Qualified_star _ ->
11361136+ ()
11371137+ in
11381138+ List.iter (fun (rc : Ast.result_col) -> go rc.expr) cols;
11391139+ !acc
11401140+11411141+(* Replace every [Window] node in [e] with the literal [lookup] gives for it. *)
11421142+let rec subst_windows lookup e =
11431143+ let r = subst_windows lookup in
11441144+ match e with
11451145+ | Window _ -> Lit (lookup e)
11461146+ | Binary (op, a, b) -> Binary (op, r a, r b)
11471147+ | Is (a, b) -> Is (r a, r b)
11481148+ | Unop_not a -> Unop_not (r a)
11491149+ | Is_null a -> Is_null (r a)
11501150+ | Is_not_null a -> Is_not_null (r a)
11511151+ | In_list (a, l) -> In_list (r a, List.map r l)
11521152+ | Cast (a, t) -> Cast (r a, t)
11531153+ | Collate (a, c) -> Collate (r a, c)
11541154+ | Func (n, args) -> Func (n, List.map r args)
11551155+ | Case (b, brs, el) ->
11561156+ Case
11571157+ ( Option.map r b,
11581158+ List.map (fun (c, x) -> (r c, r x)) brs,
11591159+ Option.map r el )
11601160+ | ( Lit _ | Col _ | Param _ | Star | Qualified_star _ | In_select _
11611161+ | Scalar_select _ | Exists _ | Distinct_agg _ | Filter _ | Ordered_agg _ )
11621162+ as e ->
11631163+ e
11641164+11651165+(* The frame [lo, hi] (ordered positions, clamped to the partition) for the row
11661166+ at [pos]: from the explicit ROWS/RANGE clause, else the default frame (whole
11671167+ partition with no ORDER BY, else UNBOUNDED PRECEDING to the current peer
11681168+ group). RANGE offsets are not value-based here; they fall back to the peer
11691169+ group, which agrees with RANGE for distinct order keys. [evcur] evaluates a
11701170+ bound offset against the current row. *)
11711171+let window_frame ~pos ~k ~peer_start ~peer_end ~evcur w =
11721172+ let off e = Int64.to_int (intify (evcur e)) in
11731173+ let lo, hi =
11741174+ match w.frame with
11751175+ | None -> (0, if w.order = [] then k - 1 else peer_end.(pos))
11761176+ | Some f ->
11771177+ let rows = f.rows in
11781178+ let resolve at_start = function
11791179+ | Unbounded_preceding -> 0
11801180+ | Unbounded_following -> k - 1
11811181+ | Current_row ->
11821182+ if rows then pos
11831183+ else if at_start then peer_start.(pos)
11841184+ else peer_end.(pos)
11851185+ | Preceding e -> if rows then pos - off e else peer_start.(pos)
11861186+ | Following e -> if rows then pos + off e else peer_end.(pos)
11871187+ in
11881188+ (resolve true f.start, resolve false f.end_)
11891189+ in
11901190+ (max 0 lo, min (k - 1) hi)
11911191+11921192+(* Wrap [cat] so [name] resolves to the given materialised [cols]/[rows] (no
11931193+ index), shadowing any real table -- the mechanism behind CTEs and derived
11941194+ tables in JOIN position. *)
11951195+let bind_relation cat name cols rows =
11961196+ {
11971197+ columns = (fun n -> if n = name then cols else cat.columns n);
11981198+ rows = (fun n -> if n = name then rows else cat.rows n);
11991199+ rows_rowid =
12001200+ (fun n ->
12011201+ if n = name then List.map (fun r -> (None, r)) rows
12021202+ else cat.rows_rowid n);
12031203+ index_scan = (fun n eqs -> if n = name then None else cat.index_scan n eqs);
12041204+ index_for = (fun n cs -> if n = name then None else cat.index_for n cs);
12051205+ collation = (fun n c -> if n = name then None else cat.collation n c);
12061206+ }
12071207+12081208+let is_col_node = function Col _ -> true | _ -> false
12091209+12101210+let subquery_column ?(scope = []) cat params s =
12111211+ !cat_subquery_runner cat scope s params
12121212+ |> List.map (function v :: _ -> v | [] -> Null)
12131213+12141214+let rec eval cat params env = function
12151215+ | Lit v -> v
12161216+ | Param i -> if i < Array.length params then params.(i) else Null
12171217+ | Col (q, n) -> lookup env q n
12181218+ | Star | Qualified_star _ -> failwith "'*' is only valid as a select item"
12191219+ | Unop_not e -> not_value (eval cat params env e)
12201220+ | Binary
12211221+ ( ((And | Or | Like | Glob | Concat | Json_get | Json_get_text) as op),
12221222+ a,
12231223+ b ) ->
12241224+ binop_value op (eval cat params env a) (eval cat params env b)
12251225+ | Binary
12261226+ ( ((Add | Sub | Mul | Div | Mod | Bit_and | Bit_or | Shl | Shr) as op),
12271227+ a,
12281228+ b ) ->
12291229+ arith op (eval cat params env a) (eval cat params env b)
12301230+ | Binary (op, a, b) ->
12311231+ let coll =
12321232+ match explicit_collation a with
12331233+ | Some _ as c -> c
12341234+ | None -> explicit_collation b
12351235+ in
12361236+ let va = eval cat params env a and vb = eval cat params env b in
12371237+ compare_with_affinity ?coll ~a_col:(is_col_node a) ~b_col:(is_col_node b)
12381238+ op va vb
12391239+ | Collate (e, _) -> eval cat params env e
12401240+ | Is_null e -> bool (eval cat params env e = Null)
12411241+ | Is_not_null e -> bool (eval cat params env e <> Null)
12421242+ | Is (a, b) -> is_value (eval cat params env a) (eval cat params env b)
12431243+ | In_list (e, l) ->
12441244+ let v = eval cat params env e in
12451245+ let cands = List.map (eval cat params env) l in
12461246+ (* The membership test uses the left operand's collation (a declared
12471247+ COLLATE made explicit by [resolve_collations]) and its affinity: a
12481248+ numeric column coerces a well-formed text candidate ([c0 IN ('2.0625')]
12491249+ matches) while a literal left keeps its value ([5 IN ('5')] mismatches).
12501250+ [bare] sees through a declared-collation wrapper to the column for the
12511251+ affinity rule, but not through a [BINARY] one -- that is unary plus,
12521252+ which strips affinity. *)
12531253+ let coll = explicit_collation e in
12541254+ let bare =
12551255+ match e with Collate (inner, c) when c <> "BINARY" -> inner | _ -> e
12561256+ in
12571257+ in_list_value ?coll ~e_col:(is_col_node bare) v cands
12581258+ | In_select (e, s) ->
12591259+ (* The subquery sees the current row as its outer scope, so a correlated
12601260+ [IN (SELECT ... WHERE inner.x = outer.y)] resolves [outer.y]. *)
12611261+ let scope = bindings_of_frames env in
12621262+ in_eval (eval cat params env e) (subquery_column ~scope cat params s)
12631263+ | Scalar_select s -> (
12641264+ (* A scalar subquery yields the first column of its first row (NULL if it
12651265+ returns none). Correlated via the same outer [env] scope. *)
12661266+ match !cat_subquery_runner cat (bindings_of_frames env) s params with
12671267+ | (v :: _) :: _ -> v
12681268+ | [] :: _ | [] -> Null)
12691269+ | Exists s ->
12701270+ bool
12711271+ (match !cat_subquery_runner cat (bindings_of_frames env) s params with
12721272+ | _ :: _ -> true
12731273+ | [] -> false)
12741274+ (* Aggregates are computed by {!aggregate} over the whole group; reaching one
12751275+ here means it was used as a scalar (in WHERE, ON, or ORDER BY). *)
12761276+ | Func (name, args) when is_agg name args ->
12771277+ Fmt.failwith
12781278+ "%s() is an aggregate function and is only valid as a select item, not \
12791279+ in WHERE, ON, or ORDER BY"
12801280+ name
12811281+ | Distinct_agg (name, _) ->
12821282+ Fmt.failwith
12831283+ "%s() is an aggregate function and is only valid as a select item, not \
12841284+ in WHERE, ON, or ORDER BY"
12851285+ name
12861286+ | Filter _ | Ordered_agg _ ->
12871287+ failwith
12881288+ "an aggregate FILTER/ORDER BY is only valid as a select item, not in \
12891289+ WHERE, ON, or ORDER BY"
12901290+ | Window _ ->
12911291+ failwith
12921292+ "a window function is only valid in the select list, not in WHERE, ON, \
12931293+ or GROUP BY"
12941294+ (* iif(C, T, F) is sugar for CASE and short-circuits, so it is evaluated
12951295+ before its branches rather than as an ordinary (eager-argument) function. *)
12961296+ | Func ("iif", [ c; t; f ]) ->
12971297+ if tri_of (eval cat params env c) = T then eval cat params env t
12981298+ else eval cat params env f
12991299+ | Func (name, args) -> apply name (List.map (eval cat params env) args)
13001300+ | Cast (e, ty) -> cast ty (eval cat params env e)
13011301+ (* CASE: the base (simple form) is evaluated once and matched with [=]
13021302+ semantics, so a NULL base matches no WHEN -- including WHEN NULL. First
13031303+ match wins; no match and no ELSE is NULL. *)
13041304+ | Case (base, branches, els) ->
13051305+ let bv = Option.map (eval cat params env) base in
13061306+ let hit cond =
13071307+ match bv with
13081308+ | Some b -> compare_op Eq b (eval cat params env cond) = Int 1L
13091309+ | None -> tri_of (eval cat params env cond) = T
13101310+ in
13111311+ let rec go = function
13121312+ | (c, r) :: rest -> if hit c then eval cat params env r else go rest
13131313+ | [] -> (
13141314+ match els with Some e -> eval cat params env e | None -> Null)
13151315+ in
13161316+ go branches
13171317+13181318+(* Inner-table equalities [col = e] in a join's ON clause where [e], evaluated
13191319+ against the current outer row, gives a concrete value -- the bindings that
13201320+ drive an index-nested-loop join. *)
13211321+let join_eqs cat params env ialias on =
13221322+ let rec go acc = function
13231323+ | Binary (And, a, b) -> go (go acc a) b
13241324+ | Binary (Eq, Col (Some q, c), e) when q = ialias && not (mentions ialias e)
13251325+ ->
13261326+ (c, eval cat params env e) :: acc
13271327+ | Binary (Eq, e, Col (Some q, c)) when q = ialias && not (mentions ialias e)
13281328+ ->
13291329+ (c, eval cat params env e) :: acc
13301330+ | _ -> acc
13311331+ in
13321332+ go [] on
13331333+13341334+(* RIGHT/FULL OUTER: the inner table's unmatched rows must also survive, so the
13351335+ left stream is materialised and every inner row is scanned for any match.
13361336+ FULL additionally keeps unmatched left rows (NULL inner), like LEFT. The
13371337+ emission order is not specified by SQL; callers that care add ORDER BY. *)
13381338+let right_full_join cat params (j : Ast.join) envs ~env_of ~trim ~null_frame
13391339+ ~left_template =
13401340+ let lefts = envs in
13411341+ let inners = Array.of_list (cat.rows j.table.name) in
13421342+ let hit = Array.make (Array.length inners) false in
13431343+ let null_left =
13441344+ match lefts with
13451345+ | e :: _ -> List.map null_pad_frame e
13461346+ | [] ->
13471347+ List.map
13481348+ (fun (a, cs) -> Pos (a, cs, List.map (fun _ -> Null) cs, None))
13491349+ left_template
13501350+ in
13511351+ let out = ref [] in
13521352+ List.iter
13531353+ (fun env ->
13541354+ let any = ref false in
13551355+ Array.iteri
13561356+ (fun k vals ->
13571357+ match tri_of (eval cat params (env @ [ env_of vals ]) j.on) with
13581358+ | T ->
13591359+ any := true;
13601360+ hit.(k) <- true;
13611361+ out := (env @ [ trim (env_of vals) ]) :: !out
13621362+ | F | U -> ())
13631363+ inners;
13641364+ if (not !any) && j.type_ = Full then out := (env @ [ null_frame ]) :: !out)
13651365+ lefts;
13661366+ Array.iteri
13671367+ (fun k vals ->
13681368+ if not hit.(k) then out := (null_left @ [ trim (env_of vals) ]) :: !out)
13691369+ inners;
13701370+ List.rev !out
13711371+13721372+(* Add one joined table to the running row stream, picking the access strategy
13731373+ once for the whole join (not per outer row):
13741374+ - index-nested-loop when a unique index covers the ON equi-columns;
13751375+ - hash join when the ON has equi-columns but no index: build a hash table on
13761376+ the inner relation keyed by those columns once, then probe per outer row
13771377+ (O(inner + outer) instead of the O(inner x outer) nested-loop);
13781378+ - plain nested-loop scan otherwise (no equi-column, e.g. a cross join).
13791379+ The full ON predicate is always re-checked, so the strategy only narrows
13801380+ candidates and NULL keys (which never equi-match) fall out. *)
13811381+let join_with ?(drop = []) ?(left_template = []) cat params (j : Ast.join) envs
13821382+ =
13831383+ let ialias, icols = frag cat j.table in
13841384+ let env_of vals = Pos (ialias, icols, vals, None) in
13851385+ (* A NATURAL join lists each common column once: the ON is evaluated on the
13861386+ full inner frame, but [drop]ped columns are removed from the frame stored
13871387+ in the row (they survive on the left and are equal there). With nothing to
13881388+ drop (the common non-NATURAL case) this is the identity, avoiding a per-row
13891389+ copy of the frame. *)
13901390+ let trim = if drop = [] then Fun.id else trim_frame drop in
13911391+ (* A LEFT JOIN keeps an outer row that matched nothing, with the inner table's
13921392+ columns all NULL. *)
13931393+ let null_frame =
13941394+ trim (Pos (ialias, icols, List.map (fun _ -> Null) icols, None))
13951395+ in
13961396+ let eq_cols = join_eq_columns ialias j.on in
13971397+ let strategy = join_strategy cat ialias env_of eq_cols j in
13981398+ (* The hash probe key as expressions over the outer row, resolved once (in
13991399+ [eq_cols] order) so each outer row only evaluates them -- no per-row
14001400+ [(col,value)] list rebuilt and re-looked-up. *)
14011401+ let probe_exprs =
14021402+ match strategy with
14031403+ | `Hash _ ->
14041404+ let m = join_probe_map ialias j.on in
14051405+ List.map (fun c -> List.assoc c m) eq_cols
14061406+ | _ -> []
14071407+ in
14081408+ let candidates env =
14091409+ match strategy with
14101410+ | `Scan -> cat.rows j.table.name
14111411+ | `Index -> (
14121412+ match
14131413+ cat.index_scan j.table.name (join_eqs cat params env ialias j.on)
14141414+ with
14151415+ | Some (_, rows) -> rows
14161416+ | None -> cat.rows j.table.name)
14171417+ | `Hash tbl ->
14181418+ let key = List.map (eval cat params env) probe_exprs in
14191419+ Hashtbl.find_all tbl key
14201420+ in
14211421+ let matched env =
14221422+ List.filter_map
14231423+ (fun vals ->
14241424+ (* Build the inner frame once and reuse it for both the ON test and the
14251425+ emitted row. *)
14261426+ let frame = env_of vals in
14271427+ match tri_of (eval cat params (env @ [ frame ]) j.on) with
14281428+ | T -> Some (env @ [ trim frame ])
14291429+ | F | U -> None)
14301430+ (candidates env)
14311431+ in
14321432+ match j.type_ with
14331433+ | Inner -> List.concat_map matched envs
14341434+ | Left ->
14351435+ List.concat_map
14361436+ (fun env ->
14371437+ match matched env with [] -> [ env @ [ null_frame ] ] | rows -> rows)
14381438+ envs
14391439+ | Right | Full ->
14401440+ right_full_join cat params j envs ~env_of ~trim ~null_frame ~left_template
14411441+14421442+let project cat params (s : Ast.select) env =
14431443+ (* A star-free select list (the common case) maps one value per item -- no
14441444+ per-item singleton list and no [concat_map] to flatten. Only [*] and [t.*],
14451445+ which expand to a whole frame, need the general path. *)
14461446+ let is_star (rc : Ast.result_col) =
14471447+ match rc.expr with Star | Qualified_star _ -> true | _ -> false
14481448+ in
14491449+ if List.exists is_star s.cols then
14501450+ List.concat_map
14511451+ (fun (rc : Ast.result_col) ->
14521452+ match rc.expr with
14531453+ | Star -> List.concat_map frame_values env
14541454+ | Qualified_star q -> (
14551455+ match aliased_ci (String.uppercase_ascii q) env with
14561456+ | Some f -> frame_values f
14571457+ | None -> Fmt.failwith "no such table: %s" q)
14581458+ | e -> [ eval cat params env e ])
14591459+ s.cols
14601460+ else
14611461+ List.map (fun (rc : Ast.result_col) -> eval cat params env rc.expr) s.cols
14621462+14631463+(* Fold one aggregate call over the group [envs]. With [distinct], the non-NULL
14641464+ argument values are deduplicated (preserving first-seen order) before the
14651465+ fold -- [count(DISTINCT x)], [sum(DISTINCT x)], etc. *)
14661466+let agg_call ?(distinct = false) cat params envs name args =
14671467+ let arg1 = function [ a ] -> a | _ -> Star in
14681468+ let dedup vs = if distinct then distinct_values vs else vs in
14691469+ let agg_vals arg =
14701470+ dedup
14711471+ (List.filter_map
14721472+ (fun env ->
14731473+ match eval cat params env arg with Null -> None | v -> Some v)
14741474+ envs)
14751475+ in
14761476+ match (name, args) with
14771477+ | "count", args -> (
14781478+ match arg1 args with
14791479+ | Star -> Int (Int64.of_int (List.length envs))
14801480+ | a -> Int (Int64.of_int (List.length (agg_vals a))))
14811481+ | ("group_concat" | "string_agg"), args -> (
14821482+ (* group_concat(X [, sep]) / string_agg(X, sep): non-NULL X values joined
14831483+ by sep (default ","); NULL over an empty group. *)
14841484+ let arg, sep =
14851485+ match args with
14861486+ | [ a ] -> (a, ",")
14871487+ | a :: s :: _ ->
14881488+ ( a,
14891489+ match envs with
14901490+ | env :: _ -> text_of_value (eval cat params env s)
14911491+ | [] -> "," )
14921492+ | [] -> (Star, ",")
14931493+ in
14941494+ match agg_vals arg with
14951495+ | [] -> Null
14961496+ | vals -> Text (String.concat sep (List.map text_of_value vals)))
14971497+ | _ -> numeric_agg name (agg_vals (arg1 args))
14981498+14991499+(* Evaluate one expression over a group [envs] with aggregate semantics: every
15001500+ aggregate call (even nested in a larger expression, e.g. [sum(x) > 5]) is
15011501+ folded over the group and substituted by its value; the rest is then read
15021502+ from an arbitrary row of the group. Used for select-list columns and HAVING. *)
15031503+let agg_value ?(empty = []) ?rep cat params envs e =
15041504+ (* FILTER (WHERE p) restricts the group rows; ORDER BY sorts them (only
15051505+ order-sensitive aggregates like group_concat observe the order). *)
15061506+ let filtered p envs =
15071507+ List.filter (fun env -> tri_of (eval cat params env p) = T) envs
15081508+ in
15091509+ let sorted ob envs =
15101510+ let rec cmp a b = function
15111511+ | [] -> 0
15121512+ | (e, dir) :: rest ->
15131513+ let c = compare_values (eval cat params a e) (eval cat params b e) in
15141514+ let c = match dir with Asc -> c | Desc -> -c in
15151515+ if c <> 0 then c else cmp a b rest
15161516+ in
15171517+ List.stable_sort (fun a b -> cmp a b ob) envs
15181518+ in
15191519+ (* Compute an aggregate expression's value over [envs], peeling FILTER/ORDER
15201520+ BY decorations down to the core Func / Distinct_agg call. *)
15211521+ let rec agg_core envs = function
15221522+ | Func (name, args) -> agg_call ~distinct:false cat params envs name args
15231523+ | Distinct_agg (name, arg) ->
15241524+ agg_call ~distinct:true cat params envs name [ arg ]
15251525+ | Filter (inner, p) -> agg_core (filtered p envs) inner
15261526+ | Ordered_agg (inner, ob) -> agg_core (sorted ob envs) inner
15271527+ | _ -> Null
15281528+ in
15291529+ let rec subst e =
15301530+ match e with
15311531+ | e when is_agg_expr e -> Lit (agg_core envs e)
15321532+ | Unop_not a -> Unop_not (subst a)
15331533+ | Binary (op, a, b) -> Binary (op, subst a, subst b)
15341534+ | Is (a, b) -> Is (subst a, subst b)
15351535+ | Is_null a -> Is_null (subst a)
15361536+ | Is_not_null a -> Is_not_null (subst a)
15371537+ | In_list (a, l) -> In_list (subst a, List.map subst l)
15381538+ | Cast (a, ty) -> Cast (subst a, ty)
15391539+ | Collate (a, c) -> Collate (subst a, c)
15401540+ | Case (base, branches, els) ->
15411541+ Case
15421542+ ( Option.map subst base,
15431543+ List.map (fun (c, r) -> (subst c, subst r)) branches,
15441544+ Option.map subst els )
15451545+ (* Recurse into a non-aggregate function's args for a nested aggregate. *)
15461546+ | Func (n, args) -> Func (n, List.map subst args)
15471547+ | (Lit _ | Col _ | Param _ | Star | Qualified_star _ | In_select _) as e ->
15481548+ e
15491549+ | (Scalar_select _ | Exists _) as e -> e
15501550+ | (Distinct_agg _ | Filter _ | Ordered_agg _ | Window _) as e -> e
15511551+ in
15521552+ let e = subst e in
15531553+ (* Bare columns: from [rep] (the min/max row), else an arbitrary or empty row. *)
15541554+ let row =
15551555+ match rep with
15561556+ | Some r -> r
15571557+ | None -> ( match envs with e :: _ -> e | [] -> empty)
15581558+ in
15591559+ eval cat params row e
15601560+15611561+let aggregate ?(empty = []) ?rep cat params (s : Ast.select) envs =
15621562+ (* [*] in a grouped/aggregated select expands to the columns of the group's
15631563+ representative row (the min/max row when one is chosen, else an arbitrary
15641564+ member), just as a bare column does. *)
15651565+ let rep_row () =
15661566+ match rep with
15671567+ | Some r -> r
15681568+ | None -> ( match envs with e :: _ -> e | [] -> empty)
15691569+ in
15701570+ let has_star =
15711571+ List.exists
15721572+ (fun (rc : Ast.result_col) ->
15731573+ match rc.expr with Star | Qualified_star _ -> true | _ -> false)
15741574+ s.cols
15751575+ in
15761576+ if not has_star then
15771577+ List.map
15781578+ (fun (rc : Ast.result_col) ->
15791579+ agg_value ~empty ?rep cat params envs rc.expr)
15801580+ s.cols
15811581+ else
15821582+ List.concat_map
15831583+ (fun (rc : Ast.result_col) ->
15841584+ match rc.expr with
15851585+ | Star -> List.concat_map frame_values (rep_row ())
15861586+ | Qualified_star q -> (
15871587+ match aliased_ci (String.uppercase_ascii q) (rep_row ()) with
15881588+ | Some f -> frame_values f
15891589+ | None -> Fmt.failwith "no such table: %s" q)
15901590+ | e -> [ agg_value ~empty ?rep cat params envs e ])
15911591+ s.cols
15921592+15931593+(* The representative row for bare (non-aggregate) columns: sqlite3 takes them
15941594+ from the row achieving a lone [min()]/[max()] in the select list (NULLs are
15951595+ ignored, as by the aggregate). [None] when there is no such aggregate, so the
15961596+ caller uses an arbitrary row. *)
15971597+let minmax_rep cat params (s : select) envs =
15981598+ let target =
15991599+ List.find_map
16001600+ (fun (rc : Ast.result_col) ->
16011601+ match rc.expr with
16021602+ | Func ("min", [ a ]) -> Some (`Min, a)
16031603+ | Func ("max", [ a ]) -> Some (`Max, a)
16041604+ | _ -> None)
16051605+ s.cols
16061606+ in
16071607+ match (target, envs) with
16081608+ | None, _ | _, [] -> None
16091609+ | Some (dir, arg), e0 :: rest ->
16101610+ let better cur env =
16111611+ let cv = eval cat params cur arg and ev = eval cat params env arg in
16121612+ if ev = Null then false
16131613+ else if cv = Null then true
16141614+ else
16151615+ let c = compare_values ev cv in
16161616+ match dir with `Min -> c < 0 | `Max -> c > 0
16171617+ in
16181618+ Some
16191619+ (List.fold_left
16201620+ (fun best env -> if better best env then env else best)
16211621+ e0 rest)
16221622+16231623+let order_cmp cat params order_by e1 e2 =
16241624+ let rec cmp = function
16251625+ | [] -> 0
16261626+ | (oe, dir) :: rest ->
16271627+ let va = eval cat params e1 oe and vb = eval cat params e2 oe in
16281628+ let c =
16291629+ match explicit_collation oe with
16301630+ | Some name -> compare_values_coll name va vb
16311631+ | None -> compare_values va vb
16321632+ in
16331633+ let c = match dir with Asc -> c | Desc -> -c in
16341634+ if c <> 0 then c else cmp rest
16351635+ in
16361636+ cmp order_by
16371637+16381638+let order cat params s envs =
16391639+ if s.order_by = [] then envs
16401640+ else List.stable_sort (order_cmp cat params s.order_by) envs
16411641+16421642+(* Partition rows into groups by the GROUP BY key tuple, keeping the groups in
16431643+ first-appearance order; each group is the list of its rows. *)
16441644+let group_envs cat params keys envs =
16451645+ (* Two rows fall in the same group when their key values are equal under the
16461646+ key's collation: a NOCASE GROUP BY key folds case. [resolve_collations] has
16471647+ already made a column's declared collation explicit, so a non-default
16481648+ sequence shows up as a [Collate] wrapper. *)
16491649+ let group_key env =
16501650+ List.map
16511651+ (fun e ->
16521652+ match (explicit_collation e, eval cat params env e) with
16531653+ | Some c, Text s -> collation_key c (Text s)
16541654+ | _, v -> v)
16551655+ keys
16561656+ in
16571657+ let tbl = Hashtbl.create 16 in
16581658+ let seen = ref [] in
16591659+ List.iter
16601660+ (fun env ->
16611661+ let k = group_key env in
16621662+ match Hashtbl.find_opt tbl k with
16631663+ | Some r -> r := env :: !r
16641664+ | None ->
16651665+ Hashtbl.add tbl k (ref [ env ]);
16661666+ seen := k :: !seen)
16671667+ envs;
16681668+ List.rev_map (fun k -> List.rev !(Hashtbl.find tbl k)) !seen
16691669+16701670+(* One row's window value: ranking from its position, navigation/aggregate over
16711671+ the frame [lo, hi] (ordered positions). [at i e] evaluates [e] at the i-th
16721672+ ordered row. *)
16731673+let window_value cat params ~name ~args ~distinct ~envs_a ~oa ~peer_start ~k
16741674+ ~dense ~pos ~lo ~hi =
16751675+ let at i e = eval cat params envs_a.(oa.(i)) e in
16761676+ let evcur e = eval cat params envs_a.(oa.(pos)) e in
16771677+ match name with
16781678+ | "row_number" -> Int (Int64.of_int (pos + 1))
16791679+ | "rank" -> Int (Int64.of_int (peer_start.(pos) + 1))
16801680+ | "dense_rank" -> Int (Int64.of_int dense)
16811681+ | "first_value" -> if lo <= hi then at lo (List.hd args) else Null
16821682+ | "last_value" -> if lo <= hi then at hi (List.hd args) else Null
16831683+ | "lag" | "lead" ->
16841684+ let off =
16851685+ match List.nth_opt args 1 with
16861686+ | Some o -> Int64.to_int (intify (evcur o))
16871687+ | None -> 1
16881688+ in
16891689+ let dflt =
16901690+ match List.nth_opt args 2 with Some d -> evcur d | None -> Null
16911691+ in
16921692+ let t = if name = "lag" then pos - off else pos + off in
16931693+ if t >= 0 && t < k then at t (List.hd args) else dflt
16941694+ | "nth_value" ->
16951695+ let nn = Int64.to_int (intify (evcur (List.nth args 1))) in
16961696+ let t = lo + (nn - 1) in
16971697+ if nn >= 1 && t <= hi then at t (List.hd args) else Null
16981698+ | "ntile" ->
16991699+ let nb = max 1 (Int64.to_int (intify (evcur (List.hd args)))) in
17001700+ let base = k / nb and rem = k mod nb in
17011701+ let big = rem * (base + 1) in
17021702+ let b =
17031703+ if pos < big then pos / (base + 1) else rem + ((pos - big) / base)
17041704+ in
17051705+ Int (Int64.of_int (b + 1))
17061706+ | _ ->
17071707+ let frame =
17081708+ if lo > hi then []
17091709+ else List.init (hi - lo + 1) (fun r -> envs_a.(oa.(lo + r)))
17101710+ in
17111711+ agg_call ~distinct cat params frame name args
17121712+17131713+(* Compute one partition's window values into [arr]: order [idxs] by the
17141714+ window's ORDER BY, find peer-group bounds (peers share a rank and a frame),
17151715+ and write each row's value (ranking by position, aggregate over its frame). *)
17161716+let fill_window_partition cat params ~name ~args ~distinct w envs_a arr idxs =
17171717+ let oa =
17181718+ Array.of_list
17191719+ (List.stable_sort
17201720+ (fun i j -> order_cmp cat params w.order envs_a.(i) envs_a.(j))
17211721+ idxs)
17221722+ in
17231723+ let k = Array.length oa in
17241724+ let peer_start = Array.make k 0 and peer_end = Array.make k 0 in
17251725+ let p = ref 0 in
17261726+ while !p < k do
17271727+ let q = ref !p in
17281728+ while
17291729+ !q + 1 < k
17301730+ && order_cmp cat params w.order envs_a.(oa.(!p)) envs_a.(oa.(!q + 1)) = 0
17311731+ do
17321732+ incr q
17331733+ done;
17341734+ for r = !p to !q do
17351735+ peer_start.(r) <- !p;
17361736+ peer_end.(r) <- !q
17371737+ done;
17381738+ p := !q + 1
17391739+ done;
17401740+ let dense = ref 0 and prev_start = ref (-1) in
17411741+ for pos = 0 to k - 1 do
17421742+ if peer_start.(pos) <> !prev_start then begin
17431743+ incr dense;
17441744+ prev_start := peer_start.(pos)
17451745+ end;
17461746+ let evcur e = eval cat params envs_a.(oa.(pos)) e in
17471747+ let lo, hi = window_frame ~pos ~k ~peer_start ~peer_end ~evcur w in
17481748+ arr.(oa.(pos)) <-
17491749+ window_value cat params ~name ~args ~distinct ~envs_a ~oa ~peer_start ~k
17501750+ ~dense:!dense ~pos ~lo ~hi
17511751+ done
17521752+17531753+(* Evaluate the window function [Window (f, w)] over [envs], returning a value
17541754+ per env (aligned to [envs] order). Rows are partitioned by [w.partition],
17551755+ ordered within each partition by [w.order]; ranking functions use the
17561756+ position, aggregates use the default frame (the whole partition with no
17571757+ ORDER BY, else running through the current row's peer group). *)
17581758+let eval_window cat params we envs =
17591759+ let fexpr, w = match we with Window (f, w) -> (f, w) | _ -> assert false in
17601760+ let name, args, distinct =
17611761+ match fexpr with
17621762+ | Func (n, a) -> (n, a, false)
17631763+ | Distinct_agg (n, a) -> (n, [ a ], true)
17641764+ | _ -> failwith "window: not a function call"
17651765+ in
17661766+ let envs_a = Array.of_list envs in
17671767+ let n = Array.length envs_a in
17681768+ let arr = Array.make n Null in
17691769+ (* partition the row indices by the partition-key tuple *)
17701770+ let tbl = Hashtbl.create 16 and keys = ref [] in
17711771+ Array.iteri
17721772+ (fun i env ->
17731773+ let k = List.map (eval cat params env) w.partition in
17741774+ match Hashtbl.find_opt tbl k with
17751775+ | Some r -> r := i :: !r
17761776+ | None ->
17771777+ Hashtbl.add tbl k (ref [ i ]);
17781778+ keys := k :: !keys)
17791779+ envs_a;
17801780+ let parts = List.rev_map (fun k -> List.rev !(Hashtbl.find tbl k)) !keys in
17811781+ List.iter
17821782+ (fun idxs ->
17831783+ fill_window_partition cat params ~name ~args ~distinct w envs_a arr idxs)
17841784+ parts;
17851785+ arr
17861786+17871787+(* Project [envs] when the select list has window functions: compute each window
17881788+ value per env, then project, substituting the windows. The outer ORDER BY
17891789+ sorts after the windows are computed (their frames are independent of it). *)
17901790+let window_project ~ordered cat params (s : Ast.select) envs =
17911791+ let wins =
17921792+ List.map
17931793+ (fun we -> (we, eval_window cat params we envs))
17941794+ (window_exprs s.cols)
17951795+ in
17961796+ let indexed = List.mapi (fun i env -> (i, env)) envs in
17971797+ let indexed =
17981798+ if ordered then
17991799+ List.stable_sort
18001800+ (fun (_, e1) (_, e2) -> order_cmp cat params s.order_by e1 e2)
18011801+ indexed
18021802+ else indexed
18031803+ in
18041804+ List.map
18051805+ (fun (i, env) ->
18061806+ let lookup we = (List.assoc we wins).(i) in
18071807+ List.concat_map
18081808+ (fun (rc : Ast.result_col) ->
18091809+ match rc.expr with
18101810+ | Star -> List.concat_map frame_values env
18111811+ | Qualified_star q -> (
18121812+ match aliased_ci (String.uppercase_ascii q) env with
18131813+ | Some f -> frame_values f
18141814+ | None -> Fmt.failwith "no such table: %s" q)
18151815+ | e -> [ eval cat params env (subst_windows lookup e) ])
18161816+ s.cols)
18171817+ indexed
18181818+18191819+(* Run one SELECT/VALUES core to output rows: combine, filter, group/aggregate
18201820+ or project, distinct. [~order] applies the core's own ORDER BY (true for a
18211821+ plain select; false for a compound's cores, whose ordering is the
18221822+ compound-wide ORDER BY applied to the combined rows). *)
18231823+(* Turn the WHERE-filtered row environments [envs] into output rows: a GROUP BY
18241824+ folds each group's aggregates (ordering the groups when [ordered]); an
18251825+ aggregate select list or HAVING with no GROUP BY is one implicit group; a
18261826+ window select projects through {!window_project}; otherwise it is an ordinary
18271827+ sort-and-project. Shared by {!exec_core} (which scans the table) and
18281828+ {!aggregate_scan} (which is handed already-scanned rows). *)
18291829+let core_rows ~ordered cat params s envs =
18301830+ (* The all-NULL schema env for an empty implicit group (an empty table with no
18311831+ GROUP BY), so a bare column in HAVING/the select list reads as NULL. *)
18321832+ let empty = schema_env cat s in
18331833+ (* HAVING keeps a group only when its (aggregate) predicate is true; a NULL
18341834+ result is untrue and drops the group. *)
18351835+ let keep_group g =
18361836+ match s.having with
18371837+ | None -> true
18381838+ | Some h -> tri_of (agg_value ~empty cat params g h) = T
18391839+ in
18401840+ if s.group_by <> [] then
18411841+ let groups = group_envs cat params s.group_by envs in
18421842+ let groups =
18431843+ if not ordered then groups
18441844+ (* a compound arm: combine_compound re-orders, so leave first-seen *)
18451845+ else
18461846+ (* sqlite3 emits GROUP BY result rows in the group key's ascending order
18471847+ when there is no ORDER BY; an explicit ORDER BY overrides it. *)
18481848+ let keys =
18491849+ if s.order_by <> [] then s.order_by
18501850+ else List.map (fun e -> (e, Asc)) s.group_by
18511851+ in
18521852+ List.stable_sort
18531853+ (fun g1 g2 -> order_cmp cat params keys (List.hd g1) (List.hd g2))
18541854+ groups
18551855+ in
18561856+ List.filter_map
18571857+ (fun g ->
18581858+ let rep = minmax_rep cat params s g in
18591859+ if keep_group g then Some (aggregate ?rep cat params s g) else None)
18601860+ groups
18611861+ else if List.exists is_aggregate s.cols || s.having <> None then
18621862+ (* No GROUP BY but an aggregate select list or a HAVING clause: the whole
18631863+ input is one implicit group (lang_select.html). *)
18641864+ let rep = minmax_rep cat params s envs in
18651865+ if keep_group envs then [ aggregate ~empty ?rep cat params s envs ] else []
18661866+ else if window_exprs s.cols <> [] then
18671867+ window_project ~ordered cat params s envs
18681868+ else
18691869+ let envs = if ordered then order cat params s envs else envs in
18701870+ List.map (project cat params s) envs
18711871+18721872+(* ORDER BY over a compound's output rows: a bare integer is an ordinal (1-based
18731873+ output column), a bare name is an output column, anything else is evaluated
18741874+ against the row keyed by the output column names. *)
18751875+let compound_order cat params names order_by rows =
18761876+ if order_by = [] then rows
18771877+ else
18781878+ let nth row i = Option.value (List.nth_opt row i) ~default:Null in
18791879+ let col_index name =
18801880+ let rec go i = function
18811881+ | [] -> None
18821882+ | x :: _ when x = name -> Some i
18831883+ | _ :: r -> go (i + 1) r
18841884+ in
18851885+ go 0 names
18861886+ in
18871887+ (* [ORDER BY k COLLATE c] wraps the ordinal in [Collate]; the collation is
18881888+ read by [cmp] below, so here just look through it to the key itself. *)
18891889+ let rec key row oe =
18901890+ match oe with
18911891+ | Lit (Int n) -> nth row (Int64.to_int n - 1)
18921892+ | Col (_, name) -> (
18931893+ match col_index name with Some i -> nth row i | None -> Null)
18941894+ | Collate (e, _) -> key row e
18951895+ | _ -> eval cat params [ Pos ("", names, row, None) ] oe
18961896+ in
18971897+ List.stable_sort
18981898+ (fun r1 r2 ->
18991899+ let rec cmp = function
19001900+ | [] -> 0
19011901+ | (oe, dir) :: rest ->
19021902+ let c =
19031903+ match explicit_collation oe with
19041904+ | Some name -> compare_values_coll name (key r1 oe) (key r2 oe)
19051905+ | None -> compare_values (key r1 oe) (key r2 oe)
19061906+ in
19071907+ let c = match dir with Asc -> c | Desc -> -c in
19081908+ if c <> 0 then c else cmp rest
19091909+ in
19101910+ cmp order_by)
19111911+ rows
19121912+19131913+let apply_limit cat params s rows =
19141914+ (* OFFSET first, then LIMIT. A negative/NULL offset skips nothing; a negative
19151915+ LIMIT is unbounded; LIMIT 0 yields no rows (lang_select.html). *)
19161916+ (* An offset/limit beyond the row count (up to int64 max) must not overflow
19171917+ [Int64.to_int]; compare against the length first. *)
19181918+ let count = Int64.of_int (List.length rows) in
19191919+ let rows =
19201920+ match s.offset with
19211921+ | None -> rows
19221922+ | Some e -> (
19231923+ match eval cat params [] e with
19241924+ | Int n when n >= count -> []
19251925+ | Int n when n > 0L -> drop (Int64.to_int n) rows
19261926+ | _ -> rows)
19271927+ in
19281928+ match s.limit with
19291929+ | None -> rows
19301930+ | Some e -> (
19311931+ match eval cat params [] e with
19321932+ | Int n when n < 0L -> rows
19331933+ | Int n when n >= Int64.of_int (List.length rows) -> rows
19341934+ | Int n -> take (Int64.to_int n) rows
19351935+ | Null -> rows
19361936+ | _ -> failwith "LIMIT must be an integer")
19371937+19381938+(* Materialise each join operand that is a subquery [(q) alias] under its alias,
19391939+ so the join machinery sees it as an ordinary named relation. *)
19401940+let rec bind_join_subqueries ?(scope = []) cat params joins =
19411941+ List.fold_left
19421942+ (fun cat (j : Ast.join) ->
19431943+ match j.table.subquery with
19441944+ | None -> cat
19451945+ | Some sub ->
19461946+ let cols = select_columns cat sub in
19471947+ let rows = exec_select ~scope cat sub params in
19481948+ bind_relation cat j.table.name cols rows)
19491949+ cat joins
19501950+19511951+and combine ?(scope = []) cat params s : env list =
19521952+ let cat = bind_join_subqueries ~scope cat params s.joins in
19531953+ let no_rowid = List.map (fun r -> (None, r)) in
19541954+ let alias, cols, from_rows =
19551955+ match s.from with
19561956+ | Unit -> ("", [], [ (None, []) ])
19571957+ | Table tr ->
19581958+ let alias, cols = frag cat tr in
19591959+ (* A full scan carries each row's rowid (for the implicit rowid column);
19601960+ an index-narrowed scan drops it, so rowid stays unresolvable there. *)
19611961+ let rows =
19621962+ match from_access cat params tr s.where with
19631963+ | Some (_, rows) -> no_rowid rows
19641964+ | None -> cat.rows_rowid tr.name
19651965+ in
19661966+ (alias, cols, rows)
19671967+ | Select { fs_select; fs_alias } ->
19681968+ let alias = Option.value fs_alias ~default:"" in
19691969+ let cols = select_columns cat fs_select in
19701970+ (alias, cols, no_rowid (exec_select ~scope cat fs_select params))
19711971+ | Values tuples ->
19721972+ let n = match tuples with r :: _ -> List.length r | [] -> 0 in
19731973+ let cols = List.init n (fun i -> Fmt.str "column%d" (i + 1)) in
19741974+ let env = frames_of_scope scope in
19751975+ ("", cols, no_rowid (List.map (List.map (eval cat params env)) tuples))
19761976+ | Table_function { tf_name; tf_args; tf_alias } ->
19771977+ let alias = Option.value tf_alias ~default:tf_name in
19781978+ let env = frames_of_scope scope in
19791979+ let cols, rows =
19801980+ table_function tf_name (List.map (eval cat params env) tf_args)
19811981+ in
19821982+ (alias, cols, no_rowid rows)
19831983+ in
19841984+ let base =
19851985+ List.map (fun (rid, vals) -> [ Pos (alias, cols, vals, rid) ]) from_rows
19861986+ in
19871987+ let resolved = resolve_natural cat (alias, cols) s.joins in
19881988+ let info_of j =
19891989+ match List.find_opt (fun (j', _, _) -> j' == j) resolved with
19901990+ | Some (_, d, lt) -> (d, lt)
19911991+ | None -> ([], [])
19921992+ in
19931993+ let joined =
19941994+ List.fold_left
19951995+ (fun envs j ->
19961996+ let drop, left_template = info_of j in
19971997+ join_with cat params ~drop ~left_template j envs)
19981998+ base
19991999+ (plan_joins cat alias (List.map (fun (j, _, _) -> j) resolved))
20002000+ in
20012001+ (* The outer [scope] sits at the tail: inner tables shadow it for unqualified
20022002+ names, but a qualified [outer.col] / [OLD.col] / [NEW.col] still resolves. *)
20032003+ if scope = [] then joined
20042004+ else
20052005+ let tail = frames_of_scope scope in
20062006+ List.map (fun env -> env @ tail) joined
20072007+20082008+(* Bind each WITH table expression as a row source, materialized once (the
20092009+ state-of-the-art choice when a CTE is read more than once, as our nested-loop
20102010+ joins do). Later CTEs see the earlier ones. *)
20112011+and materialize_ctes cat params ctes =
20122012+ let bind = bind_relation in
20132013+ List.fold_left
20142014+ (fun cat (c : cte) ->
20152015+ let cols =
20162016+ match c.columns with [] -> select_columns cat c.query | cs -> cs
20172017+ in
20182018+ let rows =
20192019+ if references_table c.name c.query then
20202020+ run_recursive cat c.name cols params c.query
20212021+ else exec_select cat c.query params
20222022+ in
20232023+ bind cat c.name cols rows)
20242024+ cat ctes
20252025+20262026+(* Recursive CTE by semi-naive (delta) evaluation -- the SQLite / differential
20272027+ algorithm: seed with the non-recursive arm; each round runs the recursive
20282028+ arm(s) over only the previous round's new rows ([working]), so work is the
20292029+ delta, not the whole accumulated relation. UNION dedups new rows against the
20302030+ full result; UNION ALL keeps them. [cat] already binds [name] (to be
20312031+ rebound per round). A row cap guards against a non-terminating recursion. *)
20322032+and run_recursive cat name cols params query =
20332033+ let op = match query.compound with (op, _) :: _ -> op | [] -> Union_all in
20342034+ let dedup_union = op = Union in
20352035+ let strip s = { s with compound = []; order_by = []; limit = None } in
20362036+ let initial = strip query in
20372037+ let arms = List.map (fun (_, arm) -> strip arm) query.compound in
20382038+ let rebind rows = bind_relation cat name cols rows in
20392039+ let seed = exec_core ~ordered:false (rebind []) params initial in
20402040+ let rec loop result working =
20412041+ if working = [] then result
20422042+ else
20432043+ let cat' = rebind working in
20442044+ let delta =
20452045+ List.concat_map
20462046+ (fun arm -> exec_core ~ordered:false cat' params arm)
20472047+ arms
20482048+ in
20492049+ let delta =
20502050+ if dedup_union then
20512051+ dedup (List.filter (fun r -> not (List.mem r result)) delta)
20522052+ else delta
20532053+ in
20542054+ if delta = [] then result
20552055+ else if List.length result + List.length delta > 1_000_000 then
20562056+ failwith "recursive CTE exceeded 1,000,000 rows"
20572057+ else loop (result @ delta) delta
20582058+ in
20592059+ loop seed seed
20602060+20612061+and exec_core ~ordered ?(scope = []) cat params s =
20622062+ let s = resolve_ordinals cat s in
20632063+ let s = resolve_collations cat s in
20642064+ let s = resolve_where_aliases cat s in
20652065+ let envs = combine ~scope cat params s in
20662066+ let envs =
20672067+ match s.where with
20682068+ | None -> envs
20692069+ | Some w -> List.filter (fun env -> tri_of (eval cat params env w) = T) envs
20702070+ in
20712071+ let rows = core_rows ~ordered cat params s envs in
20722072+ if s.distinct then distinct_dedup s.cols rows else rows
20732073+20742074+and exec_select ?(scope = []) cat s params =
20752075+ let cat = if s.ctes = [] then cat else materialize_ctes cat params s.ctes in
20762076+ let rows =
20772077+ if s.compound = [] then exec_core ~ordered:true ~scope cat params s
20782078+ else
20792079+ let names = select_columns cat s in
20802080+ let combined =
20812081+ List.fold_left
20822082+ (fun acc (op, arm) ->
20832083+ set_op op acc (exec_core ~ordered:false ~scope cat params arm))
20842084+ (exec_core ~ordered:false ~scope cat params s)
20852085+ s.compound
20862086+ in
20872087+ compound_order cat params names s.order_by combined
20882088+ in
20892089+ apply_limit cat params s rows
20902090+20912091+(* Does any expression reference the implicit [rowid] pseudo-column? A subquery
20922092+ or window expression returns [true] conservatively (the caller then keeps such
20932093+ a select on the tree-walker rather than the rowid-free VM scan). *)
20942094+(* Whether [e] references a [rowid] column. A subquery / [EXISTS] reads as [true]
20952095+ (its scan is separate). A [Window] is opaque by default (the plain aggregate
20962096+ path cannot scan it); with [~through_window], it descends into the window's
20972097+ function, partition, and order keys -- the window node itself is what the
20982098+ window post-pass compiles, not a rowid reference, but a rowid inside its
20992099+ scanned components still matters. *)
21002100+let rec expr_references_rowid ?(through_window = false) e =
21012101+ let r = expr_references_rowid ~through_window in
21022102+ match e with
21032103+ | Col (_, name) -> is_rowid_name name
21042104+ | Lit _ | Param _ | Star | Qualified_star _ -> false
21052105+ | Unop_not e
21062106+ | Is_null e
21072107+ | Is_not_null e
21082108+ | Cast (e, _)
21092109+ | Collate (e, _)
21102110+ | Distinct_agg (_, e) ->
21112111+ r e
21122112+ | Binary (_, a, b) | Is (a, b) | Filter (a, b) -> r a || r b
21132113+ | In_list (e, l) -> r e || List.exists r l
21142114+ | Func (_, args) -> List.exists r args
21152115+ | Ordered_agg (a, ob) -> r a || List.exists (fun (e, _) -> r e) ob
21162116+ | Case (base, brs, els) ->
21172117+ Option.fold ~none:false ~some:r base
21182118+ || List.exists (fun (c, x) -> r c || r x) brs
21192119+ || Option.fold ~none:false ~some:r els
21202120+ | Window (f, w) ->
21212121+ through_window
21222122+ && (r f || List.exists r w.partition
21232123+ || List.exists (fun (oe, _) -> r oe) w.order)
21242124+ | In_select _ | Scalar_select _ | Exists _ -> true
21252125+21262126+(* Whether [s] is an aggregate/GROUP BY select that {!aggregate_scan} can run from
21272127+ rowid-free scanned rows: it groups or aggregates or has HAVING, uses no window
21282128+ function, and references no [rowid]. *)
21292129+let aggregate_compilable (s : select) =
21302130+ (s.group_by <> [] || s.having <> None || List.exists is_aggregate s.cols)
21312131+ && window_exprs s.cols = []
21322132+ && not
21332133+ (List.exists
21342134+ (fun (rc : Ast.result_col) -> expr_references_rowid rc.expr)
21352135+ s.cols
21362136+ || (match s.having with
21372137+ | Some h -> expr_references_rowid h
21382138+ | None -> false)
21392139+ || List.exists expr_references_rowid s.group_by
21402140+ || List.exists (fun (e, _) -> expr_references_rowid e) s.order_by)
21412141+21422142+(* A select with a window function over a [rowid]-free projection: like an
21432143+ aggregate, it scans its (filtered, joined) source rows then post-processes them
21442144+ ({!window_project}, reached through {!aggregate_scan}). Lets the register VM run
21452145+ the scan instead of falling back wholesale. *)
21462146+let window_compilable (s : select) =
21472147+ let rowid = expr_references_rowid ~through_window:true in
21482148+ window_exprs s.cols <> []
21492149+ && not
21502150+ (List.exists (fun (rc : Ast.result_col) -> rowid rc.expr) s.cols
21512151+ || List.exists (fun (e, _) -> rowid e) s.order_by)
21522152+21532153+(* Run an aggregate/GROUP BY [s] from its already-scanned source [rows] and reuse
21542154+ {!core_rows} for the grouping/aggregation/HAVING and ORDER BY, then dedup
21552155+ (DISTINCT) and apply OFFSET/LIMIT. [segments] names the join's tables in FROM
21562156+ order as [(alias, cols)]; each scanned row is the concatenation of its tables'
21572157+ columns in that order, so it is sliced back into one [Pos] binding per table
21582158+ -- a single-table aggregate is just a one-element [segments]. Lets the
21592159+ register VM do the (possibly joined) scan and reuse this aggregation. *)
21602160+let aggregate_scan cat params (s : select) segments rows =
21612161+ let bind vals =
21622162+ let env, _ =
21632163+ List.fold_left
21642164+ (fun (acc, rest) (alias, cols) ->
21652165+ let n = List.length cols in
21662166+ (Pos (alias, cols, take n rest, None) :: acc, drop n rest))
21672167+ ([], vals) segments
21682168+ in
21692169+ List.rev env
21702170+ in
21712171+ let envs = List.map bind rows in
21722172+ let rows = core_rows ~ordered:true cat params s envs in
21732173+ let rows = if s.distinct then dedup rows else rows in
21742174+ apply_limit cat params s rows
21752175+21762176+(* The default subquery runner: the tree-walker over the enclosing-row [cat]. The
21772177+ engine overrides it with a catalog-VM runner reading the same [cat]. *)
21782178+let () =
21792179+ cat_subquery_runner :=
21802180+ fun cat scope s params -> exec_select ~scope cat s params
21812181+21822182+(* Combine the already-computed arm rows of a compound [s] -- [base_rows] for the
21832183+ leading arm and [(op, rows)] for each following arm -- exactly as the compound
21842184+ branch of {!exec_select} does: fold the set operators, order by [s.order_by]
21852185+ over the output columns, then apply OFFSET/LIMIT. Lets a caller compute each
21862186+ arm however it likes (e.g. through the register VM) and reuse this orchestration. *)
21872187+let combine_compound cat params (s : select) base_rows compound_rows =
21882188+ let combined =
21892189+ List.fold_left
21902190+ (fun acc (op, rows) -> set_op op acc rows)
21912191+ base_rows compound_rows
21922192+ in
21932193+ let ordered =
21942194+ compound_order cat params (select_columns cat s) s.order_by combined
21952195+ in
21962196+ apply_limit cat params s ordered
21972197+21982198+(* Each FROM source of [s] as (alias, its column names) -- the column scope a
21992199+ subquery body resolves against. A name that does not resolve (e.g. a CTE absent
22002200+ from the base catalog) contributes no columns, so a subquery using it reads as
22012201+ correlated and is left for the tree-walker rather than wrongly hoisted. *)
22022202+let from_scope cat (s : select) : (string * string list) list =
22032203+ let cols_of name = try cat.columns name with Failure _ -> [] in
22042204+ let alias_or_name (tr : table_ref) =
22052205+ match tr.alias with Some a -> a | None -> tr.name
22062206+ in
22072207+ let of_ref (tr : table_ref) =
22082208+ match tr.subquery with
22092209+ | Some q -> (alias_or_name tr, select_columns cat q)
22102210+ | None -> (alias_or_name tr, cols_of tr.name)
22112211+ in
22122212+ let leading =
22132213+ match s.from with
22142214+ | Table tr -> [ of_ref tr ]
22152215+ | Select { fs_select; fs_alias } ->
22162216+ [ (Option.value fs_alias ~default:"", select_columns cat fs_select) ]
22172217+ | Values tuples ->
22182218+ let n = match tuples with r :: _ -> List.length r | [] -> 0 in
22192219+ [ ("", List.init n (fun i -> Fmt.str "column%d" (i + 1))) ]
22202220+ | Unit | Table_function _ -> []
22212221+ in
22222222+ leading @ List.map (fun (j : join) -> of_ref j.table) s.joins
22232223+22242224+(* Whether [s], read as a subquery under the column scope [bound] (alias -> its
22252225+ columns), references a FREE column -- one bound by neither [bound] nor [s]'s own
22262226+ FROM. A free column makes the subquery CORRELATED to an enclosing query, so it
22272227+ cannot be materialised on its own. A nested subquery extends the scope, so it is
22282228+ checked under [scope]. Conservative: a qualified column whose alias is unknown,
22292229+ or an unqualified column in no in-scope table, counts as free. *)
22302230+let rec correlated cat bound (s : select) =
22312231+ let scope = bound @ from_scope cat s in
22322232+ let col_free = function
22332233+ | Col (Some q, _) -> not (List.mem_assoc q scope)
22342234+ | Col (None, c) ->
22352235+ not (List.exists (fun (_, cols) -> List.mem c cols) scope)
22362236+ | _ -> false
22372237+ in
22382238+ let rec free e =
22392239+ match e with
22402240+ | Col _ -> col_free e
22412241+ | Lit _ | Param _ | Star | Qualified_star _ -> false
22422242+ | Unop_not x
22432243+ | Is_null x
22442244+ | Is_not_null x
22452245+ | Cast (x, _)
22462246+ | Collate (x, _)
22472247+ | Distinct_agg (_, x) ->
22482248+ free x
22492249+ | Binary (_, a, b) | Is (a, b) | Filter (a, b) -> free a || free b
22502250+ | In_list (x, l) -> free x || List.exists free l
22512251+ | Func (_, args) -> List.exists free args
22522252+ | Ordered_agg (x, ob) -> free x || List.exists (fun (e, _) -> free e) ob
22532253+ | Window (x, w) ->
22542254+ free x
22552255+ || List.exists free w.partition
22562256+ || List.exists (fun (e, _) -> free e) w.order
22572257+ | Case (base, branches, els) ->
22582258+ Option.fold ~none:false ~some:free base
22592259+ || List.exists (fun (c, r) -> free c || free r) branches
22602260+ || Option.fold ~none:false ~some:free els
22612261+ | Scalar_select sub | Exists sub -> correlated cat scope sub
22622262+ | In_select (x, sub) -> free x || correlated cat scope sub
22632263+ in
22642264+ List.exists (fun (rc : result_col) -> free rc.expr) s.cols
22652265+ || Option.fold ~none:false ~some:free s.where
22662266+ || Option.fold ~none:false ~some:free s.having
22672267+ || List.exists free s.group_by
22682268+ || List.exists (fun (e, _) -> free e) s.order_by
22692269+ || List.exists (fun (j : join) -> free j.on) s.joins
22702270+ || List.exists (fun (_, arm) -> correlated cat bound arm) s.compound
22712271+22722272+exception Subst_hard
22732273+(** Raised by {!subst_expr} on a node the simple substituter does not descend
22742274+ into (a nested subquery or a window), so the caller falls back. *)
22752275+22762276+(* Rebuild [e], applying [replace qual name e] to each column reference and
22772277+ recursing structurally everywhere else. A nested subquery / window raises
22782278+ {!Subst_hard}. *)
22792279+let rec subst_expr ~replace e =
22802280+ let go = subst_expr ~replace in
22812281+ match e with
22822282+ | Col (qual, name) -> replace qual name e
22832283+ | Scalar_select _ | In_select _ | Exists _ | Window _ -> raise Subst_hard
22842284+ | Lit _ | Param _ | Star | Qualified_star _ -> e
22852285+ | Unop_not a -> Unop_not (go a)
22862286+ | Binary (op, a, b) -> Binary (op, go a, go b)
22872287+ | Is (a, b) -> Is (go a, go b)
22882288+ | Is_null a -> Is_null (go a)
22892289+ | Is_not_null a -> Is_not_null (go a)
22902290+ | In_list (a, l) -> In_list (go a, List.map go l)
22912291+ | Func (n, args) -> Func (n, List.map go args)
22922292+ | Distinct_agg (n, a) -> Distinct_agg (n, go a)
22932293+ | Filter (a, b) -> Filter (go a, go b)
22942294+ | Ordered_agg (a, ob) ->
22952295+ Ordered_agg (go a, List.map (fun (e, d) -> (go e, d)) ob)
22962296+ | Cast (a, t) -> Cast (go a, t)
22972297+ | Collate (a, c) -> Collate (go a, c)
22982298+ | Case (b, br, el) ->
22992299+ Case
23002300+ ( Option.map go b,
23012301+ List.map (fun (c, r) -> (go c, go r)) br,
23022302+ Option.map go el )
23032303+23042304+(* Substitute each FREE column of subquery [s] -- a column bound by neither [s]'s
23052305+ own FROM (which shadows the outer query) nor a deeper scope -- with its value
23062306+ from [scope] (the enclosing row), turning a correlated subquery into an
23072307+ uncorrelated one that can run on its own. [None] when [s] is too tangled for the
23082308+ simple rule (a FROM-subquery, a nested subquery, or a window), so the caller
23092309+ evaluates it under [~scope] on the tree-walker instead. *)
23102310+let substitute_scope cat scope (s : select) : select option =
23112311+ let own = from_scope cat s in
23122312+ let own_aliases = List.map fst own in
23132313+ let own_cols = List.concat_map snd own in
23142314+ let bound qual name =
23152315+ match qual with
23162316+ | Some q -> List.mem q own_aliases
23172317+ | None -> List.mem name own_cols
23182318+ in
23192319+ (* Resolve a free column against [scope]. The alias matches case-insensitively
23202320+ (as the tree-walker's {!lookup} does, for the [OLD]/[NEW] pseudo-tables
23212321+ written in any case); the column name matches exactly, as {!frame_lookup}. *)
23222322+ let lookup qual name =
23232323+ let alias_eq q a =
23242324+ String.equal q a
23252325+ || String.equal (String.uppercase_ascii q) (String.uppercase_ascii a)
23262326+ in
23272327+ let rec find = function
23282328+ | (alias, cols) :: rest -> (
23292329+ match qual with
23302330+ | Some q when not (alias_eq q alias) -> find rest
23312331+ | _ -> (
23322332+ match List.assoc_opt name cols with
23332333+ | Some v -> Some v
23342334+ | None -> find rest))
23352335+ | [] -> None
23362336+ in
23372337+ find scope
23382338+ in
23392339+ (* A free column must resolve to a scope value; one that does not (a deeper
23402340+ correlation than [scope] carries) makes the whole substitution [None], so the
23412341+ caller keeps the tree-walker, rather than leaving an unbound column that would
23422342+ fail when the substituted select runs without a scope. *)
23432343+ let replace qual name e =
23442344+ if bound qual name then e
23452345+ else
23462346+ match lookup qual name with Some v -> Lit v | None -> raise Subst_hard
23472347+ in
23482348+ let sub = subst_expr ~replace in
23492349+ let sub_col (rc : Ast.result_col) = { rc with expr = sub rc.expr } in
23502350+ (* A FROM/join subquery is left untouched, which is sound only when it is
23512351+ uncorrelated; a lateral one (referencing the outer scope) falls back. *)
23522352+ let from_subq_ok q = not (correlated cat [] q) in
23532353+ try
23542354+ (match s.from with
23552355+ | Select { fs_select; _ } when not (from_subq_ok fs_select) ->
23562356+ raise Subst_hard
23572357+ | _ -> ());
23582358+ List.iter
23592359+ (fun (j : join) ->
23602360+ match j.table.subquery with
23612361+ | Some q when not (from_subq_ok q) -> raise Subst_hard
23622362+ | Some _ | None -> ())
23632363+ s.joins;
23642364+ Some
23652365+ {
23662366+ s with
23672367+ cols = List.map sub_col s.cols;
23682368+ where = Option.map sub s.where;
23692369+ group_by = List.map sub s.group_by;
23702370+ having = Option.map sub s.having;
23712371+ order_by = List.map (fun (e, d) -> (sub e, d)) s.order_by;
23722372+ joins = List.map (fun (j : join) -> { j with on = sub j.on }) s.joins;
23732373+ }
23742374+ with Subst_hard -> None
23752375+23762376+(* Replace each uncorrelated scalar / [IN (SELECT ...)] / [EXISTS] subquery in
23772377+ [s]'s scalar-expression positions with its materialised value, so the register
23782378+ VM -- which has no subquery opcode -- sees a literal or IN-list instead and
23792379+ compiles the outer query. A correlated subquery is left untouched (the VM falls
23802380+ back for it). The subquery is evaluated with the outer [params] (a [?] indexes
23812381+ the same array) but no row scope, which is exactly its value when uncorrelated;
23822382+ safe to evaluate eagerly because {!eval} is itself eager on AND/OR. *)
23832383+let hoist_subqueries ~run cat (s : select) : select =
23842384+ let first_col = function v :: _ -> v | [] -> Null in
23852385+ let scalar sub =
23862386+ match run sub with (v :: _) :: _ -> Lit v | _ -> Lit Null
23872387+ in
23882388+ let rec go e =
23892389+ match e with
23902390+ | Scalar_select sub when not (correlated cat [] sub) -> scalar sub
23912391+ | Exists sub when not (correlated cat [] sub) -> Lit (bool (run sub <> []))
23922392+ | In_select (x, sub) when not (correlated cat [] sub) ->
23932393+ In_list (go x, List.map (fun v -> Lit v) (List.map first_col (run sub)))
23942394+ | Lit _ | Param _ | Col _ | Star | Qualified_star _ | Scalar_select _
23952395+ | Exists _ ->
23962396+ e
23972397+ | In_select (x, sub) -> In_select (go x, sub)
23982398+ | Unop_not x -> Unop_not (go x)
23992399+ | Is_null x -> Is_null (go x)
24002400+ | Is_not_null x -> Is_not_null (go x)
24012401+ | Cast (x, t) -> Cast (go x, t)
24022402+ | Collate (x, c) -> Collate (go x, c)
24032403+ | Distinct_agg (n, x) -> Distinct_agg (n, go x)
24042404+ | Binary (op, a, b) -> Binary (op, go a, go b)
24052405+ | Is (a, b) -> Is (go a, go b)
24062406+ | Filter (a, b) -> Filter (go a, go b)
24072407+ | In_list (x, l) -> In_list (go x, List.map go l)
24082408+ | Func (n, args) -> Func (n, List.map go args)
24092409+ | Ordered_agg (x, ob) ->
24102410+ Ordered_agg (go x, List.map (fun (e, d) -> (go e, d)) ob)
24112411+ | Window (x, w) ->
24122412+ Window
24132413+ ( go x,
24142414+ {
24152415+ w with
24162416+ partition = List.map go w.partition;
24172417+ order = List.map (fun (e, d) -> (go e, d)) w.order;
24182418+ } )
24192419+ | Case (base, br, els) ->
24202420+ Case
24212421+ ( Option.map go base,
24222422+ List.map (fun (c, r) -> (go c, go r)) br,
24232423+ Option.map go els )
24242424+ in
24252425+ {
24262426+ s with
24272427+ cols =
24282428+ List.map (fun (rc : result_col) -> { rc with expr = go rc.expr }) s.cols;
24292429+ where = Option.map go s.where;
24302430+ group_by = List.map go s.group_by;
24312431+ having = Option.map go s.having;
24322432+ order_by = List.map (fun (e, d) -> (go e, d)) s.order_by;
24332433+ joins = List.map (fun (j : join) -> { j with on = go j.on }) s.joins;
24342434+ }
24352435+24362436+(* The join-row environments of a bare [FROM from joins] (no projection, no
24372437+ WHERE) -- each is a [binding list] usable as an outer [~scope]. This drives
24382438+ [UPDATE ... FROM], whose auxiliary tables are visible to SET and WHERE. *)
24392439+let from_bindings ?(scope = []) cat params from joins =
24402440+ let s =
24412441+ {
24422442+ ctes = [];
24432443+ distinct = false;
24442444+ cols = [ { expr = Star; alias = None } ];
24452445+ from;
24462446+ joins;
24472447+ where = None;
24482448+ group_by = [];
24492449+ having = None;
24502450+ compound = [];
24512451+ order_by = [];
24522452+ limit = None;
24532453+ offset = None;
24542454+ }
24552455+ in
24562456+ List.map bindings_of_frames (combine ~scope cat params s)
24572457+24582458+(* The access path the executor takes, one line per table. Structural (no
24592459+ parameter values): the from table uses an index when one covers its
24602460+ equalities (the same columns {!from_access} narrows on, so EXPLAIN and run
24612461+ agree), and joins are nested-loop scans. *)
24622462+let explain cat (s : Ast.select) =
24632463+ let line tbl = function
24642464+ | Some descr -> Fmt.str "SEARCH %s USING %s" tbl descr
24652465+ | None -> Fmt.str "SCAN %s" tbl
24662466+ in
24672467+ let from_alias =
24682468+ match s.from with
24692469+ | Unit -> ""
24702470+ | Table tr -> Option.value tr.alias ~default:tr.name
24712471+ | Select { fs_alias; _ } -> Option.value fs_alias ~default:""
24722472+ | Values _ -> ""
24732473+ | Table_function { tf_name; tf_alias; _ } ->
24742474+ Option.value tf_alias ~default:tf_name
24752475+ in
24762476+ let from_line =
24772477+ match s.from with
24782478+ | Unit -> "SCAN (constant)"
24792479+ | Table tr ->
24802480+ line tr.name (cat.index_for tr.name (eq_columns from_alias s.where))
24812481+ | Select _ -> "SCAN (subquery)"
24822482+ | Values _ -> "SCAN (values)"
24832483+ | Table_function { tf_name; _ } -> Fmt.str "SCAN (%s)" tf_name
24842484+ in
24852485+ let join_line (j : Ast.join) =
24862486+ line j.table.name
24872487+ (cat.index_for j.table.name (join_eq_columns (join_alias j) j.on))
24882488+ in
24892489+ (* Explain the same order the executor runs (cost-based for inner joins). *)
24902490+ String.concat "\n"
24912491+ (from_line :: List.map join_line (plan_joins cat from_alias s.joins))
24922492+24932493+(* ── Write-path evaluation ────────────────────────────────────── *)
24942494+24952495+(* Evaluate an expression with no row in scope -- for INSERT ... VALUES, whose
24962496+ tuples are constants and bound parameters. A column reference here raises
24972497+ (no table is in scope), which is the correct error. *)
24982498+let eval_const ?(scope = []) cat params e =
24992499+ eval cat params (frames_of_scope scope) e
25002500+25012501+(* Does [vals] (a row of [table], aligned to [cols]) satisfy [where]? Used to
25022502+ pick the rows a DELETE removes. No WHERE matches every row. *)
25032503+let row_matches ?(scope = []) ?rowid cat params table cols where vals =
25042504+ match where with
25052505+ | None -> true
25062506+ | Some w ->
25072507+ let env = Pos (table, cols, vals, rowid) :: frames_of_scope scope in
25082508+ tri_of (eval cat params env w) = T
25092509+25102510+(* Evaluate [e] against a single [table] row ([vals] aligned to [cols]) -- the
25112511+ right-hand side of an UPDATE ... SET assignment, which may read the row's
25122512+ current column values. *)
25132513+let eval_in_row ?(scope = []) ?rowid cat params table cols vals e =
25142514+ eval cat params (Pos (table, cols, vals, rowid) :: frames_of_scope scope) e
25152515+25162516+(* Evaluate an UPSERT DO UPDATE expression: the conflicting row's current values
25172517+ are in scope under [table], and the would-be-inserted values under the
25182518+ [excluded] pseudo-table (both aligned to [cols]). *)
25192519+let eval_upsert ?(scope = []) cat params ~table ~cols ~current ~excluded e =
25202520+ eval cat params
25212521+ (Pos (table, cols, current, None)
25222522+ :: Pos ("excluded", cols, excluded, None)
25232523+ :: frames_of_scope scope)
25242524+ e
25252525+25262526+(* Project a RETURNING column list over each affected [row] of [table] ([cols]
25272527+ are the table's column names, aligned to [row]). [*] expands to the whole
25282528+ row; other items are expressions over the row. *)
25292529+let returning_rows cat params ~table ~cols returning rows =
25302530+ List.map
25312531+ (fun row ->
25322532+ let env = [ Pos (table, cols, row, None) ] in
25332533+ List.concat_map
25342534+ (fun (rc : Ast.result_col) ->
25352535+ match rc.expr with
25362536+ | Star | Qualified_star _ -> row
25372537+ | e -> [ eval cat params env e ])
25382538+ returning)
25392539+ rows
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Storage-agnostic SQL query executor.
77+88+ Operates over a {!catalog} that exposes a table's column names and rows, so
99+ the evaluation logic (comparisons, [LIKE], joins, ordering) is independent
1010+ of the storage backend and can be tested in isolation. The catalog speaks
1111+ the engine core's {!Catalog.Value.t} -- so a storage adapter need not know
1212+ the SQL frontend's value type -- and the executor converts at that boundary.
1313+*)
1414+1515+type catalog = {
1616+ columns : string -> string list;
1717+ (** Column names of a table, in declared order. *)
1818+ rows : string -> Catalog.Value.t list list;
1919+ (** Rows of a table, each aligned to {!columns}. *)
2020+ rows_rowid : string -> (int64 option * Catalog.Value.t list) list;
2121+ (** Each row paired with its rowid, or [None] for a row source without one
2222+ (a view, a derived table, [VALUES]) -- lets the executor resolve the
2323+ implicit [rowid]/[oid]/[_rowid_] column of a full table scan. *)
2424+ index_scan :
2525+ string ->
2626+ (string * Catalog.Value.t) list ->
2727+ (string * Catalog.Value.t list list) option;
2828+ (** [index_scan table eqs] returns [Some (index_name, rows)] when an index
2929+ on [table] serves the column-equality bindings [eqs], or [None] for a
3030+ full {!rows} scan. The rows are still re-filtered, so this is a pure
3131+ optimisation. *)
3232+ index_for : string -> string list -> string option;
3333+ (** [index_for table cols] names a unique index on [table] all of whose
3434+ columns are among [cols] -- the structural counterpart of
3535+ {!index_scan}, used by {!explain}. *)
3636+ collation : string -> string -> string option;
3737+ (** [collation table col] is [col]'s declared [COLLATE] sequence
3838+ (uppercase) on [table], or [None] for the default binary / a derived
3939+ source. *)
4040+}
4141+4242+val of_catalog : Catalog.t -> catalog
4343+(** [of_catalog cat] adapts a {!Catalog.t} to the SQL frontend's read/explain
4444+ view. Backends such as Irmin or Parquet can implement {!Catalog.Spi.S},
4545+ build a catalog handle, and pass it through this adapter before calling
4646+ {!exec_select}. *)
4747+4848+val number_params : Ast.stmt -> Ast.stmt
4949+(** [number_params stmt] assigns each [?] placeholder its zero-based index in
5050+ left-to-right source order. *)
5151+5252+type binding = string * (string * Ast.value) list
5353+(** One named row fragment, [(alias, [(column, value); ...])]. A [binding list]
5454+ passed as [?scope] is an outer environment placed at the tail of the row's
5555+ own bindings, so inner columns shadow it for unqualified names but a
5656+ qualified [alias.col] still resolves. Used for correlated subqueries (the
5757+ enclosing row) and for a trigger's [OLD]/[NEW] rows. *)
5858+5959+val exec_select :
6060+ ?scope:binding list ->
6161+ catalog ->
6262+ Ast.select ->
6363+ Ast.value array ->
6464+ Ast.value list list
6565+(** [exec_select cat select params] runs [select], substituting [params.(i)] for
6666+ the [i]-th placeholder. Returns the result rows in output order. [?scope] is
6767+ an outer environment (e.g. a trigger's OLD/NEW rows) visible to the query.
6868+ @raise Failure on unknown tables/columns or unsupported functions. *)
6969+7070+val set_cat_subquery_runner :
7171+ (catalog ->
7272+ (string * (string * Ast.value) list) list ->
7373+ Ast.select ->
7474+ Ast.value array ->
7575+ Ast.value list list) ->
7676+ unit
7777+(** Install the runner [eval] uses for a scalar / [IN] / [EXISTS] subquery:
7878+ given the enclosing-row {!type-catalog}, the outer scope, the subquery, and
7979+ the params, it returns the subquery's rows. The default is {!exec_select}
8080+ (the tree-walker over that catalog); the engine installs one that runs the
8181+ subquery on the catalog VM -- reading the {e same} catalog, so a CTE or
8282+ derived table materialised into it is visible -- falling back to
8383+ {!exec_select} for a shape the VM cannot compile. *)
8484+8585+val from_bindings :
8686+ ?scope:binding list ->
8787+ catalog ->
8888+ Ast.value array ->
8989+ Ast.from_clause ->
9090+ Ast.join list ->
9191+ binding list list
9292+(** [from_bindings cat params from joins] is the join-row environments of a bare
9393+ [FROM from joins] (no projection, no WHERE), each a [binding list] usable as
9494+ an outer [~scope]. Drives [UPDATE ... FROM]. [?scope] adds outer bindings.
9595+*)
9696+9797+val materialize_ctes : catalog -> Ast.value array -> Ast.cte list -> catalog
9898+(** [materialize_ctes cat params ctes] runs each [WITH] table expression
9999+ (semi-naive evaluation for a recursive one) and binds its rows into [cat]
100100+ under its name, returning the extended catalog. Lets a [WITH] on
101101+ [UPDATE]/[DELETE] put its CTEs in scope for the statement's subqueries. *)
102102+103103+val resolve_collations : catalog -> Ast.select -> Ast.select
104104+(** Make each base-table column's declared [COLLATE] sequence explicit in [s]'s
105105+ comparisons and ORDER BY keys (wrapping the column in a [Collate] node by
106106+ sqlite3's left-operand rule), so the collation machinery honours it. The
107107+ tree-walker applies this itself; the register VM calls it so a compiled scan
108108+ over a collated column falls back rather than comparing binary. *)
109109+110110+val resolve_where_aliases : catalog -> Ast.select -> Ast.select
111111+(** Substitute a select-list alias referenced in [s]'s WHERE or ORDER BY with
112112+ the aliased expression -- so [SELECT a AS x FROM t WHERE x > 1] resolves [x]
113113+ (an alias is only substituted when it does not name a real source column,
114114+ which shadows it, matching sqlite3). The tree-walker applies this itself;
115115+ the register VM calls it so a compiled scan resolves the alias too. *)
116116+117117+val substitute_scope :
118118+ catalog -> binding list -> Ast.select -> Ast.select option
119119+(** [substitute_scope cat scope s] replaces each free (correlated) column of
120120+ subquery [s] -- one bound by neither its own FROM nor a deeper scope -- with
121121+ its value from [scope] (the enclosing row), turning a correlated subquery
122122+ into an uncorrelated one a caller can run on its own. [None] when [s] is too
123123+ tangled for the simple rule (a FROM-subquery, a nested subquery, or a
124124+ window), so the caller evaluates it under [~scope] on the tree-walker
125125+ instead. *)
126126+127127+val hoist_subqueries :
128128+ run:(Ast.select -> Ast.value list list) -> catalog -> Ast.select -> Ast.select
129129+(** Replace each uncorrelated scalar / [IN (SELECT ...)] / [EXISTS] subquery in
130130+ [s]'s scalar-expression positions (select list, WHERE, GROUP BY, HAVING,
131131+ ORDER BY, join ON) with its materialised value -- a literal, or an [IN]
132132+ value list. A correlated subquery (one referencing an enclosing query's
133133+ columns) is left intact. [run] materialises an uncorrelated subquery to its
134134+ rows; the engine passes a runner that executes it on the catalog VM. The
135135+ register VM has no subquery opcode, so it calls this to compile the outer
136136+ query of an uncorrelated subquery rather than fall back. Sound because an
137137+ uncorrelated subquery is constant. *)
138138+139139+val resolve_ordinals : catalog -> Ast.select -> Ast.select
140140+(** Rewrite [s]'s ORDER BY and GROUP BY so a 1-based output-column ordinal
141141+ ([ORDER BY 2] / [GROUP BY 2]) becomes the expression of that output column.
142142+ The tree-walker applies this itself; the register VM calls it so a compiled
143143+ ORDER BY/GROUP BY ordinal works too. *)
144144+145145+val select_columns : catalog -> Ast.select -> string list
146146+(** [select_columns cat s] is the output column names of [s]: each item's alias,
147147+ else a bare column's name, else [columnN]; [*] expands the source table's
148148+ columns. Used to name the columns of a [CREATE TABLE ... AS] target. *)
149149+150150+val aggregate_compilable : Ast.select -> bool
151151+(** Whether [s] is an aggregate/GROUP BY select that {!aggregate_scan} can run
152152+ from rowid-free scanned rows: it groups or aggregates or has HAVING, uses no
153153+ window function, and references no [rowid]. *)
154154+155155+val window_compilable : Ast.select -> bool
156156+(** Whether [s] has a window function over a [rowid]-free projection, so its
157157+ (filtered, joined) source rows can be scanned by the register VM and then
158158+ post-processed by {!aggregate_scan} (which reaches {!window_project}). *)
159159+160160+val aggregate_scan :
161161+ catalog ->
162162+ Ast.value array ->
163163+ Ast.select ->
164164+ (string * string list) list ->
165165+ Ast.value list list ->
166166+ Ast.value list list
167167+(** [aggregate_scan cat params s segments rows] runs an aggregate/GROUP BY [s]
168168+ from its already-scanned source [rows]: it builds the row environments and
169169+ reuses the grouping, aggregation, HAVING, ORDER BY, DISTINCT, and
170170+ OFFSET/LIMIT of {!exec_select}. [segments] is the join's tables in FROM
171171+ order as [(alias, cols)]; each row is the concatenation of those tables'
172172+ columns in that order and is sliced back into one binding per table (a
173173+ single-table aggregate is a one-element [segments]). Lets the register VM do
174174+ the scan and reuse this aggregation. *)
175175+176176+val combine_compound :
177177+ catalog ->
178178+ Ast.value array ->
179179+ Ast.select ->
180180+ Ast.value list list ->
181181+ (Ast.set_op * Ast.value list list) list ->
182182+ Ast.value list list
183183+(** [combine_compound cat params s base_rows compound_rows] combines a compound
184184+ select's already-computed arm rows -- [base_rows] for the leading arm,
185185+ [(op, rows)] per following arm -- as {!exec_select} does: fold the set
186186+ operators, apply [s]'s ORDER BY over the output columns, then OFFSET/LIMIT.
187187+ Lets a caller produce each arm's rows itself (e.g. via the register VM) and
188188+ reuse this orchestration. *)
189189+190190+val explain : catalog -> Ast.select -> string
191191+(** [explain cat select] describes the access path the executor takes, one line
192192+ per table ([SCAN t] or [SEARCH t USING INDEX i]). Structural -- it uses the
193193+ same index decision as {!exec_select}, independent of parameter values. *)
194194+195195+val is_agg : string -> Ast.expr list -> bool
196196+(** [is_agg name args] is whether [name] (with that argument count) is an
197197+ aggregate -- [count]/[sum]/[avg]/..., or [min]/[max] with at most one
198198+ argument. An aggregate is folded over a group, not applied per row. *)
199199+200200+val eval_const :
201201+ ?scope:binding list -> catalog -> Ast.value array -> Ast.expr -> Ast.value
202202+(** [eval_const cat params e] evaluates [e] with no table row in scope, for
203203+ [INSERT ... VALUES] tuples. A column reference raises [Failure] unless it
204204+ resolves in [?scope] (a trigger's OLD/NEW rows). *)
205205+206206+val row_matches :
207207+ ?scope:binding list ->
208208+ ?rowid:int64 ->
209209+ catalog ->
210210+ Ast.value array ->
211211+ string ->
212212+ string list ->
213213+ Ast.expr option ->
214214+ Ast.value list ->
215215+ bool
216216+(** [row_matches cat params table cols where vals] is whether the row [vals] of
217217+ [table] (aligned to [cols]) satisfies [where]. [None] matches every row.
218218+ Used to select the rows a [DELETE] removes. [?scope] adds outer bindings;
219219+ [?rowid] is the row's rowid, so [where] may reference the implicit rowid
220220+ column. *)
221221+222222+val eval_in_row :
223223+ ?scope:binding list ->
224224+ ?rowid:int64 ->
225225+ catalog ->
226226+ Ast.value array ->
227227+ string ->
228228+ string list ->
229229+ Ast.value list ->
230230+ Ast.expr ->
231231+ Ast.value
232232+(** [eval_in_row cat params table cols vals e] evaluates [e] against the single
233233+ [table] row [vals] (aligned to [cols]) -- the right-hand side of an
234234+ [UPDATE ... SET] assignment, which may reference the row's columns. [?scope]
235235+ adds outer bindings (a trigger's OLD/NEW rows); [?rowid] is the row's rowid,
236236+ so [e] may reference the implicit rowid column. *)
237237+238238+val eval_upsert :
239239+ ?scope:binding list ->
240240+ catalog ->
241241+ Ast.value array ->
242242+ table:string ->
243243+ cols:string list ->
244244+ current:Ast.value list ->
245245+ excluded:Ast.value list ->
246246+ Ast.expr ->
247247+ Ast.value
248248+(** [eval_upsert cat params ~table ~cols ~current ~excluded e] evaluates an
249249+ [UPSERT] [DO UPDATE] expression with the conflicting row's [current] values
250250+ in scope under [table] and the would-be-inserted [excluded] values under the
251251+ [excluded] pseudo-table (both aligned to [cols]). *)
252252+253253+val returning_rows :
254254+ catalog ->
255255+ Ast.value array ->
256256+ table:string ->
257257+ cols:string list ->
258258+ Ast.result_col list ->
259259+ Ast.value list list ->
260260+ Ast.value list list
261261+(** [returning_rows cat params ~table ~cols returning rows] projects a
262262+ [RETURNING] column list over each affected [row] of [table] ([cols] are the
263263+ table's column names, aligned to [row]); [*] expands to the whole row. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** Render the query AST back to SQL text — the inverse of the parser and the
77+ sibling of the executor. The output re-parses to the same query and runs
88+ unchanged through the sqlite3 CLI. *)
99+1010+val pp_expr : Ast.expr Fmt.t
1111+(** Render an expression, fully parenthesised. *)
1212+1313+val pp_select : Ast.select Fmt.t
1414+(** [pp_select] is the pretty-printer for a [SELECT] statement. *)
1515+1616+val expr : Ast.expr -> string
1717+(** [expr e] is {!pp_expr} as a string. *)
1818+1919+val select : Ast.select -> string
2020+(** [select s] is {!pp_select} as a string. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2026 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(* SQLite's value-level binary operators keyed by the AST's {!Ast.binop}: the
77+ comparison value (with column affinity), the opcode-free operators
88+ (AND/OR/LIKE/GLOB/||/JSON), and [IN (list)] membership. Built on the
99+ AST-free {!Value_ops} and the function layer {!Func}; shared by the
1010+ tree-walker and the register VM so an operator's value agrees in both. *)
1111+1212+open Ast
1313+open Value_ops
1414+1515+(* A comparison's value: NULL if either operand is NULL, else 1/0 for the
1616+ ordering under the optional collation. *)
1717+let compare_op ?coll op a b =
1818+ if a = Null || b = Null then Null
1919+ else
2020+ let c =
2121+ match coll with
2222+ | Some name -> compare_values_coll name a b
2323+ | None -> compare_values a b
2424+ in
2525+ match op with
2626+ | Eq -> bool (c = 0)
2727+ | Neq -> bool (c <> 0)
2828+ | Lt -> bool (c < 0)
2929+ | Le -> bool (c <= 0)
3030+ | Gt -> bool (c > 0)
3131+ | Ge -> bool (c >= 0)
3232+ | And | Or | Like | Glob | Add | Sub | Mul | Div | Mod | Bit_and | Bit_or
3333+ | Shl | Shr | Concat | Json_get | Json_get_text ->
3434+ assert false
3535+3636+(* [v IN (candidates)] as a three-valued membership: an empty set is always
3737+ false (so [NOT IN ()] is always true, even for a NULL left operand); else a
3838+ NULL left operand is NULL; else 1 if any candidate compares equal, NULL if
3939+ none match but a NULL candidate is present, 0 otherwise. *)
4040+let in_eval ?coll v candidates =
4141+ if candidates = [] then Int 0L
4242+ else if v = Null then Null
4343+ else
4444+ let cmp = List.map (fun x -> compare_op ?coll Eq v x) candidates in
4545+ if List.exists (fun r -> r = Int 1L) cmp then Int 1L
4646+ else if List.exists (fun r -> r = Null) cmp then Null
4747+ else Int 0L
4848+4949+(* A comparison's value with SQLite's column affinity: a numeric-valued column
5050+ coerces a well-formed text operand to a number ([c = '5'] matches when [c] is
5151+ numeric), while two literals get no affinity. [a_col]/[b_col] mark whether
5252+ each operand is a column. *)
5353+let compare_with_affinity ?coll ~a_col ~b_col op va vb =
5454+ let va, vb =
5555+ match (a_col, va, b_col, vb) with
5656+ | true, (Int _ | Float _), _, (Text _ | Blob _) -> (va, num_coerce vb)
5757+ | _, (Text _ | Blob _), true, (Int _ | Float _) -> (num_coerce va, vb)
5858+ | _ -> (va, vb)
5959+ in
6060+ compare_op ?coll op va vb
6161+6262+(* The value of a binary operator with no per-element opcode and no affinity:
6363+ the three-valued AND/OR (both operands evaluated -- SQLite's AND/OR are pure
6464+ 3VL, not short-circuit), LIKE/GLOB pattern tests, [||] concatenation, and the
6565+ JSON [->]/[->>] accessors. Arithmetic and comparison are handled elsewhere. *)
6666+let binop_value op va vb =
6767+ match op with
6868+ | And -> value_of_tri (tri_and (tri_of va) (tri_of vb))
6969+ | Or -> value_of_tri (tri_or (tri_of va) (tri_of vb))
7070+ | Like -> like_op va vb
7171+ | Glob -> glob_op va vb
7272+ | Concat -> concat_op va vb
7373+ | Json_get -> Func.json_arrow ~as_text:false va vb
7474+ | Json_get_text -> Func.json_arrow ~as_text:true va vb
7575+ | Eq | Neq | Lt | Le | Gt | Ge | Add | Sub | Mul | Div | Mod | Bit_and
7676+ | Bit_or | Shl | Shr ->
7777+ assert false
7878+7979+(* The value of [v IN (cands)] with SQLite's affinity: a numeric left column
8080+ ([e_col] with a numeric [v]) coerces each text candidate to a number, then
8181+ the three-valued {!in_eval} decides membership. *)
8282+let in_list_value ?coll ~e_col v cands =
8383+ let cands =
8484+ match (e_col, v) with
8585+ | true, (Int _ | Float _) -> List.map num_coerce cands
8686+ | _ -> cands
8787+ in
8888+ in_eval ?coll v cands
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2026 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** SQLite's value-level binary operators, keyed by the AST's {!Ast.binop}.
77+88+ The comparison value (with column affinity and collation), the opcode-free
99+ operators (AND/OR/LIKE/GLOB/[||]/JSON), and [IN (list)] membership, over
1010+ {!Value_ops} and {!Func}. Shared by the tree-walker and the register VM so
1111+ an operator's value agrees in both. *)
1212+1313+val compare_op :
1414+ ?coll:string -> Ast.binop -> Ast.value -> Ast.value -> Ast.value
1515+(** [compare_op op a b] is a comparison's value: NULL if either operand is NULL,
1616+ else [1]/[0] for the ordering under the optional collation [coll]. *)
1717+1818+val in_eval : ?coll:string -> Ast.value -> Ast.value list -> Ast.value
1919+(** [in_eval v candidates] is [v IN (candidates)] as a three-valued membership:
2020+ an empty set is [0] (so [NOT IN ()] is [1]); a NULL [v] over a non-empty set
2121+ is NULL; else [1] on a match, NULL if none match but a NULL candidate is
2222+ present, [0] otherwise. *)
2323+2424+val compare_with_affinity :
2525+ ?coll:string ->
2626+ a_col:bool ->
2727+ b_col:bool ->
2828+ Ast.binop ->
2929+ Ast.value ->
3030+ Ast.value ->
3131+ Ast.value
3232+(** [compare_with_affinity ~a_col ~b_col op a b] is the comparison value
3333+ applying SQLite's column affinity: a numeric column coerces a well-formed
3434+ text operand to a number ([c = '5'] matches a numeric [c]), two literals get
3535+ no affinity. [a_col]/[b_col] mark whether each operand is a column. *)
3636+3737+val binop_value : Ast.binop -> Ast.value -> Ast.value -> Ast.value
3838+(** [binop_value op a b] is the value of an opcode-free binary operator: the
3939+ three-valued AND/OR, LIKE/GLOB, [||] concatenation, and the JSON accessors.
4040+ Arithmetic and comparison operators do not reach here. *)
4141+4242+val in_list_value :
4343+ ?coll:string -> e_col:bool -> Ast.value -> Ast.value list -> Ast.value
4444+(** [in_list_value ~e_col v cands] is [v IN (cands)] with affinity: a numeric
4545+ left column [e_col] coerces each text candidate to a number, then {!in_eval}
4646+ decides membership. *)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2026 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+open Catalog.Value
77+88+let type_rank = function
99+ | Null -> 0
1010+ | Int _ | Float _ -> 1
1111+ | Text _ -> 2
1212+ | Blob _ -> 3
1313+1414+let float_of_value = function
1515+ | Int i -> Int64.to_float i
1616+ | Float f -> f
1717+ | _ -> invalid_arg "float_of_value"
1818+1919+(* Exact order of an int64 [i] against a float [r] without the precision loss of
2020+ widening [i] to a double (sqlite3IntFloatCompare): a float of magnitude >= 2^63
2121+ is outside int64 range and decides by sign; otherwise compare against the
2222+ truncated float, breaking a tie by [i] rendered back as a double. So
2323+ [9223372036854775807] sorts below the double [9223372036854775808.0] rather
2424+ than rounding up to equal it. *)
2525+let int_float_compare i r =
2626+ let two63 = 0x1p63 in
2727+ if Float.is_nan r then 1
2828+ else if r >= two63 then -1
2929+ else if r < -.two63 then 1
3030+ else
3131+ let y = Int64.of_float r in
3232+ let c = Int64.compare i y in
3333+ if c <> 0 then c else Float.compare (Int64.to_float i) r
3434+3535+let compare_values a b =
3636+ match (a, b) with
3737+ | Null, Null -> 0
3838+ | Int x, Int y -> Int64.compare x y
3939+ | Int x, Float y -> int_float_compare x y
4040+ | Float x, Int y -> -int_float_compare y x
4141+ | Float x, Float y -> Float.compare x y
4242+ | Text x, Text y -> String.compare x y
4343+ | Blob x, Blob y -> String.compare x y
4444+ | _ -> Int.compare (type_rank a) (type_rank b)
4545+4646+(* The canonical bytes a collation compares by: BINARY is the string itself,
4747+ NOCASE folds ASCII case, RTRIM drops trailing spaces. Two texts collate equal
4848+ exactly when their keys are byte-equal, so it also drives a collation-aware
4949+ DISTINCT. An unknown name is an error, as in sqlite3. *)
5050+let collate_key name s =
5151+ match name with
5252+ | "BINARY" -> s
5353+ | "NOCASE" -> String.lowercase_ascii s
5454+ | "RTRIM" ->
5555+ let n = ref (String.length s) in
5656+ while !n > 0 && s.[!n - 1] = ' ' do
5757+ decr n
5858+ done;
5959+ String.sub s 0 !n
6060+ | _ -> Fmt.failwith "no such collating sequence: %s" name
6161+6262+let collate_text name x y =
6363+ String.compare (collate_key name x) (collate_key name y)
6464+6565+(* [compare_values] with a named collation applied to a text/text comparison.
6666+ The collation is irrelevant to non-text operands, which compare as usual. *)
6767+let compare_values_coll name a b =
6868+ match (a, b) with
6969+ | Text x, Text y -> collate_text name x y
7070+ | _ -> compare_values a b
7171+7272+(* A value's canonical form under collation [name]: text is mapped so two values
7373+ equal under the collation share a form (and order by it), anything else is
7474+ unchanged. Lets a caller bucket / dedup by a collated key with plain equality.
7575+ An unknown collation raises [Not_found] so the caller can fall back. *)
7676+let collation_key name v =
7777+ match v with
7878+ | Text s -> (
7979+ try Text (collate_key name s) with Failure _ -> raise Not_found)
8080+ | _ -> v
8181+8282+(* Render a float as the sqlite3 CLI does: a whole value with one decimal
8383+ ("2.0"), otherwise C [%.15g] (15 significant digits, the precision sqlite3
8484+ prints), forcing a decimal point so it reads back as a real. *)
8585+let text_of_float f =
8686+ if Float.is_nan f then "NULL"
8787+ else if f = Float.infinity then "Inf"
8888+ else if f = Float.neg_infinity then "-Inf"
8989+ else if Float.is_integer f && Float.abs f < 1e15 then Fmt.str "%.1f" f
9090+ else
9191+ let s = Fmt.str "%.15g" f in
9292+ if String.exists (function '.' | 'e' | 'E' -> true | _ -> false) s then s
9393+ else s ^ ".0"
9494+9595+let text_of_value = function
9696+ | Text s | Blob s -> s
9797+ | Int i -> Int64.to_string i
9898+ | Float f -> text_of_float f
9999+ | Null -> ""
100100+101101+(* Three-valued logic for WHERE/AND/OR/NOT/HAVING: a value's truth is unknown
102102+ ([U]) for NULL, true/false for a non-zero/zero number, and a text/blob is
103103+ coerced by its leading numeric prefix (so ['abc'] and [''] are false, ['5x']
104104+ is true) -- the same coercion as numeric affinity, as sqlite3's [if] test. *)
105105+type tri = T | F | U
106106+107107+let tri_of v =
108108+ match v with
109109+ | Null -> U
110110+ | Int 0L -> F
111111+ | Int _ -> T
112112+ | Float f -> if f = 0. then F else T
113113+ | Text _ | Blob _ -> (
114114+ match leading_number (text_of_value v) with
115115+ | Int 0L | Float 0. -> F
116116+ | _ -> T)
117117+118118+let value_of_tri = function T -> Int 1L | F -> Int 0L | U -> Null
119119+let tri_and a b = match (a, b) with F, _ | _, F -> F | T, T -> T | _ -> U
120120+let tri_or a b = match (a, b) with T, _ | _, T -> T | F, F -> F | _ -> U
121121+let tri_not = function T -> F | F -> T | U -> U
122122+let not_value v = value_of_tri (tri_not (tri_of v))
123123+let bool v = if v then Int 1L else Int 0L
124124+125125+(* SQLite's NULL-safe equality [a IS b]: two NULLs are equal, a NULL and a
126126+ non-NULL are not, else an ordinary value comparison (no affinity). Always 1 or
127127+ 0, never NULL. *)
128128+let is_value va vb =
129129+ bool
130130+ (match (va, vb) with
131131+ | Null, Null -> true
132132+ | Null, _ | _, Null -> false
133133+ | _ -> compare_values va vb = 0)
134134+135135+(* SUM/AVG/MIN/MAX/TOTAL over [vals], the non-NULL argument values of a group
136136+ (lang_aggfunc.html): SUM is integer when every value is, AVG and TOTAL are
137137+ real; SUM/MIN/MAX over no values is NULL, TOTAL is 0.0. *)
138138+let numeric_agg name vals =
139139+ (* SUM over all-integer values stays an integer, but -- unlike [+], which
140140+ promotes on overflow -- sqlite3 raises "integer overflow" if the int64 sum
141141+ overflows (TOTAL and a mixed/real SUM accumulate as float and never do). *)
142142+ let sum_int () =
143143+ let add a v =
144144+ let x = match v with Int i -> i | _ -> 0L in
145145+ let r = Int64.add a x in
146146+ if a >= 0L = (x >= 0L) && r >= 0L <> (a >= 0L) then
147147+ failwith "integer overflow"
148148+ else r
149149+ in
150150+ Int (List.fold_left add 0L vals)
151151+ in
152152+ let sum_float () =
153153+ List.fold_left (fun a v -> a +. float_of_value v) 0. vals
154154+ in
155155+ let all_int = List.for_all (function Int _ -> true | _ -> false) vals in
156156+ let extreme keep = function
157157+ | [] -> Null
158158+ | v :: vs ->
159159+ List.fold_left
160160+ (fun a x -> if keep (compare_values x a) then x else a)
161161+ v vs
162162+ in
163163+ match name with
164164+ | "sum" ->
165165+ if vals = [] then Null
166166+ else if all_int then sum_int ()
167167+ else Float (sum_float ())
168168+ | "total" -> Float (sum_float ())
169169+ | "avg" ->
170170+ if vals = [] then Null
171171+ else Float (sum_float () /. float_of_int (List.length vals))
172172+ | "min" -> extreme (fun c -> c < 0) vals
173173+ | "max" -> extreme (fun c -> c > 0) vals
174174+ | _ -> Fmt.failwith "unknown aggregate %s()" name
175175+176176+(* The distinct values of [vs], by {!compare_values} equality, keeping the first
177177+ occurrence -- a [DISTINCT] aggregate's argument values. *)
178178+let distinct_values vs =
179179+ let seen = ref [] in
180180+ List.filter
181181+ (fun v ->
182182+ if List.exists (fun u -> compare_values u v = 0) !seen then false
183183+ else (
184184+ seen := v :: !seen;
185185+ true))
186186+ vs
187187+188188+(* Whether [s] contains the substring [sub]. *)
189189+let contains_sub s sub = Re.execp Re.(compile (str sub)) s
190190+191191+(* A declared type name's storage affinity (datatype3.html sec.3.1): a name
192192+ containing INT is INTEGER, CHAR/CLOB/TEXT is TEXT, BLOB or "" is BLOB,
193193+ REAL/FLOA/DOUB is REAL, anything else NUMERIC. *)
194194+let affinity_of_type ty =
195195+ let u = String.uppercase_ascii ty in
196196+ if contains_sub u "INT" then `Integer
197197+ else if
198198+ contains_sub u "CHAR" || contains_sub u "CLOB" || contains_sub u "TEXT"
199199+ then `Text
200200+ else if contains_sub u "BLOB" || ty = "" then `Blob
201201+ else if
202202+ contains_sub u "REAL" || contains_sub u "FLOA" || contains_sub u "DOUB"
203203+ then `Real
204204+ else `Numeric
205205+206206+(* Apply a column's storage affinity to a value being written (datatype3 sec.3):
207207+ NUMERIC/INTEGER read a well-formed numeric TEXT as an integer (or real when it
208208+ is fractional or too big for int64), TEXT renders a number as text, REAL
209209+ coerces a number to float; a BLOB, NULL, and non-numeric text are unchanged.
210210+ [ty] is the column's declared type text ([""] = no affinity). *)
211211+let apply_affinity ty v =
212212+ let numify s =
213213+ let s = String.trim s in
214214+ match Int64.of_string_opt s with
215215+ | Some i -> Int i
216216+ | None -> (
217217+ match float_of_string_opt s with Some f -> Float f | None -> v)
218218+ in
219219+ match (affinity_of_type ty, v) with
220220+ | _, Null -> v
221221+ | `Blob, _ -> v
222222+ | `Text, (Int _ | Float _) -> Text (text_of_value v)
223223+ | `Text, _ -> v
224224+ | (`Numeric | `Integer), Text s -> numify s
225225+ | (`Numeric | `Integer), _ -> v
226226+ | `Real, Text s -> (
227227+ match float_of_string_opt (String.trim s) with
228228+ | Some f -> Float f
229229+ | None -> v)
230230+ | `Real, Int i -> Float (Int64.to_float i)
231231+ | `Real, _ -> v
232232+233233+(* Leading numeric prefix of [s] as a float, 0. if none -- SQLite's
234234+ text-to-number cast parses a prefix (after optional whitespace) and ignores
235235+ trailing junk, exactly like C's [strtod], which [Scanf]'s [%f] mirrors. *)
236236+let numeric_prefix s =
237237+ try Scanf.sscanf s " %f" Fun.id
238238+ with Scanf.Scan_failure _ | End_of_file -> 0.
239239+240240+(* Leading INTEGER prefix of [s] (optional sign then digits), 0 if none --
241241+ CAST(text AS INTEGER) reads only the integer part, stopping at the first
242242+ non-digit, so [CAST('123e+5' AS INTEGER)] is 123, not 12300000 (which is the
243243+ REAL/NUMERIC reading). Overflow saturates rather than wrapping. *)
244244+let integer_prefix s =
245245+ let s = String.trim s in
246246+ let n = String.length s in
247247+ let i = ref 0 in
248248+ if !i < n && (s.[!i] = '+' || s.[!i] = '-') then incr i;
249249+ let digits_start = !i in
250250+ while !i < n && s.[!i] >= '0' && s.[!i] <= '9' do
251251+ incr i
252252+ done;
253253+ if !i = digits_start then 0L
254254+ else
255255+ match Int64.of_string_opt (String.sub s 0 !i) with
256256+ | Some x -> x
257257+ | None -> if s.[0] = '-' then Int64.min_int else Int64.max_int
258258+259259+(* CAST(v AS ty) per the target affinity (lang_expr.html#castexpr). NULL is
260260+ preserved; text/blob to a numeric type parses a leading numeric prefix. *)
261261+let cast ty v =
262262+ match (affinity_of_type ty, v) with
263263+ | _, Null -> Null
264264+ | `Text, _ -> Text (text_of_value v)
265265+ | `Blob, Blob _ -> v
266266+ | `Blob, _ -> Blob (text_of_value v)
267267+ | `Integer, Int _ -> v
268268+ | `Integer, Float f -> Int (Int64.of_float f)
269269+ | `Integer, _ -> Int (integer_prefix (text_of_value v))
270270+ | `Real, Int i -> Float (Int64.to_float i)
271271+ | `Real, Float _ -> v
272272+ | `Real, _ -> Float (numeric_prefix (text_of_value v))
273273+ | `Numeric, (Int _ | Float _) -> v
274274+ | `Numeric, _ ->
275275+ let s = text_of_value v in
276276+ let f = numeric_prefix s in
277277+ (* sqlite3's NUMERIC affinity collapses an integral value to INTEGER:
278278+ integer-syntax text up to the int64 range, but float-syntax text (with a
279279+ '.' or exponent) only within +/-2^51, the lossless float<->int window of
280280+ sqlite3RealSameAsInt. A non-integral or larger value stays REAL. *)
281281+ let float_syntax =
282282+ String.exists (function '.' | 'e' | 'E' -> true | _ -> false) s
283283+ in
284284+ let limit = if float_syntax then 0x1p51 else 9.2e18 in
285285+ if Float.is_integer f && Float.abs f < limit then Int (Int64.of_float f)
286286+ else Float f
287287+288288+(* Whether [v] reads as a real rather than an integer -- a float, or text that
289289+ spells a real but not an integer. *)
290290+let is_real = function
291291+ | Float _ -> true
292292+ | Text s -> (
293293+ let s = String.trim s in
294294+ match Int64.of_string_opt s with
295295+ | Some _ -> false
296296+ | None -> float_of_string_opt s <> None)
297297+ | _ -> false
298298+299299+(* Numeric affinity: integers and reals pass through; text becomes the integer
300300+ or real it spells (an integer if it has no fractional/exponent part), or 0 if
301301+ it is not numeric -- matching how sqlite3 coerces an operand in arithmetic. *)
302302+let numeric_affinity v =
303303+ match v with
304304+ | Int _ | Float _ -> v
305305+ | _ -> (
306306+ match leading_number (text_of_value v) with
307307+ | Int n -> Int n
308308+ | Float f -> Float f
309309+ | _ -> Int 0L)
310310+311311+(* Comparison-affinity coercion: convert a text/blob to a number only when it is
312312+ a well-formed numeric literal, otherwise leave it unchanged. Unlike
313313+ {!numeric_affinity} (arithmetic, where non-numeric text is 0), a non-numeric
314314+ string stays text so it does not spuriously equal a number. *)
315315+let num_coerce v =
316316+ match v with
317317+ | Text s | Blob s -> (
318318+ let s = String.trim s in
319319+ match Int64.of_string_opt s with
320320+ | Some i -> Int i
321321+ | None -> (
322322+ match float_of_string_opt s with Some f -> Float f | None -> v))
323323+ | _ -> v
324324+325325+(* [v] as an int64 -- SQLite's integer coercion of a value (a float truncates,
326326+ text reads its leading number, anything non-numeric is 0). *)
327327+let intify = function
328328+ | Int i -> i
329329+ | Float f -> Int64.of_float f
330330+ | v -> (
331331+ match leading_number (text_of_value v) with
332332+ | Int n -> n
333333+ | Float f -> Int64.of_float f
334334+ | _ -> 0L)
335335+336336+(* SQL [LIKE]: [%] matches any run, [_] any single character, ASCII
337337+ case-insensitive; [~escape] makes a character match the next one literally. *)
338338+let like_match ?escape pattern str =
339339+ let p = String.lowercase_ascii pattern in
340340+ let s = String.lowercase_ascii str in
341341+ let esc = Option.map Char.lowercase_ascii escape in
342342+ let pl = String.length p and sl = String.length s in
343343+ let rec go pi si =
344344+ if pi = pl then si = sl
345345+ else if Some p.[pi] = esc && pi + 1 < pl then
346346+ si < sl && s.[si] = p.[pi + 1] && go (pi + 2) (si + 1)
347347+ else
348348+ match p.[pi] with
349349+ | '%' -> go (pi + 1) si || (si < sl && go pi (si + 1))
350350+ | '_' -> si < sl && go (pi + 1) (si + 1)
351351+ | c -> si < sl && s.[si] = c && go (pi + 1) (si + 1)
352352+ in
353353+ go 0 0
354354+355355+(* GLOB: case-sensitive Unix-style wildcards. [*] matches any run, [?] one
356356+ character, and [[..]] a character class (ranges with [-], negation with a
357357+ leading [^]). *)
358358+let glob_match pattern str =
359359+ let pl = String.length pattern and sl = String.length str in
360360+ let class_match neg cstart cend c =
361361+ let rec scan k =
362362+ if k >= cend then false
363363+ else if k + 2 < cend && pattern.[k + 1] = '-' then
364364+ if c >= pattern.[k] && c <= pattern.[k + 2] then true else scan (k + 3)
365365+ else if pattern.[k] = c then true
366366+ else scan (k + 1)
367367+ in
368368+ let m = scan cstart in
369369+ if neg then not m else m
370370+ in
371371+ let bracket pi =
372372+ let neg = pi + 1 < pl && pattern.[pi + 1] = '^' in
373373+ let cstart = if neg then pi + 2 else pi + 1 in
374374+ let j =
375375+ ref (if cstart < pl && pattern.[cstart] = ']' then cstart + 1 else cstart)
376376+ in
377377+ while !j < pl && pattern.[!j] <> ']' do
378378+ incr j
379379+ done;
380380+ if !j >= pl then `Literal else `Class (neg, cstart, !j)
381381+ in
382382+ let rec go pi si =
383383+ if pi = pl then si = sl
384384+ else
385385+ match pattern.[pi] with
386386+ | '*' -> go (pi + 1) si || (si < sl && go pi (si + 1))
387387+ | '?' -> si < sl && go (pi + 1) (si + 1)
388388+ | '[' -> (
389389+ match bracket pi with
390390+ | `Literal -> si < sl && str.[si] = '[' && go (pi + 1) (si + 1)
391391+ | `Class (neg, cstart, close) ->
392392+ si < sl
393393+ && class_match neg cstart close str.[si]
394394+ && go (close + 1) (si + 1))
395395+ | c -> si < sl && str.[si] = c && go (pi + 1) (si + 1)
396396+ in
397397+ go 0 0
398398+399399+(* [a LIKE b]: NULL if either operand is NULL, else the LIKE match as 1/0;
400400+ [~escape] is the optional single-character ESCAPE. *)
401401+let like_op ?escape a b =
402402+ match (a, b) with
403403+ | Null, _ | _, Null -> Null
404404+ | _ -> (
405405+ match escape with
406406+ | Some e when text_of_value e = "" ->
407407+ failwith "ESCAPE expression must be a single character"
408408+ | Some e ->
409409+ let ec = (text_of_value e).[0] in
410410+ bool (like_match ~escape:ec (text_of_value b) (text_of_value a))
411411+ | None -> bool (like_match (text_of_value b) (text_of_value a)))
412412+413413+(* [a GLOB b]: NULL if either operand is NULL, else the GLOB match as 1/0. *)
414414+let glob_op a b =
415415+ match (a, b) with
416416+ | Null, _ | _, Null -> Null
417417+ | _ -> bool (glob_match (text_of_value b) (text_of_value a))
418418+419419+(* [a || b]: NULL if either operand is NULL, else the text forms concatenated. *)
420420+let concat_op a b =
421421+ match (a, b) with
422422+ | Null, _ | _, Null -> Null
423423+ | _ -> Text (text_of_value a ^ text_of_value b)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2026 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** SQLite's value-level semantics, shared by the tree-walker and the
77+ register-VM paths: storage-class comparison order, collated comparison, text
88+ conversion, and the aggregate folds. A pure layer over {!Catalog.Value.t}
99+ with no dependency on the AST or the evaluator, so every executor folds and
1010+ orders values the same way. *)
1111+1212+val compare_values : Catalog.Value.t -> Catalog.Value.t -> int
1313+(** [compare_values a b] orders two values by SQLite's storage-class rule (NULL
1414+ < number < text < blob; numbers compared exactly across int/real). *)
1515+1616+val compare_values_coll : string -> Catalog.Value.t -> Catalog.Value.t -> int
1717+(** [compare_values_coll name a b] orders under collation [name] (BINARY /
1818+ NOCASE / RTRIM) for a text/text comparison, else by {!compare_values}. *)
1919+2020+val collation_key : string -> Catalog.Value.t -> Catalog.Value.t
2121+(** [collation_key name v] is [v]'s canonical form under collation [name]: two
2222+ text values equal under the collation share a form (so a caller can bucket /
2323+ dedup by it with plain equality), anything else unchanged.
2424+ @raise Not_found on an unknown collation. *)
2525+2626+val float_of_value : Catalog.Value.t -> float
2727+(** [float_of_value v] is the [REAL] value of a numeric [v] ([Int]/[Float]).
2828+ @raise Invalid_argument on a non-numeric value. *)
2929+3030+val text_of_value : Catalog.Value.t -> string
3131+(** [text_of_value v] is SQLite's text conversion of [v] (the [CAST(v AS TEXT)]
3232+ rendering: a float carries a decimal point, NULL is the empty string). *)
3333+3434+type tri =
3535+ | T
3636+ | F
3737+ | U (** A three-valued truth: true, false, or unknown (NULL). *)
3838+3939+val tri_of : Catalog.Value.t -> tri
4040+(** [tri_of v] is [v]'s truth: unknown for NULL, true/false for a non-zero/zero
4141+ number, a text/blob coerced by its leading numeric prefix. *)
4242+4343+val value_of_tri : tri -> Catalog.Value.t
4444+(** [value_of_tri t] is [1] for true, [0] for false, NULL for unknown. *)
4545+4646+val tri_and : tri -> tri -> tri
4747+(** Three-valued [AND]: false dominates, unknown is contagious otherwise. *)
4848+4949+val tri_or : tri -> tri -> tri
5050+(** Three-valued [OR]: true dominates, unknown is contagious otherwise. *)
5151+5252+val tri_not : tri -> tri
5353+(** Three-valued [NOT]: true and false flip, unknown stays. *)
5454+5555+val not_value : Catalog.Value.t -> Catalog.Value.t
5656+(** [not_value v] is the three-valued logical [NOT] of [v] as a [1]/[0]/NULL. *)
5757+5858+val bool : bool -> Catalog.Value.t
5959+(** [bool b] is [1] for [true], [0] for [false]. *)
6060+6161+val is_value : Catalog.Value.t -> Catalog.Value.t -> Catalog.Value.t
6262+(** [is_value a b] is SQLite's NULL-safe equality [a IS b] as [1]/[0] (never
6363+ NULL): two NULLs are equal, a NULL and a non-NULL are not, else an ordinary
6464+ value comparison. *)
6565+6666+val numeric_agg : string -> Catalog.Value.t list -> Catalog.Value.t
6767+(** [numeric_agg name vals] folds the non-NULL aggregate values [vals] for one
6868+ of [sum]/[total]/[avg]/[min]/[max]: SUM stays integer when every value is
6969+ (raising ["integer overflow"] on int64 overflow), AVG/TOTAL are real;
7070+ [min]/[max] use {!compare_values}; an empty [vals] is NULL ([total] is 0.0).
7171+ @raise Invalid_argument via {!float_of_value} on a non-numeric [sum]/[avg].
7272+*)
7373+7474+val affinity_of_type : string -> [ `Blob | `Integer | `Numeric | `Real | `Text ]
7575+(** [affinity_of_type ty] is the storage affinity of a declared type name
7676+ (datatype3.html sec.3.1): a name containing [INT] is [`Integer], CHAR/CLOB/
7777+ TEXT is [`Text], BLOB or [""] is [`Blob], REAL/FLOA/DOUB is [`Real], else
7878+ [`Numeric]. *)
7979+8080+val apply_affinity : string -> Catalog.Value.t -> Catalog.Value.t
8181+(** [apply_affinity ty v] applies the column affinity of declared type [ty] to a
8282+ value being written: NUMERIC/INTEGER read well-formed numeric text as a
8383+ number, TEXT renders a number as text, REAL coerces a number to float; a
8484+ BLOB, NULL, and non-numeric text are unchanged. *)
8585+8686+val cast : string -> Catalog.Value.t -> Catalog.Value.t
8787+(** [cast ty v] is [CAST(v AS ty)] by the target affinity
8888+ (lang_expr.html#castexpr): NULL is preserved; text/blob to a numeric type
8989+ parses a leading numeric prefix. *)
9090+9191+val is_real : Catalog.Value.t -> bool
9292+(** [is_real v] is whether [v] reads as a real rather than an integer: a float,
9393+ or text that spells a real but not an integer. *)
9494+9595+val numeric_affinity : Catalog.Value.t -> Catalog.Value.t
9696+(** [numeric_affinity v] is the arithmetic coercion: a number passes through,
9797+ text becomes the integer or real it spells, or [0] if it is not numeric. *)
9898+9999+val num_coerce : Catalog.Value.t -> Catalog.Value.t
100100+(** [num_coerce v] is the comparison-affinity coercion: a text/blob becomes a
101101+ number only when it is a well-formed numeric literal, else it is unchanged
102102+ (so a non-numeric string does not spuriously equal a number). *)
103103+104104+val intify : Catalog.Value.t -> int64
105105+(** [intify v] is SQLite's integer coercion of a value: a float truncates, text
106106+ reads its leading number, anything non-numeric is [0]. *)
107107+108108+val like_match : ?escape:char -> string -> string -> bool
109109+(** [like_match pattern s] is SQL [LIKE] (ASCII case-insensitive): [%] matches
110110+ any run, [_] any single character; [~escape] makes its character match the
111111+ next one literally. *)
112112+113113+val glob_match : string -> string -> bool
114114+(** [glob_match pattern s] is SQL [GLOB] (case-sensitive Unix wildcards): [*]
115115+ any run, [?] one character, [[..]] a character class. *)
116116+117117+val like_op :
118118+ ?escape:Catalog.Value.t ->
119119+ Catalog.Value.t ->
120120+ Catalog.Value.t ->
121121+ Catalog.Value.t
122122+(** [like_op a b] is [a LIKE b] as [1]/[0]/NULL; [~escape] is the optional
123123+ single-character ESCAPE value. *)
124124+125125+val glob_op : Catalog.Value.t -> Catalog.Value.t -> Catalog.Value.t
126126+(** [glob_op a b] is [a GLOB b] as [1]/[0]/NULL. *)
127127+128128+val concat_op : Catalog.Value.t -> Catalog.Value.t -> Catalog.Value.t
129129+(** [concat_op a b] is [a || b]: NULL if either is NULL, else the text forms
130130+ concatenated. *)
131131+132132+val distinct_values : Catalog.Value.t list -> Catalog.Value.t list
133133+(** [distinct_values vs] keeps the first occurrence of each value by
134134+ {!compare_values} equality -- a [DISTINCT] aggregate's argument values. *)