SQL frontend over the catalog engine
0

Configure Feed

Select the types of activity you want to include in your feed.

ocaml-sql / lib / query.mli
12 kB 259 lines
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6(** Storage-agnostic SQL query executor. 7 8 Operates over a {!catalog} that exposes a table's column names and rows, so 9 the evaluation logic (comparisons, [LIKE], joins, ordering) is independent 10 of the storage backend and can be tested in isolation. The catalog speaks 11 the engine core's {!Catalog.Value.t} -- so a storage adapter need not know 12 the SQL frontend's value type -- and the executor converts at that boundary. 13*) 14 15type catalog = { 16 columns : string -> string list; 17 (** Column names of a table, in declared order. *) 18 rows : string -> Catalog.Value.t list list; 19 (** Rows of a table, each aligned to {!columns}. *) 20 rows_rowid : string -> (int64 option * Catalog.Value.t list) list; 21 (** Each row paired with its rowid, or [None] for a row source without one 22 (a view, a derived table, [VALUES]) -- lets the executor resolve the 23 implicit [rowid]/[oid]/[_rowid_] column of a full table scan. *) 24 index_scan : 25 string -> 26 (string * Catalog.Value.t) list -> 27 (string * Catalog.Value.t list list) option; 28 (** [index_scan table eqs] returns [Some (index_name, rows)] when an index 29 on [table] serves the column-equality bindings [eqs], or [None] for a 30 full {!rows} scan. The rows are still re-filtered, so this is a pure 31 optimisation. *) 32 index_for : string -> string list -> string option; 33 (** [index_for table cols] names a unique index on [table] all of whose 34 columns are among [cols] -- the structural counterpart of 35 {!index_scan}, used by {!explain}. *) 36 collation : string -> string -> string option; 37 (** [collation table col] is [col]'s declared [COLLATE] sequence 38 (uppercase) on [table], or [None] for the default binary / a derived 39 source. *) 40} 41 42val of_catalog : Catalog.t -> catalog 43(** [of_catalog cat] adapts a {!Catalog.t} to the SQL frontend's read/explain 44 view. Backends such as Irmin or Parquet can implement {!Catalog.Spi.S}, 45 build a catalog handle, and pass it through this adapter before calling 46 {!Engine.run_select}. *) 47 48val number_params : Ast.stmt -> Ast.stmt 49(** [number_params stmt] assigns each [?] placeholder its zero-based index in 50 left-to-right source order. *) 51 52type binding = string * (string * Ast.value) list 53(** One named row fragment, [(alias, [(column, value); ...])]. A [binding list] 54 passed as [?scope] is an outer environment placed at the tail of the row's 55 own bindings, so inner columns shadow it for unqualified names but a 56 qualified [alias.col] still resolves. Used for correlated subqueries (the 57 enclosing row) and for a trigger's [OLD]/[NEW] rows. *) 58 59val set_cat_subquery_runner : 60 (catalog -> 61 (string * (string * Ast.value) list) list -> 62 Ast.select -> 63 Ast.value array -> 64 Ast.value list list) -> 65 unit 66(** [set_cat_subquery_runner] is install the runner used for every SELECT [eval] 67 reaches -- a scalar / [IN] / [EXISTS] subquery, a FROM-subquery, a CTE body, 68 a recursive-CTE arm -- given the enclosing-row {!type-catalog}, the outer 69 scope, the subquery, and the params, returning its rows. 70 {!Engine.run_select} (the catalog-VM engine) installs itself here on load; 71 without it a SELECT raises a clear error. *) 72 73val from_bindings : 74 ?scope:binding list -> 75 catalog -> 76 Ast.value array -> 77 Ast.from_clause -> 78 Ast.join list -> 79 binding list list 80(** [from_bindings cat params from joins] is the join-row environments of a bare 81 [FROM from joins] (no projection, no WHERE), each a [binding list] usable as 82 an outer [~scope]. Drives [UPDATE ... FROM]. [?scope] adds outer bindings. 83*) 84 85val materialize_ctes : catalog -> Ast.value array -> Ast.cte list -> catalog 86(** [materialize_ctes cat params ctes] runs each [WITH] table expression 87 (semi-naive evaluation for a recursive one) and binds its rows into [cat] 88 under its name, returning the extended catalog. Lets a [WITH] on 89 [UPDATE]/[DELETE] put its CTEs in scope for the statement's subqueries. *) 90 91val resolve_collations : catalog -> Ast.select -> Ast.select 92(** [resolve_collations] is make each base-table column's declared [COLLATE] 93 sequence explicit in [s]'s comparisons and ORDER BY keys (wrapping the 94 column in a [Collate] node by sqlite3's left-operand rule), so the collation 95 machinery honours it. The engine applies this before lowering, so a compiled 96 scan over a collated column compares under the collation rather than by 97 binary value. *) 98 99val resolve_where_aliases : catalog -> Ast.select -> Ast.select 100(** [resolve_where_aliases] is substitute a select-list alias referenced in 101 [s]'s WHERE or ORDER BY with the aliased expression -- so 102 [SELECT a AS x FROM t WHERE x > 1] resolves [x] (an alias is only 103 substituted when it does not name a real source column, which shadows it, 104 matching sqlite3). The engine applies this before lowering so a compiled 105 scan resolves the alias too. *) 106 107val substitute_scope : 108 catalog -> binding list -> Ast.select -> Ast.select option 109(** [substitute_scope cat scope s] replaces each free (correlated) column of 110 subquery [s] -- one bound by neither its own FROM nor a deeper scope -- with 111 its value from [scope] (the enclosing row), turning a correlated subquery 112 into an uncorrelated one a caller can run on its own. [None] when [s] is too 113 tangled for the simple rule (a FROM-subquery, a nested subquery, or a 114 window). *) 115 116val hoist_subqueries : 117 run:(Ast.select -> Ast.value list list) -> catalog -> Ast.select -> Ast.select 118(** [hoist_subqueries] is replace each uncorrelated scalar / [IN (SELECT ...)] / 119 [EXISTS] subquery in [s]'s scalar-expression positions (select list, WHERE, 120 GROUP BY, HAVING, ORDER BY, join ON) with its materialised value -- a 121 literal, or an [IN] value list. A correlated subquery (one referencing an 122 enclosing query's columns) is left intact. [run] materialises an 123 uncorrelated subquery to its rows; the engine passes a runner that executes 124 it on the catalog VM. The register VM has no subquery opcode, so it calls 125 this to compile the outer query of an uncorrelated subquery rather than fall 126 back. Sound because an uncorrelated subquery is constant. *) 127 128val resolve_ordinals : catalog -> Ast.select -> Ast.select 129(** [resolve_ordinals] is rewrite [s]'s ORDER BY and GROUP BY so a 1-based 130 output-column ordinal ([ORDER BY 2] / [GROUP BY 2]) becomes the expression 131 of that output column. The engine applies this before lowering so a compiled 132 ORDER BY/GROUP BY ordinal works too. *) 133 134val select_columns : catalog -> Ast.select -> string list 135(** [select_columns cat s] is the output column names of [s]: each item's alias, 136 else a bare column's name, else [columnN]; [*] expands the source table's 137 columns. Used to name the columns of a [CREATE TABLE ... AS] target. *) 138 139val is_aggregate_query : Ast.select -> bool 140(** [is_aggregate_query s] is whether [s] aggregates at all -- it groups, has 141 HAVING, or a select column folds an aggregate. A LIMIT/OFFSET cannot be 142 pushed into the scan of such a query (the aggregate folds every row first), 143 so a fast path must recognise and skip it. *) 144 145val aggregate_compilable : Ast.select -> bool 146(** [aggregate_compilable] is whether [s] is an aggregate/GROUP BY select that 147 {!aggregate_scan} can run: it groups or aggregates or has HAVING and uses no 148 window function. The scan carries the implicit rowid column, so an aggregate 149 over [rowid] (e.g. [count(rowid)]) compiles. *) 150 151val window_compilable : Ast.select -> bool 152(** [window_compilable] is whether [s] has a window function over a [rowid]-free 153 projection, so its (filtered, joined) source rows can be scanned by the 154 register VM and then post-processed by {!aggregate_scan} (which reaches 155 {!window_project}). *) 156 157val aggregate_scan : 158 catalog -> 159 Ast.value array -> 160 Ast.select -> 161 (string * string list) list -> 162 Ast.value list list -> 163 Ast.value list list 164(** [aggregate_scan cat params s segments rows] runs an aggregate/GROUP BY [s] 165 from its already-scanned source [rows]: it builds the row environments and 166 runs the grouping, aggregation, HAVING, ORDER BY, DISTINCT, and 167 OFFSET/LIMIT. [segments] is the join's tables in FROM order as 168 [(alias, cols)]; each row is the concatenation of those tables' columns in 169 that order and is sliced back into one binding per table (a single-table 170 aggregate is a one-element [segments]). Lets the register VM do the scan and 171 reuse this aggregation. *) 172 173val combine_compound : 174 catalog -> 175 Ast.value array -> 176 Ast.select -> 177 Ast.value list list -> 178 (Ast.set_op * Ast.value list list) list -> 179 Ast.value list list 180(** [combine_compound cat params s base_rows compound_rows] combines a compound 181 select's already-computed arm rows -- [base_rows] for the leading arm, 182 [(op, rows)] per following arm: fold the set operators, apply [s]'s ORDER BY 183 over the output columns, then OFFSET/LIMIT. Lets a caller produce each arm's 184 rows itself (via the register VM) and reuse this orchestration. *) 185 186val explain : catalog -> Ast.select -> string 187(** [explain cat select] describes the access path the executor takes, one line 188 per table ([SCAN t] or [SEARCH t USING INDEX i]). Structural -- the same 189 index decision the engine makes, independent of parameter values. *) 190 191val is_agg : string -> Ast.expr list -> bool 192(** [is_agg name args] is whether [name] (with that argument count) is an 193 aggregate -- [count]/[sum]/[avg]/..., or [min]/[max] with at most one 194 argument. An aggregate is folded over a group, not applied per row. *) 195 196val eval_const : 197 ?scope:binding list -> catalog -> Ast.value array -> Ast.expr -> Ast.value 198(** [eval_const cat params e] evaluates [e] with no table row in scope, for 199 [INSERT ... VALUES] tuples. A column reference raises [Failure] unless it 200 resolves in [?scope] (a trigger's OLD/NEW rows). *) 201 202val row_matches : 203 ?scope:binding list -> 204 ?rowid:int64 -> 205 catalog -> 206 Ast.value array -> 207 string -> 208 string list -> 209 Ast.expr option -> 210 Ast.value list -> 211 bool 212(** [row_matches cat params table cols where vals] is whether the row [vals] of 213 [table] (aligned to [cols]) satisfies [where]. [None] matches every row. 214 Used to select the rows a [DELETE] removes. [?scope] adds outer bindings; 215 [?rowid] is the row's rowid, so [where] may reference the implicit rowid 216 column. *) 217 218val eval_in_row : 219 ?scope:binding list -> 220 ?rowid:int64 -> 221 catalog -> 222 Ast.value array -> 223 string -> 224 string list -> 225 Ast.value list -> 226 Ast.expr -> 227 Ast.value 228(** [eval_in_row cat params table cols vals e] evaluates [e] against the single 229 [table] row [vals] (aligned to [cols]) -- the right-hand side of an 230 [UPDATE ... SET] assignment, which may reference the row's columns. [?scope] 231 adds outer bindings (a trigger's OLD/NEW rows); [?rowid] is the row's rowid, 232 so [e] may reference the implicit rowid column. *) 233 234val eval_upsert : 235 ?scope:binding list -> 236 catalog -> 237 Ast.value array -> 238 table:string -> 239 cols:string list -> 240 current:Ast.value list -> 241 excluded:Ast.value list -> 242 Ast.expr -> 243 Ast.value 244(** [eval_upsert cat params ~table ~cols ~current ~excluded e] evaluates an 245 [UPSERT] [DO UPDATE] expression with the conflicting row's [current] values 246 in scope under [table] and the would-be-inserted [excluded] values under the 247 [excluded] pseudo-table (both aligned to [cols]). *) 248 249val returning_rows : 250 catalog -> 251 Ast.value array -> 252 table:string -> 253 cols:string list -> 254 Ast.result_col list -> 255 Ast.value list list -> 256 Ast.value list list 257(** [returning_rows cat params ~table ~cols returning rows] projects a 258 [RETURNING] column list over each affected [row] of [table] ([cols] are the 259 table's column names, aligned to [row]); [*] expands to the whole row. *)