Something went wrong. Try again.
My own corner of monopam
Something went wrong. Try again.
28 kB · 653 lines
OCaml
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654(*--------------------------------------------------------------------------- Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. SPDX-License-Identifier: ISC ---------------------------------------------------------------------------*)
(* What a CREATE TABLE declares, read back out of it: the columns and their affinities, the DEFAULT expressions, which columns an index keys on and under which sequence and order, and the UNIQUE and CHECK constraints. All of it answers about a schema, so none of it needs a handle or a b-tree. *)
open Modelmodule Engine = Sql.Enginemodule Query = Sql.Querymodule Lexer = Sql.Lexer
(* A catalog for an expression that may not reach a relation: it answers none and says which expression asked, carrying no per-table state so one value serves every evaluation. *)let closed_catalog what : Query.catalog = let no_table kind name = Fmt.failwith "%s cannot read %s %S" what kind name in { columns = (fun n -> no_table "the columns of" n); rows = (fun n -> no_table "the rows of" n); rows_rowid = (fun n -> no_table "the rows of" n); has_rowid = (fun n -> no_table "the rows of" n); index_scan = (fun n _ -> no_table "an index on" n); best_index_for = (fun n _ -> no_table "an index on" n); affinity = (fun n _ -> no_table "the affinity of" n); row_count = (fun n ~upto:_ -> no_table "the rows of" n); collation = (fun n _ -> no_table "the collation of" n); table_function = (fun _ -> None); scalar_function = (fun _ -> None); subquery = (fun _ _ _ _ -> no_table "a subquery over" ""); subquery_memo = Hashtbl.create 1; recursion_cap = Sql.Query.default_recursion_cap; }
(* A DEFAULT expression is required to be constant, so evaluating one needs no table (CREATE TABLE refuses a default that references a column). *)let const_catalog = closed_catalog "a DEFAULT expression"
(* Text this engine cannot read is not an error here: it is a table whose facts it does not know, kept so the rest of the database stays readable. *)let parse_sql sql = match Sql.Lexer.parse sql with Ok ct -> Some ct | Error _ -> None
(* The columns a table's PRIMARY KEY is over, whether it was declared on a column or as a table constraint. *)let primary_key_columns (ct : Sql.Ast.create_table) = List.filter_map (fun (c : Sql.Ast.column_def) -> if c.primary_key <> None then Some c.name else None) ct.columns @ List.concat_map (function | Sql.Ast.Primary_key (cs, _) -> Sql.Ast.indexed_column_names cs | _ -> []) ct.table_constraints
(* Whether [c] is the rowid under another name. An INTEGER PRIMARY KEY aliases the rowid only where there is one to alias, and a WITHOUT ROWID table has none, so the parser states what the column alone declares and this what the table makes of it. *)let is_rowid_alias (ct : Sql.Ast.create_table) (c : Sql.Ast.column_def) = c.is_rowid_alias && not ct.ct_without_rowid
(* Whether column [c] must hold a value, and how a NULL there resolves. A PRIMARY KEY column carries NOT NULL whether or not it declares one in a STRICT table and in a WITHOUT ROWID one, whose key cannot be missing; the rowid alias is outside both. Such an imposed NOT NULL is ABORT without a clause; anywhere else a PRIMARY KEY column accepts NULL. *)let column_not_null (ct : Sql.Ast.create_table) pk (c : Sql.Ast.column_def) = match c.not_null with | Some _ as declared -> declared | None -> if (ct.ct_strict || ct.ct_without_rowid) && (not (is_rowid_alias ct c)) && List.exists (Catalog.Ident.equal c.name) pk then Some Sql.Ast.Unspecified else None
let columns_of_parsed = function | None -> [] | Some (ct : Sql.Ast.create_table) -> let pk = primary_key_columns ct in List.map (fun (c : Sql.Ast.column_def) -> { name = c.name; affinity = c.affinity; is_rowid_alias = is_rowid_alias ct c; not_null = column_not_null ct pk c; primary_key = c.primary_key; collation = c.collation; generated = Option.map (fun (g : Sql.Ast.generated_column) -> g.kind) (Catalog.Ident.assoc_opt c.name ct.generated); }) ct.columns
let parse_create_table sql = columns_of_parsed (parse_sql sql)
(* Whether the table computes column [name] rather than storing it. *)let is_generated (ct : Sql.Ast.create_table) name = List.exists (fun (g, _) -> Sql.Ast.ident_equal g name) ct.generated
(* The two rules sqlite3 decides while parsing the column they are declared on (sqlite3AddDefaultValue and sqlite3AddPrimaryKey, sqlite/src/build.c), which is why they are raised ahead of everything below: a generated column is computed from the row it is written with, which leaves it nothing to default to and nothing settled enough to be keyed by. *)let check_generated_column (ct : Sql.Ast.create_table) = List.iter (fun (col, _) -> if is_generated ct col then failwith "cannot use DEFAULT on a generated column") ct.defaults; List.iter (fun col -> if is_generated ct col then failwith "generated columns cannot be part of the PRIMARY KEY") (primary_key_columns ct)
(* A table constraint's key columns and a foreign key's child columns, resolved against the columns this table declares, both decided before the stored expressions. A table with a defect in each is reported for the constraint, where sqlite3 reports the earlier of the two. *)let check_constraint_columns (ct : Sql.Ast.create_table) ~declares = List.iter (fun c -> if not (declares c) then Fmt.failwith "no such column: %s" c) (List.concat_map (function | Sql.Ast.Unique (cs, _) | Sql.Ast.Primary_key (cs, _) -> Sql.Ast.indexed_column_names cs | Sql.Ast.Other -> []) ct.table_constraints); List.iter (fun (fk : Sql.Ast.foreign_key) -> List.iter (fun c -> if not (declares c) then Fmt.failwith "unknown column %S in foreign key definition" c) fk.cols) ct.foreign_keys
(* A generated column is computed from the row it is written with, so a table declaring nothing else has no row to compute one from and is refused. Asked ahead of the stored expressions, which is what a DROP COLUMN leaving only generated columns reads. *)let check_non_generated_column (ct : Sql.Ast.create_table) = match ct.generated with | [] -> () | _ :: _ -> if List.for_all (fun (c : Sql.Ast.column_def) -> is_generated ct c.name) ct.columns then failwith "must have at least one non-generated column"
(* A CHECK constraint and a generation expression are both stored and re-evaluated against one row, so each is narrowed to what one row answers ({!Sql.Self_ref}). sqlite3 resolves them at the end of the table (sqlite3ResolveSelfReference from sqlite3EndTable), after every rule above. *)let check_stored_exprs (ct : Sql.Ast.create_table) ~cols = let table = ct.tbl_name in let rowid = not ct.ct_without_rowid in List.iter (fun (c : Sql.Ast.check_constraint) -> Sql.Self_ref.check Check_constraint ~rowid ~table ~cols c.expr) ct.checks; List.iter (fun (_, (g : Sql.Ast.generated_column)) -> Sql.Self_ref.check Generated_column ~rowid ~table ~cols g.expr) ct.generated
(* Everything a CREATE TABLE can say that sqlite3 refuses to hold in a schema, in the order it raises them. Asked twice of every table: of the creating statement, and of the text read back out of sqlite_master. The generation cycle is not here; it belongs to CREATE TABLE alone ({!Sql.Self_ref.check_generated_loop}). *)let check_declaration (ct : Sql.Ast.create_table) = let cols = List.map (fun (c : Sql.Ast.column_def) -> c.name) ct.columns in let declares c = List.exists (Sql.Ast.ident_equal c) cols in check_generated_column ct; check_constraint_columns ct ~declares; check_non_generated_column ct; check_stored_exprs ct ~cols
(* The two halves of reading a stored CREATE TABLE as one answer: the text declares this table, or it does not and this is why. The reason comes back rather than the sentence, each caller wording its own refusal. *)let checked_declaration sql = match Sql.Lexer.parse sql with | Error reason -> Error reason | Ok ct -> ( match check_declaration ct with | () -> Ok ct | exception Failure reason -> Error reason)
let column_names columns = List.map (fun (c : column) -> c.name) columns
(* Whether the table was declared STRICT, read off the parse rather than off a flag this engine set: a reopened database, and a database another SQLite wrote, enforce the keyword exactly as the connection that created the table did. *)let strict_table = function | None -> false | Some (ct : Sql.Ast.create_table) -> ct.ct_strict
let strict_type ~table (c : column) = match Sql.Strict.of_type_name c.affinity with | Some st -> st | None -> if c.affinity = "" then Fmt.failwith "missing datatype for %s.%s" table c.name else Fmt.failwith "unknown datatype for %s.%s: %S" table c.name c.affinity
(* Each column paired with the type it must hold: the one it declares in a STRICT table, [None] in an ordinary one. Deferred and kept, since every value of every stored row is taken by this. *)let declared_types ~tbl_name ~parsed ~columns = lazy (let strict = strict_table parsed in List.map (fun (c : column) -> (c, if strict then Some (strict_type ~table:tbl_name c) else None)) columns)
(* Which columns a read widens an integer to a real in: one byte per declared column, set where the column's type gives it REAL affinity, and [None] where none has. A REAL column stores an integer-valued one as an integer and widens it back on the way out, so this is asked of every row a read spreads back. Deferred and kept, since a mask is a byte per column. *)let real_columns ~columns = lazy (let mask = Bytes.make (List.length columns) '\000' in List.iteri (fun i (c : column) -> if Catalog.Value_ops.affinity_of_type c.affinity = `Real then Bytes.set mask i '\001') columns; if Bytes.exists (fun b -> b <> '\000') mask then Some mask else None)
(* The catalog an expression over one row is evaluated through: a generation expression, a partial index's predicate or an expression key. Such an expression must answer from the row itself, so the catalog reaches no relation and what is left is what the columns declare. Kept beside the schema, since it is asked of every row a write touches. *)let column_named columns col = List.find_opt (fun (c : column) -> Catalog.Ident.equal c.name col) columns
let declared_sequence columns col = Option.bind (column_named columns col) (fun (c : column) -> c.collation)
let declared_affinity columns col = Option.map (fun (c : column) -> Catalog.Value_ops.affinity_of_type c.affinity) (column_named columns col)
let row_catalog ~columns = lazy { (closed_catalog "an expression over one row") with collation = (fun _ col -> declared_sequence columns col); affinity = (fun _ col -> declared_affinity columns col); }
(* An expression over one row with the sequence each column it compares declares made explicit, which is how the comparison reads it: the evaluator takes a sequence off an explicit [COLLATE] node, not off the catalog. The row is in scope under the table's own name, the only relation a stored expression may read. *)let collate_in_row ~tbl_name ~columns e = let env : Sql.Resolve.env = { columns = (fun _ -> column_names columns); collation = (fun _ col -> declared_sequence columns col); affinity = (fun _ col -> declared_affinity columns col); table_function_columns = (fun _ -> []); has_rowid = (fun _ -> true); } in Sql.Resolve.collate_over env [ (tbl_name, tbl_name) ] e
(* The generation expression of each [... AS (expr)] column, keyed by column, with the sequences its columns declare made explicit. One list for both evaluators, since a VIRTUAL column is recomputed on every read and a STORED one written once over the same row. *)let generation_exprs ~tbl_name ~parsed ~columns = lazy (match parsed with | None -> [] | Some (ct : Sql.Ast.create_table) -> List.map (fun (name, (g : Sql.Ast.generated_column)) -> (name, collate_in_row ~tbl_name ~columns g.expr)) ct.generated)
(* The CHECK constraints of a table: each the verbatim predicate source, for the error message, paired with the expression it is, sequences made explicit. A CHECK is evaluated by the write path and by the integrity check, each handed a different catalog, so a sequence taken off the catalog would differ per path and must be in the expression. *)let check_exprs ~tbl_name ~sql ~parsed ~columns = lazy (match parsed with | None -> [] | Some (ct : Sql.Ast.create_table) -> List.map (fun (c : Sql.Ast.check_constraint) -> ( String.sub sql c.start (c.stop - c.start), collate_in_row ~tbl_name ~columns c.expr )) ct.checks)
(* A partial index's predicate as the index carries it: what the CREATE INDEX text spells, sequences made explicit. [None] for an index that is not partial. The index carries it because more than one reader decides by it, and an entry only one would have written is a file the other reads as damaged. *)let index_predicate (schema : schema) where = Option.map (collate_in_row ~tbl_name:schema.tbl_name ~columns:schema.columns) where
(* The value each column defaults to, positionally, [Null] where none is declared. Deferred rather than evaluated with the rest of the schema: a DEFAULT this engine cannot evaluate is a table it can still read, and only a short record reaches these at all. *)let evaluated_defaults ~parsed ~columns = lazy (let defs = match parsed with None -> [] | Some ct -> ct.Sql.Ast.defaults in Array.of_list (List.map (fun (c : column) -> match Catalog.Ident.assoc_opt c.name defs with | Some e -> Engine.eval_const const_catalog [||] e | None -> Sql.Ast.Null) columns))
(* The one place a schema record is built. Its derived fields are functions of the columns and the parse beside them, so building them anywhere else is how a schema ends up carrying a lazy computed from the columns it replaced -- the ALTER paths derive a new column list from the old one, and go back through here for the rest. *)let schema ~tbl_name ~sql ~parsed ~columns = { tbl_name; columns; sql; parsed; defaults = evaluated_defaults ~parsed ~columns; names = lazy (column_names columns); strict = declared_types ~tbl_name ~parsed ~columns; real_cols = real_columns ~columns; row_catalog = row_catalog ~columns; generated = generation_exprs ~tbl_name ~parsed ~columns; checks = check_exprs ~tbl_name ~sql ~parsed ~columns; }
(* A schema whose columns the CREATE TABLE text alone decides, for a caller that has already parsed it: the one parse serves both the schema and whatever the caller does with the statement. *)let schema_of_ast ~tbl_name ~sql ct = let parsed = Some ct in schema ~tbl_name ~sql ~parsed ~columns:(columns_of_parsed parsed)
(* A schema whose columns the CREATE TABLE text alone decides. *)let schema_of_sql ~tbl_name sql = let parsed = parse_sql sql in schema ~tbl_name ~sql ~parsed ~columns:(columns_of_parsed parsed)
(* A schema whose columns the caller already holds: the kv tables, which declare theirs, and the ALTER paths, which derive the new column list from the old one rather than from the text they rewrite. *)let schema_of_columns ~tbl_name ~columns ~sql = schema ~tbl_name ~sql ~parsed:(parse_sql sql) ~columns
(* The first column a table declares twice, spelled as the offending declaration spells it. Two names differing only in ASCII case are one column (sqlite3AddColumn compares with sqlite3StrICmp, sqlite/src/build.c), so a table declaring both has two columns no query can tell apart -- and writing one is writing a sqlite_master row real sqlite3 refuses to open. *)let duplicate_column columns = let rec go seen = function | [] -> None | (c : column) :: rest -> if List.exists (Catalog.Ident.equal c.name) seen then Some c.name else go (c.name :: seen) rest in go [] columns
(* A table's PRIMARY KEY column names, in key order. Either spelling reaches it: a [PRIMARY KEY] on a column, or a table-level [PRIMARY KEY (a, b)]. *)let pk_column_names (schema : schema) = match schema.parsed with None -> [] | Some ct -> primary_key_columns ct
let column_index names name = Catalog.Ident.index name names
(* The collating sequence key column [pos] of an index is ordered by: the one the index definition names, else the column's declared COLLATE, else BINARY. This door takes the resolver's refusal as a value rather than a raise, since an application may register its own sequences and a file can name one this engine has never heard of, costing its indexes and not the database. *)let resolve_collation name = match Sql.Collation.resolve name with | c -> Ok c | exception Failure e -> Error e
(* The sentence a key column the table does not declare is refused with: the resolver's, since every index key reaches sqlite3ResolveSelfReference one at a time, so all three key shapes are refused the same way. *)let err_no_column col = Fmt.kstr (fun e -> Error e) "no such column: %s" col
(* The name BINARY, which a key naming no sequence is ordered by and which the pragmas print for it. *)let default_collation_name = Catalog.Collation.to_string Catalog.Collation.default
(* The collating sequence key column [pos] names, as the schema spells it: the index definition's own COLLATE, else the column's declared one, else BINARY. sqlite3 keeps the spelling on the index (Index.azColl) and prints it back unchanged, folding case only to resolve it. *)let key_collation_name (columns : column list) ~named pos = let declared = Option.bind (List.nth_opt columns pos) (fun c -> c.collation) in match if Option.is_some named then named else declared with | Some name -> name | None -> default_collation_name
let key_collation_result (columns : column list) ~named pos = let declared = Option.bind (List.nth_opt columns pos) (fun c -> c.collation) in let name = if Option.is_some named then named else declared in match name with | None -> Ok Catalog.Collation.default | Some name -> resolve_collation name
(* The same, where the caller is a statement being run rather than a schema being loaded: there is nothing to degrade, so an unknown name fails it. *)let key_collation columns ~named pos = match key_collation_result columns ~named pos with | Ok c -> c | Error e -> failwith e
(* The order key column [pos] of an index is compared in: the sequence above, and the direction the definition's ASC/DESC names. Every index resolves its orders here -- a UNIQUE/PRIMARY KEY autoindex and an explicit CREATE INDEX, built fresh or reconnected from the stored schema text -- so a reopened database orders its keys exactly as the handle that wrote them did. *)let key_order_result columns (c : Sql.Ast.indexed_column) pos = Result.map (fun coll -> ( Catalog.Order.v ~desc:c.desc coll, key_collation_name columns ~named:c.collation pos )) (key_collation_result columns ~named:c.collation pos)
(* The order an expression key is compared in: the sequence written on the key itself, its outermost node where that is a postfix COLLATE, else BINARY. The key computes under the depth-first sequence inside it and is filed under this one, sqlite3's read at CREATE INDEX; reading the inner one instead orders the entries by a sequence no other SQLite would use. *)let expression_key_order ~desc e = match e with | Sql.Ast.Collate (_, name) -> Result.map (fun c -> (Catalog.Order.v ~desc c, name)) (resolve_collation name) | _ -> Ok (Catalog.Order.v ~desc Catalog.Collation.default, default_collation_name)
(* [e] with every top-level COLLATE peeled off (sqlite/src/expr.c, sqlite3ExprSkipCollate). *)let rec skip_collate = function | Sql.Ast.Collate (e, _) -> skip_collate e | e -> e
(* An index key read as CREATE INDEX reads it. sqlite3 peels every top-level COLLATE off a key before deciding what the key is, so a COLLATE over a bare column is that column, ordered by the outermost sequence. A qualified name is no column key however wrapped, since sqlite3 refuses the "." while resolving it. *)let index_key_read (k : Sql.Ast.indexed_key) = match k with | Sql.Ast.Column _ -> k | Sql.Ast.Expression { expr; desc } -> ( match (expr, skip_collate expr) with | Sql.Ast.Collate (_, collation), Sql.Ast.Col (None, column) -> Sql.Ast.Column { column; collation = Some collation; desc } | _ -> k)
(* The sequence a bare reference to column [pos] compares under, which is the column's declared COLLATE (else BINARY) -- no index definition in sight. [None] where the column declares a sequence this engine does not have, since then nothing is known about the order such a reference wants. *)let declared_collation columns pos = Result.to_option (key_collation_result columns ~named:None pos)
(* Resolve the key columns of a UNIQUE/PRIMARY KEY constraint or a CREATE INDEX against the table they read: the position of each, the order each is compared in, what put the index in the schema, and how a duplicate key resolves. The failure text is what the index is unusable for. *)let resolve_index_keys (columns : column list) ~table ~origin ?(on_conflict = Sql.Ast.Unspecified) (keys : Sql.Ast.indexed_key list) : (index_key, string) result = let names = column_names columns in let column_key (c : Sql.Ast.indexed_column) = match column_index names c.column with | None -> err_no_column c.column | Some pos -> Result.map (fun (order, coll) -> (Column pos, order, coll)) (key_order_result columns c pos) in let rec resolve acc = function | [] -> Ok (List.rev acc) | (k : Sql.Ast.indexed_key) :: rest -> ( let resolved = match index_key_read k with | Sql.Ast.Column c -> column_key c (* The order is read off the expression as written and the key stores it collated: the sequence a comparison inside it reads is the one its column operand declares, while the sequence the key itself is ordered by is the one the expression assigns -- and the rewrite assigns sequences the expression did not. *) | Sql.Ast.Expression e -> Result.map (fun (order, coll) -> ( Expression (collate_in_row ~tbl_name:table ~columns e.expr), order, coll )) (expression_key_order ~desc:e.desc e.expr) in match resolved with | Error _ as e -> e | Ok r -> resolve (r :: acc) rest) in Result.map (fun resolved -> { keys = List.map (fun (k, _, _) -> k) resolved; orders = List.map (fun (_, o, _) -> o) resolved; colls = List.map (fun (_, _, c) -> c) resolved; origin; on_conflict; }) (resolve [] keys)
let resolve_index_columns columns ~table ~origin ?on_conflict (cols : Sql.Ast.indexed_column list) = resolve_index_keys columns ~table ~origin ?on_conflict (List.map (fun c -> Sql.Ast.Column c) cols)
(* A table-level UNIQUE / PRIMARY KEY builds the same kind of index a column-level one does, and its [ON CONFLICT] rides along the same way: that clause is the index's own resolution, sqlite3's pIdx->onError. *)let table_unique_constraint columns ~table = function | Sql.Ast.Unique (cols, on_conflict) -> Some (resolve_index_columns columns ~table ~origin:Unique_constraint ~on_conflict cols) | Sql.Ast.Primary_key (cols, on_conflict) -> Some (resolve_index_columns columns ~table ~origin:Primary_key ~on_conflict cols) | _ -> None
(* [~without_rowid] decides whether an [INTEGER PRIMARY KEY] column is a key of its own: where the rows are keyed by a rowid it is an alias for that rowid and needs no index, where there is no rowid it is an ordinary key column. *)let column_unique_constraint ~without_rowid columns ~table (c : Sql.Ast.column_def) = let keyed = match c.primary_key with | Some res when without_rowid || not c.is_rowid_alias -> Some (Primary_key, res) | _ -> Option.map (fun res -> (Unique_constraint, res)) c.unique in (* A column-level UNIQUE names no sequence of its own, so its index takes the column's declared COLLATE. Its [ON CONFLICT] rides along: that clause is the index's own resolution, sqlite3's pIdx->onError. A column-level PRIMARY KEY may name a direction, and that direction is the index's, so it is carried over; a UNIQUE cannot, and reads as ascending. *) Option.map (fun (origin, on_conflict) -> let desc = origin = Primary_key && c.pk_desc in resolve_index_columns columns ~table ~origin ~on_conflict [ { column = c.name; collation = None; desc } ]) keyed
(* One entry per UNIQUE/PRIMARY KEY constraint the table declares, in the order sqlite3 numbers the autoindexes: the order the CREATE TABLE text declares them, every column-level constraint before every table-level one. A constraint that does not resolve keeps its place as an [Error], since dropping it would renumber the sqlite_autoindex_* names after it. *)let unique_constraints ~without_rowid (schema : schema) = match schema.parsed with | None -> [] | Some ct -> let columns = schema.columns in let table = ct.tbl_name in let column_level = List.filter_map (column_unique_constraint ~without_rowid columns ~table) ct.columns in let table_level = List.filter_map (table_unique_constraint columns ~table) ct.table_constraints in column_level @ table_level
(* The autoindexes of a table this engine stores with a rowid, which is every table it writes. *)let parse_unique_constraints schema = unique_constraints ~without_rowid:false schema
(* The CHECK constraints of a table: each is the verbatim predicate source (for the error message) paired with the expression it is ({!check_exprs}). *)let table_checks (schema : schema) = Lazy.force schema.checks
(* The DEFAULT expressions of a table, keyed by column: the source form, which a statement that has to evaluate one reads. A column with no DEFAULT is absent rather than [Null], one defaults to nothing and the other to the null value. Its evaluated counterpart is {!evaluated_defaults}. *)let table_defaults (schema : schema) = match schema.parsed with None -> [] | Some ct -> ct.Sql.Ast.defaults
(* Find the index of the rowid alias column, if any *)let rowid_alias_index columns = let rec find i = function | [] -> None | c :: _ when c.is_rowid_alias -> Some i | _ :: rest -> find (i + 1) rest in find 0 columns
(* A name written where a column name goes -- an INSERT's column list, an UPDATE's SET -- that reaches the rowid rather than a column: one of the rowid spellings, no column declaring it, on a table keyed by a rowid. The INTEGER PRIMARY KEY is not this: it answers by its own name. *)let written_rowid columns ~has_rowid name = has_rowid && Rowid.is_keyword name && not (List.exists (fun (c : column) -> Catalog.Ident.equal c.name name) columns)