Something went wrong. Try again.
My own corner of monopam
Something went wrong. Try again.
10 kB · 234 lines
OCaml
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235(*--------------------------------------------------------------------------- Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*)
type t = | Check_constraint | Partial_index | Index_expression | Generated_column
let to_string = function | Check_constraint -> "CHECK constraints" | Partial_index -> "partial index WHERE clauses" | Index_expression -> "index expressions" | Generated_column -> "generated columns"
(* How sqlite3 names the context in a refusal raised while the expression runs rather than while it is resolved (sqlite/src/vdbeaux.c, sqlite3NotPureFunc). It is a different sentence from {!to_string}'s, and it tells an index key from a partial predicate not at all: to a reader of the message both are the index. *)let in_ = function | Check_constraint -> "a CHECK constraint" | Generated_column -> "a generated column" | Partial_index | Index_expression -> "an index"
let pp = Fmt.of_to_string to_stringlet prohibited t what = Fmt.failwith "%s prohibited in %s" what (to_string t)
(* SQLite allows a non-deterministic function in a CHECK constraint and refuses it in the other three, where the value it decides is written once and read back later (sqlite/src/resolve.c: the mask is NC_IdxExpr|NC_PartIdx|NC_GenCol, without NC_IsCheck). *)let must_be_deterministic = function | Check_constraint -> false | Partial_index | Index_expression | Generated_column -> true
(* SQLite refuses the "." operator itself in an index key and a generated column, before resolving what it qualifies (sqlite/src/resolve.c, the TK_DOT case under NC_IdxExpr|NC_GenCol), so the indexed table's own name is refused as flatly as another table's. A CHECK constraint and a partial index predicate resolve it instead, and silently ignore a schema qualifier on it. *)let admits_qualifier = function | Check_constraint | Partial_index -> true | Index_expression | Generated_column -> false
let ignores_schema = function | Check_constraint | Partial_index -> true | Index_expression | Generated_column -> false
(* The implicit rowid falls to the same mask (sqlite/src/resolve.c, lookupName: the rowid case is reached only when [(pNC->ncFlags & (NC_IdxExpr|NC_GenCol))==0], and then only for a table that has one). Two contexts resolve it and two refuse it, and the file a refused one would have written is one sqlite3 declines to open. *)let admits_rowid = function | Check_constraint | Partial_index -> true | Index_expression | Generated_column -> false
(* The name of the function call inside an expression that is one: a call wears FILTER, ORDER BY and OVER decorations that are not the call. *)let rec called_name : Ast.expr -> string option = function | Ast.Func (n, _) | Ast.Distinct_agg (n, _) -> Some n | Ast.Filter (e, _) | Ast.Ordered_agg (e, _) | Ast.Window (e, _) -> called_name e | _ -> None
(* The aggregate call inside an expression that is one, with its name. A window function is not one: the same name over a frame is refused as a window function, which is what SQLite calls it. *)let rec agg_name : Ast.expr -> string option = function | Ast.Func (n, args) when Query.is_agg n args -> Some n | Ast.Distinct_agg (n, _) -> Some n | Ast.Filter (e, _) | Ast.Ordered_agg (e, _) -> agg_name e | _ -> None
(* The expression resolves against its own table and nothing else, so a qualifier naming anything but that table is as unresolvable as an unknown column -- and reads that way, as sqlite3 spells it. *)let resolve_column ~rowid ~table ~cols qual name = let known = List.exists (Ast.ident_equal name) cols || (rowid && Query.is_rowid_name name) in match qual with | None -> if not known then Fmt.failwith "no such column: %s" name | Some q -> if (not (Ast.ident_equal q table)) || not known then Fmt.failwith "no such column: %s.%s" q name
let column t ~rowid ~table ~cols qual name = match qual with | Some _ when not (admits_qualifier t) -> prohibited t "the \".\" operator" | _ -> resolve_column ~rowid:(rowid && admits_rowid t) ~table ~cols qual name
(* A three-part name whose schema was resolved rather than dropped. There is one schema here, so it holds no table and the whole reference reads back. *)let schema_column ~schema ~table ~column = Fmt.failwith "no such column: %s.%s.%s" schema table column
(* The node itself, before its children: an aggregate is refused by the call rather than by what is inside it, as sqlite3 refuses it (resolve.c prunes the walk at the call). *)let node t ~rowid ~table ~cols e = match agg_name e with | Some n -> Fmt.failwith "misuse of aggregate function %s()" n | None -> ( match e with | Ast.In_select _ | Ast.Scalar_select _ | Ast.Exists _ -> prohibited t "subqueries" | Ast.Param _ -> prohibited t "parameters" | Ast.Window (w, _) -> ( match called_name w with | Some n -> failwith (Func.window_misuse n) | None -> prohibited t "window functions") | Ast.Func (n, _) -> (* SQLite resolves the name where the expression is written, so a name that is no function is refused here rather than at the first row (resolve.c, the TK_FUNCTION case: sqlite3FindFunction ahead of every other test). *) if not (Func.exists n) then Fmt.failwith "no such function: %s" n; if must_be_deterministic t && not (Func.deterministic n) then prohibited t "non-deterministic functions" | Ast.Col (qual, name) -> column t ~rowid ~table ~cols qual name (* Where the schema is ignored, dropping it and asking again is the whole rule; where it is resolved, it names nothing. The grammar has already dropped it for the two contexts that ignore it, so what arrives here is one of the other two -- the mask is what decides, all the same. *) | Ast.Schema_col (s, qual, name) -> if ignores_schema t then column t ~rowid ~table ~cols (Some qual) name else schema_column ~schema:s ~table:qual ~column:name | _ -> ())
let children : Ast.expr -> Ast.expr list = function | Ast.Lit _ | Ast.Big_int _ | Ast.Param _ | Ast.Col _ | Ast.Schema_col _ | Ast.Star | Ast.Qualified_star _ -> [] | Ast.Unop_not a | Ast.Is_null a | Ast.Is_not_null a | Ast.Cast (a, _) | Ast.Unary_plus a | Ast.Unary_minus a | Ast.Collate (a, _) | Ast.Distinct_agg (_, a) -> [ a ] | Ast.Binary (_, a, b) | Ast.Is (a, b) | Ast.Filter (a, b) -> [ a; b ] | Ast.In_list (a, xs) -> a :: xs | Ast.Func (_, args) -> args | Ast.Ordered_agg (a, keys) -> a :: List.map fst keys | Ast.Case (base, arms, else_) -> Option.to_list base @ List.concat_map (fun (c, r) -> [ c; r ]) arms @ Option.to_list else_ (* Refused at the node, so their insides are never reached. *) | Ast.In_select _ | Ast.Scalar_select _ | Ast.Exists _ | Ast.Window _ -> [] (* The action is a keyword; only the message is an expression. *) | Ast.Raise (_, m) -> Option.to_list m
let check t ?(rowid = false) ~table ~cols e = let rec walk e = node t ~rowid ~table ~cols e; List.iter walk (children e) in walk e
(* The refusals that are about the names alone, which is what SQLite reaches through a bare [sqlite3ResolveExprNames] with no [NC_*] flag: an aggregate or a window function, where there is neither a group nor a frame; a name that is no function; a column the table does not have. Everything {!node} adds on top is what a stored expression's flags ask for. *)let names_node ~rowid ~table ~cols e = match agg_name e with | Some n -> Fmt.failwith "misuse of aggregate function %s()" n | None -> ( match e with | Ast.Window (w, _) -> ( match called_name w with | Some n -> failwith (Func.window_misuse n) | None -> ()) | Ast.Func (n, _) -> if not (Func.exists n) then Fmt.failwith "no such function: %s" n | Ast.Col (qual, name) -> resolve_column ~rowid ~table ~cols qual name (* No mask, so nothing ignores the schema: the qualifier is looked up like a query's, and this engine has one schema for it to miss. *) | Ast.Schema_col (s, q, c) -> schema_column ~schema:s ~table:q ~column:c | _ -> ())
let check_names ?(rowid = false) ~table ~cols e = let rec walk e = names_node ~rowid ~table ~cols e; List.iter walk (children e) in walk e
(* Every column [e] names, however deep. A subquery, a window and an aggregate are pruned by {!children}, and all three are refused outright in a generated column, so nothing a cycle could hide in is missed here. *)let rec columns_named e = (match e with Ast.Col (_, name) -> [ name ] | _ -> []) @ List.concat_map columns_named (children e)
let check_generated_loop gens = let named name = List.exists (fun (g, _) -> Ast.ident_equal g name) gens in let computable = ref [] in let is_computable name = List.exists (Ast.ident_equal name) !computable in (* One pass computes every generated column whose generated inputs are already computed, exactly as sqlite3 does, and passes repeat while one of them makes progress (sqlite/src/insert.c, the [pRedo] loop of sqlite3ComputeGeneratedColumns). What is left when a pass computes nothing is a cycle, and the column sqlite3 names is the last of those declared. *) let rec passes () = let progressed = List.fold_left (fun acc (name, e) -> if is_computable name then acc else if List.for_all (fun n -> (not (named n)) || is_computable n) (columns_named e) then begin computable := name :: !computable; true end else acc) false gens in if progressed then passes () in passes (); match List.rev (List.filter (fun (n, _) -> not (is_computable n)) gens) with | (n, _) :: _ -> Fmt.failwith "generated column loop on \"%s\"" n | [] -> ()