SQL frontend over the catalog engine
0

Configure Feed

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

ocaml-sql / lib / lower.ml
33 kB 847 lines
1(*--------------------------------------------------------------------------- 2 Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. 3 SPDX-License-Identifier: ISC 4 ---------------------------------------------------------------------------*) 5 6module Ir = Catalog.Ir 7module V = Catalog.Value 8 9let unsupported () = raise Ir.Unsupported 10 11(* The correlated subqueries found while lowering a select, in id order. [expr] 12 registers each scalar / [EXISTS] / [IN (SELECT)] here as it emits the 13 matching [Ir.Subquery] node, and [query] reads the table back so the caller 14 can wire a runner (the engine has no SQL; it calls the injected runner by 15 id). A reset-per-[query] accumulator is safe: lowering does not recurse into 16 a subquery's body (it keeps the [Ast.select] for the runner), and lowering 17 finishes -- the [query] result captures this list -- before the engine runs 18 and fires any nested subquery lowering. [allow] is cleared while lowering an 19 aggregate select, where the grouped path has no runner, so a subquery there 20 falls back. *) 21type subkind = Scalar | Exists | In 22 23let sub_acc : (subkind * Ast.select) list ref = ref [] 24let sub_count = ref 0 25let sub_allow = ref true 26 27let register_sub kind sub = 28 if not !sub_allow then unsupported (); 29 let id = !sub_count in 30 sub_acc := (kind, sub) :: !sub_acc; 31 incr sub_count; 32 id 33 34(* Compare two values under a named collation -- the tree-walker's own collated 35 comparison, for an [ORDER BY] key that carries a [COLLATE]. *) 36let collate = Value_ops.compare_values_coll 37 38(* The IR's value layer is the tree-walker's own functions, called directly: the 39 frontend and the engine share {!Catalog.Value.t}, so no value conversion sits 40 between them and a compiled query agrees with an interpreted one by sharing 41 the code. Only the operator enums differ, mapped by 42 {!ast_cmp}/{!ast_logic}. *) 43let ast_cmp : Ir.binop -> Ast.binop = function 44 | Eq -> Eq 45 | Ne -> Neq 46 | Lt -> Lt 47 | Le -> Le 48 | Gt -> Gt 49 | Ge -> Ge 50 | _ -> assert false 51 52let ast_logic : Ir.logicop -> Ast.binop = function 53 | And -> And 54 | Or -> Or 55 | Like -> Like 56 | Glob -> Glob 57 | Concat -> Concat 58 | Json_get -> Json_get 59 | Json_get_text -> Json_get_text 60 61let builtins : Ir.builtins = 62 { 63 call = Func.apply; 64 compare = 65 (fun op coll a_col b_col va vb -> 66 Value_op.compare_with_affinity ?coll ~a_col ~b_col (ast_cmp op) va vb); 67 logic = (fun op va vb -> Value_op.binop_value (ast_logic op) va vb); 68 not_ = Value_ops.not_value; 69 is_eq = Value_ops.is_value; 70 in_list = 71 (fun coll e_col v cands -> Value_op.in_list_value ?coll ~e_col v cands); 72 cast = Value_ops.cast; 73 subquery = (fun _ _ _ -> unsupported ()); 74 } 75 76type layout = (string * string list) list 77 78(* The comparison, opcode-free, and arithmetic operator groups of 79 [Ast.binop]. *) 80let is_cmp : Ast.binop -> bool = function 81 | Eq | Neq | Lt | Le | Gt | Ge -> true 82 | _ -> false 83 84let is_logic : Ast.binop -> bool = function 85 | And | Or | Like | Glob | Concat | Json_get | Json_get_text -> true 86 | _ -> false 87 88let ir_cmp : Ast.binop -> Ir.binop = function 89 | Eq -> Eq 90 | Neq -> Ne 91 | Lt -> Lt 92 | Le -> Le 93 | Gt -> Gt 94 | Ge -> Ge 95 | _ -> unsupported () 96 97let ir_logic : Ast.binop -> Ir.logicop = function 98 | And -> And 99 | Or -> Or 100 | Like -> Like 101 | Glob -> Glob 102 | Concat -> Concat 103 | Json_get -> Json_get 104 | Json_get_text -> Json_get_text 105 | _ -> unsupported () 106 107let ir_arith : Ast.binop -> Ir.binop = function 108 | Add -> Add 109 | Sub -> Sub 110 | Mul -> Mul 111 | Div -> Div 112 | Mod -> Mod 113 | Bit_and -> Band 114 | Bit_or -> Bor 115 | Shl -> Shl 116 | Shr -> Shr 117 | _ -> unsupported () 118 119(* The position of the column named [name] in [cols] (case-insensitive). *) 120let col_index name cols = 121 let uname = String.uppercase_ascii name in 122 let rec go j = function 123 | c :: _ when String.uppercase_ascii c = uname -> Some j 124 | _ :: t -> go (j + 1) t 125 | [] -> None 126 in 127 go 0 cols 128 129(* The implicit [rowid] pseudo-column and its [oid]/[_rowid_] aliases. *) 130let is_rowid_name n = 131 match String.uppercase_ascii n with 132 | "ROWID" | "OID" | "_ROWID_" -> true 133 | _ -> false 134 135(* The hidden layout slot a table scan publishes for its integer rowid. A scan 136 that exposes its rowid appends a column with this name; 137 [rowid]/[oid]/[_rowid_] resolve to it (when no real column shadows them), and 138 [Star] drops it. The NUL byte keeps it unspellable, so no user column ever 139 collides. *) 140let rowid_sentinel = "\000rowid" 141 142(* Find [alias.name] (or a bare [name]) in the layout: a qualified name picks 143 the source with that alias, a bare name takes the first source that has it. 144 [None] if the layout has no place for it. *) 145let col (layout : layout) alias name = 146 match alias with 147 | Some a -> 148 let ua = String.uppercase_ascii a in 149 let rec go ci = function 150 | (al, cols) :: t -> 151 if String.uppercase_ascii al = ua then 152 match col_index name cols with 153 | Some j -> Some (ci, j) 154 | None -> None 155 else go (ci + 1) t 156 | [] -> None 157 in 158 go 0 layout 159 | None -> 160 let rec go ci = function 161 | (_, cols) :: t -> ( 162 match col_index name cols with 163 | Some j -> Some (ci, j) 164 | None -> go (ci + 1) t) 165 | [] -> None 166 in 167 go 0 layout 168 169let resolve_col (layout : layout) alias name = 170 match col layout alias name with 171 | Some r -> r 172 | None -> 173 (* [rowid] and its aliases bind to the scan's hidden rowid slot when it 174 publishes one; without that slot the scan cannot place rowid, so raise 175 [Unsupported] to fall back rather than calling it an unknown column. *) 176 if is_rowid_name name then 177 match col layout alias rowid_sentinel with 178 | Some r -> r 179 | None -> unsupported () 180 else Fmt.failwith "unknown column %S" name 181 182(* A comparison's collation -- sqlite3's left-operand rule, made explicit by 183 [Query.resolve_collations] as a [Collate] wrapper on an operand -- and the 184 bare operand under it. [is_col_ast] treats a [Collate] wrapper as not a 185 column, so a collated comparison carries no affinity on that side, matching 186 the tree-walker. *) 187let is_col_ast = function Ast.Col _ -> true | _ -> false 188let collation_of = function Ast.Collate (_, c) -> Some c | _ -> None 189 190let comparison_collation a b = 191 match collation_of a with Some _ as c -> c | None -> collation_of b 192 193(* The collation an operand carries for membership/comparison, the default 194 [BINARY] (unary plus, which only strips affinity) carrying none. *) 195let nonbinary_collation e = 196 match collation_of e with Some c when c <> "BINARY" -> Some c | _ -> None 197 198let strip_collate = function Ast.Collate (e, _) -> e | e -> e 199 200let rec expr (layout : layout) (e : Ast.expr) : Ir.expr = 201 match e with 202 | Lit v -> Lit v 203 | Param i -> Param i 204 | Col (alias, name) -> 205 let c, j = resolve_col layout alias name in 206 Col (c, j) 207 | Unop_not e -> Not (expr layout e) 208 | Binary (op, a, b) -> 209 if is_cmp op then cmp_expr layout op a b 210 else if is_logic op then Logic (ir_logic op, expr layout a, expr layout b) 211 else Arith (ir_arith op, expr layout a, expr layout b) 212 | Is_null e -> Null_value (true, expr layout e) 213 | Is_not_null e -> Null_value (false, expr layout e) 214 | Is (a, b) -> Is (expr layout a, expr layout b) 215 | In_list (e, cands) -> 216 In 217 ( expr layout (strip_collate e), 218 nonbinary_collation e, 219 is_col_ast e, 220 List.map (expr layout) cands ) 221 (* [iif(c, t, f)] short-circuits, so it is the searched [CASE WHEN c THEN t 222 ELSE f] -- lower it as one rather than an eager function call. An aggregate 223 reaching [expr] is in a WHERE/ON/ORDER BY/scan position (a select-list 224 aggregate makes the query aggregate, lowered elsewhere) -- always an error, 225 raised here so the engine reports it without deferring to the 226 tree-walker. *) 227 | Func ("iif", [ c; t; f ]) -> expr layout (Case (None, [ (c, t) ], Some f)) 228 | Func (name, args) when not (Query.is_agg name args) -> 229 Func (name, List.map (expr layout) args) 230 | Func (name, _) -> 231 Fmt.failwith 232 "%s() is an aggregate function and is only valid as a select item, not \ 233 in WHERE, ON, or ORDER BY" 234 name 235 | Case (base, branches, els) -> 236 let branch (c, r) = 237 let p = 238 match base with 239 | None -> predicate layout c 240 | Some b -> Compare (Eq, expr layout b, expr layout c) 241 in 242 (p, expr layout r) 243 in 244 Case (List.map branch branches, Option.map (expr layout) els) 245 | Cast (e, ty) -> Cast (expr layout e, ty) 246 (* A correlated subquery becomes an opaque [Subquery] node: register the 247 [Ast.select] and its kind, and (for [IN]) lower the left operand whose 248 value the membership test reads. An uncorrelated one is already folded to a 249 constant before lowering ([Query.hoist_subqueries]), so what reaches here 250 is correlated, run per outer row by the injected runner. *) 251 | Scalar_select sub -> Subquery (register_sub Scalar sub, None) 252 | Exists sub -> Subquery (register_sub Exists sub, None) 253 | In_select (e, sub) -> Subquery (register_sub In sub, Some (expr layout e)) 254 | Star | Qualified_star _ | Distinct_agg _ | Filter _ | Ordered_agg _ 255 | Window _ | Collate _ -> 256 unsupported () 257 258(* Lower a [WHERE]/[ON]/[HAVING] expression to a filter predicate: an [AND]/[OR] 259 of comparisons, null tests, and bare truthy expressions. *) 260and predicate (layout : layout) (e : Ast.expr) : Ir.predicate = 261 match e with 262 | Binary (And, a, b) -> And (predicate layout a, predicate layout b) 263 | Binary (Or, a, b) -> Or (predicate layout a, predicate layout b) 264 | Binary (op, a, b) when is_cmp op -> cmp_pred layout op a b 265 | Is_null e -> Null_test (true, expr layout e) 266 | Is_not_null e -> Null_test (false, expr layout e) 267 (* a bare [WHERE expr] keeps the truthy rows; [expr] itself rejects a shape 268 the IR cannot model (a subquery, a window), falling the whole predicate 269 back *) 270 | e -> Truthy (expr layout e) 271 272(* Lower a comparison. A [COLLATE]-wrapped operand -- a declared collation, or a 273 [BINARY] one (which is unary plus, stripping affinity) -- routes through the 274 value-level {!Ir.Cmp_coll}: the binary VM compare cannot honour a non-binary 275 collation, and stripping the wrapper for the plain path would let the IR 276 re-derive affinity from the bare column the wrapper suppressed. The explicit 277 affinity flags read the original operands ([is_col_ast] of a wrapper is 278 [false]), matching the tree-walker; an unwrapped comparison takes the binary 279 value comparison / filter jump. *) 280and cmp_expr layout op a b : Ir.expr = 281 let a' = expr layout (strip_collate a) 282 and b' = expr layout (strip_collate b) in 283 match comparison_collation a b with 284 | Some c -> Cmp_coll (ir_cmp op, c, is_col_ast a, is_col_ast b, a', b') 285 | None -> Cmp (ir_cmp op, a', b') 286 287and cmp_pred layout op a b : Ir.predicate = 288 let a' = expr layout (strip_collate a) 289 and b' = expr layout (strip_collate b) in 290 match comparison_collation a b with 291 | Some c -> 292 Truthy (Cmp_coll (ir_cmp op, c, is_col_ast a, is_col_ast b, a', b')) 293 | None -> Compare (ir_cmp op, a', b') 294 295let ir_dir = function Ast.Asc -> Ir.Asc | Ast.Desc -> Ir.Desc 296 297(* An [ORDER BY] key's explicit collation, if any -- a [BINARY] wrapper is the 298 default, so it carries no collation. *) 299let order_collation e = 300 match collation_of e with Some c when c <> "BINARY" -> Some c | _ -> None 301 302(* Lower an [ORDER BY] key with [lower_key] (over the scan, or the synthetic 303 group row): its expression, direction, and collation. *) 304let order_key lower_key (e, d) = 305 (lower_key (strip_collate e), ir_dir d, order_collation e) 306 307(* The per-output-column collations a DISTINCT dedups under (one per result 308 column; [[]] when not DISTINCT, so the engine dedups by binary value). *) 309let distinct_collations (s : Ast.select) = 310 if s.distinct then 311 List.map (fun (rc : Ast.result_col) -> order_collation rc.expr) s.cols 312 else [] 313 314(* Expand a select item into its projected expressions: [*] and [t.*] become one 315 column reference per source column. *) 316let project_item ?(hidden = []) (layout : layout) (rc : Ast.result_col) : 317 Ir.expr list = 318 match rc.expr with 319 | Star -> 320 (* [*] lists every source column, except a column a NATURAL / USING join 321 hides (the right source's copy of a common column). *) 322 List.concat 323 (List.mapi 324 (fun ci (_, cols) -> 325 List.filter_map 326 (fun j -> 327 if List.mem (ci, j) hidden then None else Some (Ir.Col (ci, j))) 328 (List.init (List.length cols) Fun.id)) 329 layout) 330 | Qualified_star q -> 331 (* [t.*] lists [t]'s columns, skipping the hidden rowid slot. *) 332 let uq = String.uppercase_ascii q in 333 let rec go ci = function 334 | (al, cols) :: t -> 335 if String.uppercase_ascii al = uq then 336 List.filter_map 337 (fun j -> 338 if List.mem (ci, j) hidden then None 339 else Some (Ir.Col (ci, j))) 340 (List.init (List.length cols) Fun.id) 341 else go (ci + 1) t 342 | [] -> unsupported () 343 in 344 go 0 layout 345 (* A projected [COLLATE] (or unary plus) produces the bare value -- the 346 collation governs only comparison/ordering/dedup, captured separately in 347 the query's ORDER BY keys and [distinct_colls]. *) 348 | e -> [ expr layout (strip_collate e) ] 349 350(* The [int64] value of a literal / bound-parameter [LIMIT]/[OFFSET] expression, 351 or [None] for any other shape (a column, an expression) -- which falls 352 back. *) 353let lit_int64 params (e : Ast.expr) = 354 match e with 355 | Ast.Lit (Ast.Int n) -> Some n 356 | Param i when i >= 0 && i < Array.length params -> ( 357 match params.(i) with Ast.Int n -> Some n | _ -> None) 358 | _ -> None 359 360(* A non-negative row count clamped to a native int: a value beyond the native 361 range exceeds any materialised row count, so [max_int] is exact (keep all for 362 a limit, skip all for an offset) without truncating to a wrong small 363 number. *) 364let clamp_count n = 365 if Int64.compare n (Int64.of_int max_int) >= 0 then max_int 366 else Int64.to_int n 367 368(* A NATURAL join's match columns (or the [USING] list): the names shared by the 369 two joined sources -- a single [FROM] table and one joined table. *) 370let common_columns (layout : layout) (j : Ast.join) = 371 match layout with 372 | [ (_, lcols); (_, rcols) ] -> 373 if j.using <> [] then j.using 374 else 375 (* the hidden rowid slot is not a user column: it never participates in 376 the implicit equality (both sides carry one, so it would otherwise 377 match them spuriously) *) 378 let ru = List.map String.uppercase_ascii rcols in 379 List.filter 380 (fun c -> 381 c <> rowid_sentinel && List.mem (String.uppercase_ascii c) ru) 382 lcols 383 | _ -> unsupported () 384 385(* The implicit [ON] of a NATURAL / USING join: the common columns equated 386 across the two sources, as an [Ast] predicate the layout then lowers. *) 387let natural_on (layout : layout) (j : Ast.join) = 388 let la, ra = 389 match layout with [ (la, _); (ra, _) ] -> (la, ra) | _ -> unsupported () 390 in 391 let eq c = Ast.Binary (Ast.Eq, Ast.Col (Some la, c), Ast.Col (Some ra, c)) in 392 match common_columns layout j with 393 | [] -> unsupported () 394 | c0 :: rest -> 395 List.fold_left (fun acc c -> Ast.Binary (Ast.And, acc, eq c)) (eq c0) rest 396 397(* The [(cursor, column)] positions a NATURAL / USING join hides from [*]: the 398 right source's copy of each common column, so [*] lists it once. *) 399(* The hidden rowid slots: any [rowid_sentinel] column a scan published. [Star] 400 drops these so [SELECT *] never returns the implicit rowid. *) 401let sentinel_columns (layout : layout) = 402 List.concat 403 (List.mapi 404 (fun ci (_, cols) -> 405 List.filter_map 406 (fun (j, c) -> if c = rowid_sentinel then Some (ci, j) else None) 407 (List.mapi (fun j c -> (j, c)) cols)) 408 layout) 409 410let hidden_columns (layout : layout) (s : Ast.select) = 411 match s.joins with 412 | [ j ] when j.natural || j.using <> [] -> ( 413 match layout with 414 | [ _; (_, rcols) ] -> 415 List.filter_map 416 (fun c -> Option.map (fun i -> (1, i)) (col_index c rcols)) 417 (common_columns layout j) 418 | _ -> []) 419 | _ -> [] 420 421(* An inner join's effective [ON], folded into the filter: the explicit [ON], or 422 the NATURAL / USING common-column equality; an outer join goes through 423 {!left_join}. *) 424let join_on (layout : layout) (j : Ast.join) = 425 if j.type_ <> Inner then unsupported (); 426 if j.natural || j.using <> [] then natural_on layout j else j.on 427 428(* The scan filter: the [WHERE] conjoined with every inner-join [ON]. *) 429let filter_of (layout : layout) (s : Ast.select) = 430 let fexprs = 431 (match s.where with Some w -> [ w ] | None -> []) 432 @ List.map (join_on layout) s.joins 433 in 434 match fexprs with 435 | [] -> Ir.Always 436 | e :: rest -> 437 List.fold_left 438 (fun acc e -> Ir.And (acc, predicate layout e)) 439 (predicate layout e) rest 440 441(* A negative LIMIT is unbounded ([None]); a huge one clamps to [max_int] (every 442 row kept); a non-literal one falls back (sqlite3). *) 443let limit_of params (s : Ast.select) = 444 match s.limit with 445 | None -> None 446 | Some e -> ( 447 match lit_int64 params e with 448 | None -> unsupported () 449 | Some n when n < 0L -> None 450 | Some n -> Some (clamp_count n)) 451 452(* A negative OFFSET is zero; a huge one clamps to [max_int] (every row 453 skipped); a non-literal one falls back (sqlite3). *) 454let offset_of params (s : Ast.select) = 455 match s.offset with 456 | None -> 0 457 | Some e -> ( 458 match lit_int64 params e with 459 | None -> unsupported () 460 | Some n when n < 0L -> 0 461 | Some n -> clamp_count n) 462 463(* A single trailing outer join (one [FROM] table, one outer-joined table): its 464 kind and [ON] predicate, with the [WHERE] kept separate so the engine can 465 null-fill before filtering. An inner (or wider) join shape returns [None], so 466 the inner path folds every [ON] into one filter. *) 467let outer_kind_of : Ast.join_type -> Ir.outer_kind option = function 468 | Left -> Some Ir.Left 469 | Right -> Some Ir.Right 470 | Full -> Some Ir.Full 471 | Inner -> None 472 473let left_join (layout : layout) (s : Ast.select) = 474 match s.joins with 475 | [ j ] -> ( 476 match outer_kind_of j.type_ with 477 | None -> None 478 | Some kind -> 479 let on = 480 if j.natural || j.using <> [] then natural_on layout j else j.on 481 in 482 let where = 483 match s.where with 484 | Some w -> predicate layout w 485 | None -> Ir.Always 486 in 487 Some ((kind, predicate layout on), where)) 488 | _ -> None 489 490let query ?(params = [||]) (layout : layout) (s : Ast.select) : 491 Ir.query * (subkind * Ast.select) list = 492 if s.group_by <> [] || s.having <> None || s.compound <> [] then 493 unsupported (); 494 sub_acc := []; 495 sub_count := 0; 496 sub_allow := true; 497 let arities = List.map (fun (_, cols) -> List.length cols) layout in 498 let left, filter = 499 match left_join layout s with 500 | Some (kind_on, where) -> (Some kind_on, where) 501 | None -> (None, filter_of layout s) 502 in 503 let hidden = hidden_columns layout s @ sentinel_columns layout in 504 let project = List.concat_map (project_item ~hidden layout) s.cols in 505 let select = { Ir.arities; left; filter; project } in 506 let order_by = List.map (order_key (expr layout)) s.order_by in 507 let q = 508 { 509 Ir.select; 510 order_by; 511 distinct = s.distinct; 512 distinct_colls = distinct_collations s; 513 limit = limit_of params s; 514 offset = offset_of params s; 515 } 516 in 517 (q, List.rev !sub_acc) 518 519(* The aggregates {!agg_fold} folds: a star [count], the numeric aggregates, and 520 the string aggregate (group_concat / string_agg). A FILTER / ORDER-BY 521 aggregate and a window are not here, so they fall back to the tree-walker. *) 522let numeric_aggs = [ "count"; "sum"; "min"; "max"; "avg"; "total" ] 523let string_aggs = [ "group_concat"; "string_agg" ] 524 525(* A plain aggregate call: a star [count], a numeric aggregate of an argument, 526 or a string aggregate ([group_concat]/[string_agg], with an optional 527 separator argument), optionally [DISTINCT] -- its name, distinctness, and 528 argument expressions ([] for a star count). [None] for anything else. *) 529let core_aggregate : Ast.expr -> (string * bool * Ast.expr list) option = 530 function 531 | Func ("count", [ Star ]) -> Some ("count", false, []) 532 | Func (name, args) 533 when (List.mem name numeric_aggs || List.mem name string_aggs) 534 && Query.is_agg name args -> 535 Some (name, false, args) 536 | Distinct_agg (name, arg) 537 when List.mem name numeric_aggs || List.mem name string_aggs -> 538 Some (name, true, [ arg ]) 539 | _ -> None 540 541(* A plain aggregate call, peeling any [FILTER (WHERE p)] and [ORDER BY] around 542 the core call: its name, distinctness, arguments, optional FILTER predicate, 543 and ORDER BY keys. [None] for anything that is not an aggregate. *) 544let as_aggregate e = 545 let rec peel filter order = function 546 | Ast.Filter (inner, p) -> peel (Some p) order inner 547 | Ast.Ordered_agg (inner, ob) -> peel filter ob inner 548 | core -> ( 549 match core_aggregate core with 550 | Some (name, distinct, args) -> 551 Some (name, distinct, args, filter, order) 552 | None -> None) 553 in 554 peel None [] e 555 556(* The value-level aggregate fold injected into {!Catalog.Ir.run_grouped}. The 557 engine streams argument rows rather than materialising one tuple list per 558 group row; DISTINCT has already been applied. [count] counts rows (star) or 559 non-NULL first arguments; [group_concat]/[string_agg] join the non-NULL first 560 arguments by the separator (the second argument, default ","); the rest fold 561 the non-NULL first arguments through {!Value_ops.numeric_agg} -- the same 562 folds the tree-walker uses. *) 563let agg_fold name _distinct (rows : Ir.agg_rows) : V.t = 564 if rows.nargs = 0 then Int (Int64.of_int rows.rows) 565 else 566 let first_args = ref [] in 567 let sep = ref "," in 568 let sep_set = ref false in 569 rows.iter (fun row base -> 570 if rows.nargs > 0 then ( 571 let v = row.(base) in 572 if v <> V.Null then first_args := v :: !first_args; 573 if (not !sep_set) && rows.nargs > 1 then ( 574 sep := Value_ops.text_of_value row.(base + 1); 575 sep_set := true))); 576 let first_args = List.rev !first_args in 577 match () with 578 | _ when List.mem name string_aggs -> ( 579 match first_args with 580 | [] -> Null 581 | vs -> Text (String.concat !sep (List.map Value_ops.text_of_value vs))) 582 | _ when name = "count" -> Int (Int64.of_int (List.length first_args)) 583 | _ -> Value_ops.numeric_agg name first_args 584 585(* Map an output expression over the synthetic per-group row, whose columns are 586 the group keys ([0..nkeys-1]) then the aggregate results. A group key or an 587 aggregate becomes a column reference; a literal or parameter stays itself; a 588 compound expression (arithmetic, a comparison, a scalar function, CAST) 589 recurses on its operands, so an aggregate expression like [count() + 1] or a 590 scalar function of a key is computed over the group row. Anything else (a 591 bare non-key column, a window) falls back. [collect] registers an aggregate 592 and returns its slot. *) 593let rec out_operand ~nkeys ~ncols ~key_index ~collect ~resolve_bare 594 (e : Ast.expr) : Ir.expr = 595 let recur = out_operand ~nkeys ~ncols ~key_index ~collect ~resolve_bare in 596 match key_index e with 597 | Some i -> Col (0, i) 598 | None -> ( 599 match as_aggregate e with 600 (* the synthetic row is [keys, representative columns, aggregate results], 601 so an aggregate sits past the [ncols] representative columns *) 602 | Some info -> Col (0, nkeys + ncols + collect e info) 603 | None -> ( 604 match e with 605 | Lit v -> Lit v 606 | Param i -> Param i 607 | Unop_not e -> Not (recur e) 608 | Binary (op, a, b) -> 609 if is_cmp op then Cmp (ir_cmp op, recur a, recur b) 610 else if is_logic op then Logic (ir_logic op, recur a, recur b) 611 else Arith (ir_arith op, recur a, recur b) 612 | Is_null e -> Null_value (true, recur e) 613 | Is_not_null e -> Null_value (false, recur e) 614 | Is (a, b) -> Is (recur a, recur b) 615 | In_list (e, cands) -> 616 In 617 ( recur (strip_collate e), 618 nonbinary_collation e, 619 is_col_ast e, 620 List.map recur cands ) 621 | Func (name, args) when name <> "iif" && not (Query.is_agg name args) 622 -> 623 Func (name, List.map recur args) 624 | Cast (e, ty) -> Cast (recur e, ty) 625 (* a bare (non-key) column reads from the group's representative 626 row *) 627 | Col _ -> resolve_bare e 628 | _ -> unsupported ())) 629 630let rec out_pred ~op_of (e : Ast.expr) : Ir.predicate = 631 match e with 632 | Binary (And, a, b) -> And (out_pred ~op_of a, out_pred ~op_of b) 633 | Binary (Or, a, b) -> Or (out_pred ~op_of a, out_pred ~op_of b) 634 | Binary (op, a, b) when is_cmp op -> Compare (ir_cmp op, op_of a, op_of b) 635 | Is_null e -> Null_test (true, op_of e) 636 | Is_not_null e -> Null_test (false, op_of e) 637 | _ -> unsupported () 638 639(* The position of [e] among the group keys (by structural expression, ignoring 640 a [COLLATE] wrapper on either side: [GROUP BY a COLLATE NOCASE] groups by 641 [a], so a projected bare [a] is that key). *) 642let key_index keys e = 643 let e = strip_collate e in 644 let rec go i = function 645 | k :: _ when Ast.equal_expr (strip_collate k) e -> Some i 646 | _ :: t -> go (i + 1) t 647 | [] -> None 648 in 649 go 0 keys 650 651(* Whether [s]'s output (select list, HAVING, ORDER BY) reads a bare (non-key, 652 non-aggregate) column or a [*] -- the cases that need a representative group 653 row materialised. A column matching a group key (ignoring a [COLLATE] 654 wrapper) or sitting inside an aggregate does not. *) 655let references_bare_or_star keys (s : Ast.select) = 656 let is_key e = key_index keys e <> None in 657 let rec bare (e : Ast.expr) = 658 if is_key e then false 659 else if as_aggregate e <> None then false 660 else 661 match e with 662 | Star | Qualified_star _ | Col _ -> true 663 | Lit _ | Param _ -> false 664 | Unop_not e 665 | Is_null e 666 | Is_not_null e 667 | Cast (e, _) 668 | Collate (e, _) 669 | Distinct_agg (_, e) -> 670 bare e 671 | Binary (_, a, b) | Is (a, b) | Filter (a, b) -> bare a || bare b 672 | In_list (e, l) -> bare e || List.exists bare l 673 | Func (_, args) -> List.exists bare args 674 | Ordered_agg (a, ob) -> bare a || List.exists (fun (e, _) -> bare e) ob 675 | Case (base, brs, els) -> 676 Option.fold ~none:false ~some:bare base 677 || List.exists (fun (c, x) -> bare c || bare x) brs 678 || Option.fold ~none:false ~some:bare els 679 | Window _ | In_select _ | Scalar_select _ | Exists _ -> false 680 in 681 List.exists (fun (rc : Ast.result_col) -> bare rc.expr) s.cols 682 || (match s.having with Some h -> bare h | None -> false) 683 || List.exists (fun (e, _) -> bare e) s.order_by 684 685let column_bases arities = 686 let a = Array.of_list arities in 687 let b = Array.make (Array.length a) 0 in 688 for c = 1 to Array.length a - 1 do 689 b.(c) <- b.(c - 1) + a.(c - 1) 690 done; 691 b 692 693let representative_shape layout keys s = 694 let arities = List.map (fun (_, cols) -> List.length cols) layout in 695 let bases = column_bases arities in 696 let needs_rep = references_bare_or_star keys s in 697 let ncols = if needs_rep then List.fold_left ( + ) 0 arities else 0 in 698 (arities, bases, needs_rep, ncols) 699 700let representative_choice layout needs_rep (s : Ast.select) = 701 if not needs_rep then None 702 else 703 List.find_map 704 (fun (rc : Ast.result_col) -> 705 match rc.expr with 706 | Func ("min", [ a ]) -> Some (false, expr layout a) 707 | Func ("max", [ a ]) -> Some (true, expr layout a) 708 | _ -> None) 709 s.cols 710 711let bare_resolver layout ~nkeys ~bases = function 712 | Ast.Col (alias, name) -> 713 let c, j = resolve_col layout alias name in 714 Ir.Col (0, nkeys + bases.(c) + j) 715 | _ -> unsupported () 716 717let aggregate_collector layout = 718 let aggs = ref [] and nagg = ref 0 in 719 let collect e info = 720 let rec go i = function 721 | (e', _) :: _ when e' = e -> Some i 722 | _ :: t -> go (i + 1) t 723 | [] -> None 724 in 725 match go 0 (List.rev !aggs) with 726 | Some i -> i 727 | None -> 728 let name, distinct, args, filter, order = info in 729 let agg = 730 { 731 Ir.name; 732 distinct; 733 args = List.map (expr layout) args; 734 filter = Option.map (expr layout) filter; 735 order = List.map (fun (e, d) -> (expr layout e, ir_dir d)) order; 736 } 737 in 738 aggs := (e, agg) :: !aggs; 739 let slot = !nagg in 740 incr nagg; 741 slot 742 in 743 (collect, (fun () -> !nagg), fun () -> List.rev_map snd !aggs) 744 745let aggregate_star_cols layout ~nkeys ~bases qopt = 746 List.concat 747 (List.mapi 748 (fun c (al, cols) -> 749 match qopt with 750 | Some q when String.uppercase_ascii q <> String.uppercase_ascii al -> 751 [] 752 | _ -> 753 (* the hidden rowid slot is not a real column: [*] skips it *) 754 List.filteri (fun j _ -> List.nth cols j <> rowid_sentinel) cols 755 |> List.mapi (fun j _ -> Ir.Col (0, nkeys + bases.(c) + j))) 756 layout) 757 758let aggregate_project layout ~nkeys ~bases op (s : Ast.select) = 759 let star_cols = aggregate_star_cols layout ~nkeys ~bases in 760 List.concat_map 761 (fun (rc : Ast.result_col) -> 762 match rc.expr with 763 | Star -> star_cols None 764 | Qualified_star q -> 765 if 766 List.exists 767 (fun (al, _) -> 768 String.uppercase_ascii al = String.uppercase_ascii q) 769 layout 770 then star_cols (Some q) 771 else unsupported () 772 | e -> [ op e ]) 773 s.cols 774 775let aggregate_output ?(params = [||]) ~nkeys ~ncols ~nagg op project 776 (s : Ast.select) = 777 (* HAVING and ORDER BY register their aggregates through [op] too, so [nagg] 778 (the live count) is read only after both are lowered -- an aggregate that 779 appears solely in HAVING / ORDER BY still gets a synthetic-row slot. *) 780 let filter = 781 match s.having with Some h -> out_pred ~op_of:op h | None -> Always 782 in 783 let order_by = List.map (order_key op) s.order_by in 784 let arities = [ nkeys + ncols + nagg () ] in 785 { 786 Ir.select = { arities; left = None; filter; project }; 787 order_by; 788 distinct = s.distinct; 789 distinct_colls = distinct_collations s; 790 limit = limit_of params s; 791 offset = offset_of params s; 792 } 793 794let aggregate_scan layout keys (s : Ast.select) = 795 let left, filter = 796 match left_join layout s with 797 | Some (kind_on, where) -> (Some kind_on, where) 798 | None -> (None, filter_of layout s) 799 in 800 (left, filter, List.map (fun e -> expr layout (strip_collate e)) keys) 801 802let aggregate_query ?(params = [||]) (layout : layout) (s : Ast.select) : 803 Ir.grouped * (subkind * Ast.select) list = 804 if s.compound <> [] then unsupported (); 805 (* a correlated scalar / EXISTS / IN subquery in the scan filter (WHERE / ON) 806 registers here, for the caller to wire a runner -- the per-source-row scope 807 the engine passes matches what the grouped scan sees *) 808 sub_acc := []; 809 sub_count := 0; 810 sub_allow := true; 811 let keys = s.group_by in 812 let nkeys = List.length keys in 813 let key_index = key_index keys in 814 let arities, bases, needs_rep, ncols = representative_shape layout keys s in 815 (* sqlite3's companion rule: a bare column takes its value from the row 816 holding a lone min/max in the select list (else an arbitrary, here the 817 first, row) *) 818 let rep = representative_choice layout needs_rep s in 819 let resolve_bare = bare_resolver layout ~nkeys ~bases in 820 (* aggregates in first-seen order, structurally deduped so a repeated 821 aggregate shares one slot *) 822 let collect, nagg, aggs = aggregate_collector layout in 823 let op = out_operand ~nkeys ~ncols ~key_index ~collect ~resolve_bare in 824 (* [*] (or [t.*]) expands to the representative row's scanned columns, of the 825 matching cursor for a qualified star; an unmatched qualified star falls back 826 so the tree-walker reports "no such table". *) 827 (* project / HAVING / ORDER BY all register their aggregates through [op] *) 828 let project = aggregate_project layout ~nkeys ~bases op s in 829 let output = aggregate_output ~params ~nkeys ~ncols ~nagg op project s in 830 (* A single trailing LEFT JOIN null-fills before grouping; its [ON] is the 831 [left] predicate, the [WHERE] the scan filter. Otherwise every [ON] folds 832 into the filter, as for an inner join. *) 833 let left, filter, group_by = aggregate_scan layout keys s in 834 let g = 835 { 836 Ir.arities; 837 left; 838 filter; 839 group_by; 840 group_colls = List.map order_collation keys; 841 ncols; 842 rep; 843 aggs = aggs (); 844 output; 845 } 846 in 847 (g, List.rev !sub_acc)