Skip to main content

inillucent_sql/
catalog_view.rs

1//! What the binder is allowed to know about a schema.
2//!
3//! Invariant: this is a read-only view over an immutable snapshot. Nothing here
4//! can open a page, and nothing here changes while a statement is being bound,
5//! so a bound statement is a pure function of its SQL and one generation of one
6//! catalog. That is what makes prepared-statement invalidation a comparison of
7//! two numbers rather than a re-derivation.
8//!
9//! The types are defined here, below the catalog that fills them in, so the
10//! binder can be compiled and tested against a hand-built schema with no file
11//! anywhere near it.
12
13use crate::ast::{ConflictAction, ReferentialAction};
14use inillucent_value::Affinity;
15
16/// Where an index came from, which decides whether it can be dropped and how
17/// it is named in `sqlite_schema`.
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum IndexOrigin {
20    /// `CREATE INDEX`.
21    Created,
22    /// A `UNIQUE` constraint.
23    Unique,
24    /// A `PRIMARY KEY` constraint on a rowid table.
25    PrimaryKey,
26    /// An index a module owns, named by `CREATE INDEX ... USING <module>`.
27    ///
28    /// **Not a b-tree, and the planner has to know that.** Its rows live in a
29    /// virtual table, its `root` is that table's own root, and none of the
30    /// b-tree paths apply to it - there is nothing to seek and nothing to
31    /// range-scan. What it can do is answer "the k nearest to this vector",
32    /// which is a whole access path of its own.
33    Module,
34}
35
36/// The distance a `Module`-origin vector index was declared to minimise.
37///
38/// **Only a vector index has one of these, and only a real one.** An ordinary
39/// b-tree orders by a collation, not a distance, so every `IndexInfo` that is
40/// not `IndexOrigin::Module` carries `None`. A `Module` index carries `None`
41/// too unless its own module is one the planner has verified actually honours
42/// the setting: `inillucent-engine/src/vectors.rs` only ever reports `Some`
43/// for `inillucent_search` (which backs `USING inillucent_hnsw`), because that
44/// is the one module whose store was changed to read the graph under this
45/// metric. An `ivfflat` index that was declared `WITH (metric = 'l2')` still
46/// reads back as `None` here, deliberately: `ivfflat`'s own argument parser
47/// silently accepts and ignores a key it does not recognise, so trusting the
48/// text would let the planner believe an index orders by Euclidean distance
49/// when the structure behind it still computes cosine - the exact "wrong
50/// answer that looks like a working index" this field exists to prevent.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum IndexMetric {
53    /// One minus the cosine similarity of two unit vectors.
54    Cosine,
55    /// Euclidean distance.
56    L2,
57}
58
59/// One column of a table or view.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct ColumnInfo {
62    /// The name as declared.
63    pub name: Vec<u8>,
64    /// The ASCII-folded lookup key.
65    pub folded: Vec<u8>,
66    /// The declared type, exactly as written, empty when none was given.
67    pub declared_type: Vec<u8>,
68    /// The affinity derived from the declared type.
69    pub affinity: Affinity,
70    /// The folded name of the column's declared collation.
71    pub collation: Vec<u8>,
72    /// Whether the column is `NOT NULL`.
73    pub not_null: bool,
74    /// The `ON CONFLICT` clause written on the `NOT NULL`, when there was one.
75    ///
76    /// A constraint carries its own algorithm and the statement may override
77    /// it: `INSERT OR IGNORE` beats `NOT NULL ON CONFLICT ABORT`. Recording it
78    /// per constraint rather than per table is what makes that override a
79    /// choice between two known values instead of a guess.
80    pub not_null_conflict: Option<ConflictAction>,
81    /// The `ON CONFLICT` clause written on the column's `PRIMARY KEY`.
82    ///
83    /// **A different constraint from the `NOT NULL`, and a different clause.**
84    /// For a rowid alias this is the only place a rowid collision's algorithm
85    /// is written down - SQLite records `id INTEGER PRIMARY KEY ON CONFLICT
86    /// REPLACE` against the column, because the alias *is* the column and there
87    /// is no index to hang it on. Every other primary key gets an `IndexInfo`
88    /// and carries it there.
89    ///
90    /// Reading `not_null_conflict` for it, which is what the write path used to
91    /// do, answers a question about a constraint the table may not
92    /// even declare.
93    pub primary_key_conflict: Option<ConflictAction>,
94    /// The `DEFAULT` expression, as written.
95    pub default_sql: Option<Vec<u8>>,
96    /// The one-based position in the primary key, when it is in one.
97    pub primary_key_position: Option<u16>,
98    /// Whether the column is hidden from `SELECT *`.
99    pub hidden: bool,
100    /// Whether the column is generated.
101    pub generated: bool,
102    /// Whether a generated column's value is stored in the record.
103    ///
104    /// A `VIRTUAL` column occupies no slot and is computed on every read; a
105    /// `STORED` one occupies a slot like any other column. The distinction is
106    /// not cosmetic: it changes which *record position* every column after it
107    /// lives at, so a reader that ignored it would read the wrong column.
108    pub stored: bool,
109    /// The generating expression, as the source text it was written as.
110    pub generated_sql: Option<Vec<u8>>,
111}
112
113/// One key column of an index.
114#[derive(Clone, Debug, PartialEq, Eq)]
115pub struct IndexColumnInfo {
116    /// The table column this key indexes, when it indexes a bare column.
117    pub column: Option<u16>,
118    /// The key expression, as written, when the key is an expression.
119    pub expr_sql: Option<Vec<u8>>,
120    /// The folded collation name the key is ordered by.
121    pub collation: Vec<u8>,
122    /// Whether the key is stored descending.
123    pub descending: bool,
124    /// Whether the *declaration* said descending, whatever the storage does.
125    ///
126    /// **A different question from `descending`, and the two used to be one.**
127    /// This engine's trees are always built ascending, so the catalog flattens
128    /// `descending` to false for the planner's sake - a planner told about a
129    /// descending tree that does not exist draws three inverted conclusions
130    /// (see `inillucent-catalog`'s `stored_ascending`). But
131    /// `PRAGMA index_xinfo` reports what was *declared*, and an application
132    /// reading it to reconstruct a `CREATE INDEX` needs the `DESC` back.
133    pub declared_descending: bool,
134}
135
136impl IndexColumnInfo {
137    /// Returns the table column the key holds as it is stored, when it holds one.
138    ///
139    /// **Not the same as `column`.** A key on a `VIRTUAL` generated column
140    /// names that column, so `PRAGMA index_info`, a unique violation's message
141    /// and `DROP COLUMN` all see it, and it also carries the column's
142    /// expression in `expr_sql`, because the column is in no record and every
143    /// entry has to be computed. The binder replaces a reference to such a
144    /// column with its expression, so a planner that matched the key by column
145    /// would never find a term to seek on. The planner reads this instead and
146    /// matches a computed key by its expression.
147    pub fn plain_column(&self) -> Option<u16> {
148        self.column.filter(|_| self.expr_sql.is_none())
149    }
150
151    /// Returns the text a computed key is evaluated from, when the key is one.
152    ///
153    /// An expression key is its own text. A key on a `VIRTUAL` generated column
154    /// is the column's name instead of the column's expression: reading the
155    /// column through the binder converts the value with the column's
156    /// affinity, which is the value SQLite puts in the index, and the bare
157    /// expression is not that value (`k INT AS (a)` over `'1'` is the integer 1,
158    /// and the expression alone gives the text).
159    ///
160    /// @param table - the table the index is on
161    pub fn computed_text(&self, table: &TableInfo) -> Option<Vec<u8>> {
162        let sql = self.expr_sql.as_ref()?;
163        let Some(info) = self.column.and_then(|declared| table.column(declared)) else {
164            return Some(sql.clone());
165        };
166        Some(quoted_name(&info.name))
167    }
168}
169
170/// Returns an identifier in double quotes, with any quote inside it doubled.
171///
172/// For the places that build the text of an expression from a column name and
173/// bind it, so a name that is not a plain word still reads as one identifier.
174///
175/// @param name - the identifier as declared
176pub fn quoted_name(name: &[u8]) -> Vec<u8> {
177    let mut quoted = Vec::with_capacity(name.len().saturating_add(2));
178    quoted.push(b'"');
179    for byte in name {
180        if *byte == b'"' {
181            quoted.push(b'"');
182        }
183        quoted.push(*byte);
184    }
185    quoted.push(b'"');
186    quoted
187}
188
189/// An index over a table.
190#[derive(Clone, Debug, PartialEq, Eq)]
191pub struct IndexInfo {
192    /// The index name.
193    pub name: Vec<u8>,
194    /// The ASCII-folded lookup key.
195    pub folded: Vec<u8>,
196    /// The root page of the index B-tree.
197    pub root: u32,
198    /// Whether the index enforces uniqueness.
199    pub unique: bool,
200    /// The key columns, in order.
201    pub columns: Vec<IndexColumnInfo>,
202    /// The partial-index predicate, as written.
203    pub partial_sql: Option<Vec<u8>>,
204    /// Where the index came from.
205    pub origin: IndexOrigin,
206    /// The `ON CONFLICT` clause the constraint that created it carried.
207    pub conflict: Option<ConflictAction>,
208    /// For each leading prefix of the key, the average number of rows sharing
209    /// it, as `ANALYZE` measured.
210    ///
211    /// Empty until the schema has been analysed, which is the *usual* state and
212    /// not an error: the planner falls back to SQLite's own guesses, and those
213    /// guesses are what make an unanalysed plan match the reference's.
214    pub prefix_rows: Vec<i64>,
215    /// How many entries the index itself holds, as `ANALYZE` measured.
216    ///
217    /// **The same number as the table's row count for an ordinary index, and a
218    /// different one for a partial index**, which holds only
219    /// the rows its predicate accepted. It is what lets the planner price
220    /// reading the whole of such an index against scanning the table it is on -
221    /// 120 entries against 6,000 rows, in the case this was found on.
222    ///
223    /// `None` until the schema has been analysed.
224    pub analysed_rows: Option<i64>,
225    /// The distance a vector index minimises, when it is one the planner may
226    /// trust to answer for it. See [`IndexMetric`].
227    pub metric: Option<IndexMetric>,
228}
229
230/// What kind of schema object a name resolves to.
231#[derive(Clone, Copy, Debug, PartialEq, Eq)]
232pub enum TableKind {
233    /// An ordinary table.
234    Table,
235    /// A view.
236    View,
237    /// A virtual table.
238    Virtual,
239    /// A nested query standing in for a table: a FROM subquery, a CTE
240    /// reference, or an expanded view.
241    ///
242    /// It is a kind rather than a flag because every question the binder asks
243    /// of a table - has it a rowid, can it be written to, may an index be used
244    /// on it - has the same answer for all three, and a kind makes the answer
245    /// one match arm instead of three conditions that can drift apart.
246    Subquery,
247}
248
249/// A view's parsed definition.
250///
251/// The arena lives here, in the catalog snapshot, rather than being re-parsed
252/// on every reference. That is not only a saving: the binder holds the snapshot
253/// for the whole statement, so a body kept here outlives the bind and can be
254/// bound in place, while one parsed inside the binder would be a local whose
255/// borrow ends before the bound tree does.
256#[derive(Clone, Debug, PartialEq, Eq)]
257pub struct ViewBody {
258    /// The arena the view's `SELECT` was parsed into.
259    pub ast: crate::ast::Ast,
260    /// The `SELECT` inside the arena.
261    pub select: crate::ast::SelectId,
262    /// The explicit column list, when the `CREATE VIEW` wrote one.
263    pub columns: Vec<Vec<u8>>,
264}
265
266/// What a trigger fires on, with `UPDATE OF` already folded.
267#[derive(Clone, Debug, PartialEq, Eq)]
268pub enum TriggerEventInfo {
269    /// `INSERT`.
270    Insert,
271    /// `DELETE`.
272    Delete,
273    /// `UPDATE`, optionally narrowed to a set of folded column names.
274    Update(Vec<Vec<u8>>),
275}
276
277/// A trigger's parsed definition.
278///
279/// Kept parsed here for the same reason a view body is: the arena belongs to
280/// the catalog snapshot, which the binder holds for the whole statement, so a
281/// body can be bound in place. A body re-parsed inside the binder would be a
282/// local whose borrow ends before the bound tree does.
283#[derive(Clone, Debug, PartialEq, Eq)]
284pub struct TriggerInfo {
285    /// The trigger name as declared.
286    pub name: Vec<u8>,
287    /// The ASCII-folded lookup key.
288    pub folded: Vec<u8>,
289    /// When it fires. `CREATE TRIGGER` with no time written means `BEFORE`.
290    pub time: crate::ast::TriggerTime,
291    /// What it fires on.
292    pub event: TriggerEventInfo,
293    /// The arena the `WHEN` guard and the body were parsed into.
294    pub ast: crate::ast::Ast,
295    /// The `WHEN` guard, when one was written.
296    pub when: Option<crate::ast::ExprId>,
297    /// The body statements, in written order.
298    pub body: Vec<crate::ast::Statement>,
299    /// The folded database the `ON` clause named, as in `ON main.t`, when it
300    /// named one.
301    pub table_database: Option<Vec<u8>>,
302}
303
304impl ColumnInfo {
305    /// Reports whether the column was declared a vector at all.
306    ///
307    /// `VECTOR(768)` and a bare `VECTOR` both answer true, where
308    /// [`ColumnInfo::vector_dimensions`] answers a width only for the first.
309    /// The difference matters to the operators: a bare `VECTOR` cannot be
310    /// indexed, but adding two of them is just as meaningless.
311    pub fn is_vector(&self) -> bool {
312        let declared = self.declared_type.to_ascii_lowercase();
313        let Some(rest) = declared.strip_prefix(b"vector".as_slice()) else {
314            return false;
315        };
316        rest.is_empty()
317            || rest
318                .first()
319                .is_some_and(|byte| !byte.is_ascii_alphanumeric())
320    }
321
322    /// Returns how many dimensions a `VECTOR(N)` column declares.
323    ///
324    /// **Read out of the declared type rather than stored beside it**, because
325    /// every path that builds a `ColumnInfo` - the catalog loader, a module's
326    /// declaration, the binder's synthetic ones - would otherwise have to know
327    /// about vectors, and a column's declared type is the one place SQLite
328    /// itself keeps what a column was called.
329    ///
330    /// `VECTOR(768)` and `vector( 768 )` both answer 768. A bare `VECTOR`
331    /// answers `None`, which means "a vector of whatever arrives" and is what a
332    /// table holding two models' embeddings needs; anything that is not a
333    /// vector answers `None` too, and its caller then checks nothing.
334    ///
335    /// **The affinity is deliberately left alone.** SQLite gives `VECTOR(768)`
336    /// NUMERIC affinity, and NUMERIC leaves a blob exactly as it arrived - so
337    /// the bytes round-trip without this engine having to disagree with the
338    /// reference about what an affinity is. What the declaration buys is the
339    /// width check on write, and a column an index can be built over.
340    pub fn vector_dimensions(&self) -> Option<usize> {
341        // **The common answer before any allocation (task-2175).** The write
342        // path asks this of every column of every row it writes, and lowering
343        // the whole declared type first was an allocation per column per row:
344        // 4% of an `UPDATE` of 100,000 rows, for columns declared `TEXT`.
345        let head = self.declared_type.get(..6)?;
346        if !head.eq_ignore_ascii_case(b"vector") {
347            return None;
348        }
349        let declared = self.declared_type.to_ascii_lowercase();
350        let rest = declared.strip_prefix(b"vector".as_slice())?;
351        let inside: Vec<u8> = rest
352            .iter()
353            .copied()
354            .skip_while(|byte| byte.is_ascii_whitespace())
355            .collect();
356        let inside = inside.strip_prefix(b"(".as_slice())?;
357        let inside = inside.strip_suffix(b")".as_slice())?;
358        let text = std::str::from_utf8(inside).ok()?.trim();
359        let width: usize = text.parse().ok()?;
360        (width > 0).then_some(width)
361    }
362}
363
364impl TriggerInfo {
365    /// Returns whether this trigger fires for one event on one column set.
366    ///
367    /// `changed` is the folded names an UPDATE assigns, and is empty for the
368    /// other two events. `UPDATE OF a, b` fires only when the statement writes
369    /// `a` or `b` - which SQLite decides from the *statement*, not from whether
370    /// the value actually differs.
371    pub fn fires_for(&self, event: &TriggerEventInfo, changed: &[Vec<u8>]) -> bool {
372        match (&self.event, event) {
373            (TriggerEventInfo::Insert, TriggerEventInfo::Insert) => true,
374            (TriggerEventInfo::Delete, TriggerEventInfo::Delete) => true,
375            (TriggerEventInfo::Update(of), TriggerEventInfo::Update(_)) => {
376                of.is_empty() || of.iter().any(|name| changed.contains(name))
377            }
378            _ => false,
379        }
380    }
381}
382
383/// A table, view or virtual table.
384#[derive(Clone, Debug, PartialEq, Eq)]
385pub struct TableInfo {
386    /// The name as declared.
387    pub name: Vec<u8>,
388    /// The ASCII-folded lookup key.
389    pub folded: Vec<u8>,
390    /// Which attached database it belongs to.
391    pub database: usize,
392    /// The root page of the table B-tree, or zero for a view.
393    pub root: u32,
394    /// The columns, in declaration order.
395    pub columns: Vec<ColumnInfo>,
396    /// The column that is an alias for the rowid, when there is one.
397    pub rowid_alias: Option<u16>,
398    /// Whether the table is `WITHOUT ROWID`.
399    pub without_rowid: bool,
400    /// Whether the table is `STRICT`.
401    pub strict: bool,
402    /// Whether the rowid alias was declared `AUTOINCREMENT`.
403    ///
404    /// It changes where a new rowid comes from: an ordinary table reuses the
405    /// numbers its deleted rows had, and an `AUTOINCREMENT` one never does,
406    /// because it remembers the largest it has ever handed out in
407    /// `sqlite_sequence`.
408    pub autoincrement: bool,
409    /// What kind of object this is.
410    pub kind: TableKind,
411    /// The `CREATE` text as stored in `sqlite_schema`.
412    pub create_sql: Vec<u8>,
413    /// The indexes over this table.
414    pub indexes: Vec<IndexInfo>,
415    /// The parsed body, when this is a view.
416    pub view: Option<Box<ViewBody>>,
417    /// The triggers attached to this table or view, in schema order.
418    pub triggers: Vec<TriggerInfo>,
419    /// How many rows `ANALYZE` counted, when it has run.
420    pub analysed_rows: Option<i64>,
421    /// The triggers this table's writes fire because of a foreign key.
422    ///
423    /// Both directions are here, because both are things that happen when
424    /// *this* table is written: the checks its own keys need when a row
425    /// arrives, and the actions the keys pointing at it need when a row
426    /// leaves. They are built once when the schema is read rather than once
427    /// per statement, because generating and parsing them is the same work
428    /// every time and the schema is what decides them.
429    pub foreign_key_triggers: Vec<ForeignKeyTrigger>,
430    /// Every foreign key declared on this table, in declaration order.
431    ///
432    /// The child's side of the relationship, which is the side the table
433    /// carries. Finding the keys that point *at* a table means walking the
434    /// database's tables and asking each one, which is what
435    /// `CatalogView::foreign_keys_referencing` does - and is what SQLite does
436    /// too, because nothing in the file records the reverse direction.
437    pub foreign_keys: Vec<ForeignKeyInfo>,
438    /// Every `CHECK` constraint, as the source text it was written as.
439    ///
440    /// The text rather than a bound expression, for the same reason
441    /// `default_sql` is text: the catalog is below the binder, so it cannot
442    /// bind anything, and a constraint that had been half-interpreted on the
443    /// way through would be a second source of truth beside the `CREATE`
444    /// statement the file actually stores.
445    pub checks: Vec<CheckInfo>,
446    /// The module a virtual table is implemented by, and its arguments.
447    ///
448    /// The catalog records the question and the session fills in the answer:
449    /// what columns the table has is the module's to say, not the file's, so a
450    /// virtual table arrives here with a module and no columns and leaves the
451    /// connection's schema load with both.
452    pub module: Option<crate::vtab::ModuleRef>,
453}
454
455/// One trigger a foreign key implies, or the reason there is not one.
456#[derive(Clone, Debug, PartialEq, Eq)]
457pub struct ForeignKeyTrigger {
458    /// Whether it refuses a write rather than repairing one.
459    ///
460    /// Only a check can be deferred. An action is what the constraint *does*,
461    /// and doing it at commit time instead would leave the rows in between
462    /// visible to the statements that come after.
463    pub is_check: bool,
464    /// Whether the key it enforces was declared `INITIALLY DEFERRED`.
465    pub deferred: bool,
466    /// The trigger, or `None` when the key cannot be enforced at all.
467    pub trigger: Option<TriggerInfo>,
468    /// Why it cannot be, when it cannot.
469    ///
470    /// A key whose parent table is missing, or whose parent columns are not a
471    /// key of the parent, is legal to declare: SQLite reports it when
472    /// something writes, not when the schema is read, so that a schema can be
473    /// loaded in any order. The message is kept here and reported then.
474    pub fault: Vec<u8>,
475    /// Whether the key's child table and its parent table are the same table.
476    ///
477    /// **Read by `DROP TABLE`'s implicit delete (task-1979, F6).** That delete
478    /// removes every row of one table, so a key whose child is that same table
479    /// cannot be violated once the statement has finished - the rows that would
480    /// be left pointing at nothing are themselves gone. SQLite reaches the same
481    /// answer a different way: its immediate foreign keys are a counter checked
482    /// at the end of the statement, so the violation deleting the first row
483    /// creates is cancelled by deleting the row that made it.
484    pub self_referencing: bool,
485}
486
487/// One foreign key, from the child table that declares it.
488#[derive(Clone, Debug, PartialEq, Eq)]
489pub struct ForeignKeyInfo {
490    /// The constraint's position in its table, counting from zero.
491    ///
492    /// `PRAGMA foreign_key_list` reports it, and it is how a diagnostic names
493    /// a constraint that was written without a name - which is most of them.
494    pub id: u32,
495    /// The child columns, in the order they were written.
496    pub columns: Vec<u16>,
497    /// The parent table's name as written.
498    pub parent: Vec<u8>,
499    /// The parent table's folded name.
500    pub parent_folded: Vec<u8>,
501    /// The parent columns as written, or empty when the clause named none.
502    ///
503    /// Empty means the parent's primary key, and it stays empty rather than
504    /// being resolved here: the catalog builds one table at a time and the
505    /// parent may not have been read yet - or may not exist, which is legal
506    /// until something writes a row.
507    pub parent_columns: Vec<Vec<u8>>,
508    /// What happens to the child rows when a parent row is deleted.
509    pub on_delete: ReferentialAction,
510    /// What happens to the child rows when a parent key changes.
511    pub on_update: ReferentialAction,
512    /// The `MATCH` clause as written, which SQLite parses and ignores.
513    pub match_clause: Vec<u8>,
514    /// Whether `DEFERRABLE` was written.
515    pub deferrable: bool,
516    /// Whether `INITIALLY DEFERRED` was written.
517    pub initially_deferred: bool,
518    /// Whether following this key can lead back to the table that declares it.
519    ///
520    /// A tree with `ON DELETE CASCADE` on its parent column is the everyday
521    /// case, and it is the one case an action cannot simply be inlined into
522    /// the statement that fires it: the body would have to appear once per
523    /// level the data happens to be deep, which is not known when the
524    /// statement is compiled. A cyclic key's action is applied by repeating it
525    /// until nothing changes instead, and this is what says which keys need
526    /// that.
527    pub cyclic: bool,
528}
529
530impl ForeignKeyInfo {
531    /// Reports whether the constraint's checks wait until the transaction
532    /// commits.
533    pub fn is_deferred(&self) -> bool {
534        self.deferrable && self.initially_deferred
535    }
536}
537
538/// One `CHECK` constraint.
539#[derive(Clone, Debug, PartialEq, Eq)]
540pub struct CheckInfo {
541    /// The constraint's name, when one was written.
542    pub name: Option<Vec<u8>>,
543    /// The predicate, as the source text between its parentheses.
544    pub expr_sql: Vec<u8>,
545    /// The `ON CONFLICT` clause a table-level `CHECK` was written with.
546    ///
547    /// **Recorded and not acted on**, because that is what the reference does:
548    /// SQLite's grammar accepts `CHECK (expr) onconf` on a table constraint and
549    /// its builder never reads the clause, so such a constraint aborts like any
550    /// other. It is kept here so the derivation is a full account of the text
551    /// rather than a lossy one, and so the next reader finds the measurement
552    /// instead of the question.
553    pub conflict: Option<ConflictAction>,
554}
555
556impl TableInfo {
557    /// Returns the position of a column by its folded name.
558    pub fn column_position(&self, folded: &[u8]) -> Option<u16> {
559        self.columns
560            .iter()
561            .position(|column| column.folded == folded)
562            .map(|index| index as u16)
563    }
564
565    /// Returns a column by position.
566    pub fn column(&self, position: u16) -> Option<&ColumnInfo> {
567        self.columns.get(position as usize)
568    }
569
570    /// Returns whether the table has a rowid a query may refer to.
571    pub fn has_rowid(&self) -> bool {
572        // A virtual table has one unless its module declared otherwise: FTS5
573        // and the R-Tree both key their rows by it, and `SELECT rowid FROM t`
574        // is how an application joins to them.
575        matches!(self.kind, TableKind::Table | TableKind::Virtual) && !self.without_rowid
576    }
577
578    /// Returns a table that stands for an eponymous module.
579    ///
580    /// A module reached as a name rather than through `CREATE VIRTUAL TABLE` -
581    /// `generate_series`, `json_each`, `pragma_table_info` - belongs to no
582    /// database and has no `sqlite_schema` row, so everything a stored table
583    /// carries is absent and only the module's declaration remains.
584    ///
585    /// @param name - the module's name, which is also the table's
586    /// @param columns - the columns the module declared
587    /// @param module - the module reference the executor resolves it by
588    /// @param without_rowid - whether the module declared no rowid
589    pub fn eponymous(
590        name: Vec<u8>,
591        columns: Vec<ColumnInfo>,
592        module: crate::vtab::ModuleRef,
593        without_rowid: bool,
594    ) -> TableInfo {
595        let folded = name.to_ascii_lowercase();
596        TableInfo {
597            name,
598            folded,
599            database: 0,
600            root: 0,
601            columns,
602            rowid_alias: None,
603            without_rowid,
604            strict: false,
605            autoincrement: false,
606            kind: TableKind::Virtual,
607            create_sql: Vec::new(),
608            foreign_keys: Vec::new(),
609            foreign_key_triggers: Vec::new(),
610            module: Some(module),
611            view: None,
612            triggers: Vec::new(),
613            analysed_rows: None,
614            indexes: Vec::new(),
615            checks: Vec::new(),
616        }
617    }
618
619    /// Returns a table that stands for a nested query's result.
620    ///
621    /// The column list is the block's result columns: their names are what a
622    /// reference to the subquery resolves against, and their affinity and
623    /// collation are the ones the expressions behind them carry, so a
624    /// comparison against a subquery column applies the same rules it would
625    /// have applied one level down.
626    pub fn subquery(name: Vec<u8>, database: usize, columns: Vec<ColumnInfo>) -> TableInfo {
627        let folded = name.to_ascii_lowercase();
628        TableInfo {
629            name,
630            folded,
631            database,
632            root: 0,
633            columns,
634            rowid_alias: None,
635            without_rowid: true,
636            strict: false,
637            autoincrement: false,
638            kind: TableKind::Subquery,
639            create_sql: Vec::new(),
640            foreign_keys: Vec::new(),
641            foreign_key_triggers: Vec::new(),
642            module: None,
643            view: None,
644            triggers: Vec::new(),
645            analysed_rows: None,
646            indexes: Vec::new(),
647            checks: Vec::new(),
648        }
649    }
650
651    /// Returns the record slot a column's value lives in, when it has one.
652    ///
653    /// `VIRTUAL` generated columns take no slot, so the slots of the columns
654    /// after them shift down. Every read of a stored column has to go through
655    /// this rather than through the column's declared position, and a `VIRTUAL`
656    /// column has no slot at all - it is computed.
657    pub fn record_slot(&self, column: u16) -> Option<usize> {
658        if self.without_rowid {
659            return self
660                .record_order()
661                .iter()
662                .position(|stored| *stored == column);
663        }
664        let mut slot = 0usize;
665        for (position, info) in self.columns.iter().enumerate() {
666            if info.generated && !info.stored {
667                if position == usize::from(column) {
668                    return None;
669                }
670                continue;
671            }
672            if position == usize::from(column) {
673                return Some(slot);
674            }
675            slot = slot.saturating_add(1);
676        }
677        None
678    }
679
680    /// Returns the primary key's columns, in key order.
681    ///
682    /// Key order, not declaration order: `PRIMARY KEY(b, a)` is ordered by `b`
683    /// and then `a` however the columns were declared, and for a `WITHOUT
684    /// ROWID` table that order also decides where in the record they sit.
685    pub fn primary_key(&self) -> Vec<u16> {
686        let mut keys: Vec<(u16, u16)> = self
687            .columns
688            .iter()
689            .enumerate()
690            .filter_map(|(position, column)| {
691                column
692                    .primary_key_position
693                    .map(|key| (key, position as u16))
694            })
695            .collect();
696        keys.sort_by_key(|(key, _)| *key);
697        keys.into_iter().map(|(_, position)| position).collect()
698    }
699
700    /// Returns the columns a record holds, in the order it holds them.
701    ///
702    /// A rowid table stores its columns as declared. A `WITHOUT ROWID` table's
703    /// B-tree is an index whose key is the primary key, so its record is the
704    /// key columns first, in key order, and then everything else as declared -
705    /// verified against a file the pinned build wrote: `PRIMARY KEY(b, a)` over
706    /// `(a, b, c)` stores `(b, a, c)`.
707    pub fn record_order(&self) -> Vec<u16> {
708        let stored = |position: usize| {
709            self.columns
710                .get(position)
711                .is_some_and(|column| !column.generated || column.stored)
712        };
713        if !self.without_rowid {
714            return (0..self.columns.len())
715                .filter(|position| stored(*position))
716                .map(|position| position as u16)
717                .collect();
718        }
719        let keys = self.primary_key();
720        let mut order = keys.clone();
721        for position in 0..self.columns.len() {
722            if keys.contains(&(position as u16)) || !stored(position) {
723                continue;
724            }
725            order.push(position as u16);
726        }
727        order
728    }
729
730    /// Returns whether a name is one of the rowid's three spellings and is not
731    /// shadowed by a real column.
732    ///
733    /// SQLite's rule is exactly this: `rowid`, `_rowid_` and `oid` name the
734    /// rowid *unless* the table declares a column with that name, in which case
735    /// the column wins. A table without a rowid has none of the three.
736    pub fn is_rowid_name(&self, folded: &[u8]) -> bool {
737        if !self.has_rowid() {
738            return false;
739        }
740        let spelled = folded == b"rowid" || folded == b"_rowid_" || folded == b"oid";
741        spelled && self.column_position(folded).is_none()
742    }
743}
744
745/// The read-only schema the binder resolves names against.
746pub trait CatalogView {
747    /// Returns the number of attached databases.
748    fn database_count(&self) -> usize;
749
750    /// Returns the name of an attached database by index.
751    fn database_name(&self, index: usize) -> &[u8];
752
753    /// Returns the index of an attached database by folded name.
754    fn database_index(&self, folded: &[u8]) -> Option<usize>;
755
756    /// Returns a table, view or virtual table by name.
757    ///
758    /// With no qualifier the search follows SQLite's order: `temp`, then
759    /// `main`, then every other attached database in attachment order.
760    fn find_table(&self, database: Option<&[u8]>, folded: &[u8]) -> Option<&TableInfo>;
761
762    /// Returns a table as a shared pointer, for a caller that has to keep it.
763    ///
764    /// A binder keeps what it finds for the life of the bound statement.
765    /// [`CatalogView::find_table`] hands back a borrow, so keeping it meant
766    /// cloning a `TableInfo` - two name vectors, a `ColumnInfo` per column with
767    /// its own heap fields, the `CREATE` text and an `IndexInfo` per index -
768    /// for every table reference in every statement. Measured at 2,938 ns of
769    /// `prepare.point`'s 6,093 ns compile.
770    ///
771    /// The default is that clone, so an implementor that has nothing to share
772    /// keeps working and is merely no faster. `StaticCatalog` shares.
773    ///
774    /// @param database - the schema qualifier, if the statement wrote one
775    /// @param folded - the table's folded name
776    fn shared_table(
777        &self,
778        database: Option<&[u8]>,
779        folded: &[u8],
780    ) -> Option<std::rc::Rc<TableInfo>> {
781        self.find_table(database, folded)
782            .map(|table| std::rc::Rc::new(table.clone()))
783    }
784
785    /// Returns the table an index belongs to, together with the index.
786    ///
787    /// Index names live in the same namespace as table names in SQLite, but
788    /// the catalog stores an index inside the table it indexes - which is
789    /// where every reader of one wants it. `DROP INDEX` is the caller that
790    /// has only the name, so the search lives here rather than being written
791    /// out again wherever a name has to be resolved.
792    fn find_index(
793        &self,
794        database: Option<&[u8]>,
795        folded: &[u8],
796    ) -> Option<(&TableInfo, &IndexInfo)>;
797
798    /// Returns the table a trigger is attached to, together with the trigger.
799    ///
800    /// Triggers share the name namespace with tables and indexes and are stored
801    /// on the object they fire for, so this is `find_index` again for the other
802    /// kind of attached object: `DROP TRIGGER` and `CREATE TRIGGER` both have
803    /// only the name.
804    fn find_trigger(
805        &self,
806        database: Option<&[u8]>,
807        folded: &[u8],
808    ) -> Option<(&TableInfo, &TriggerInfo)> {
809        let wanted = database.and_then(|name| self.database_index(name));
810        for table in self.every_table() {
811            if wanted.is_some_and(|index| index != table.database) {
812                continue;
813            }
814            if let Some(trigger) = table.triggers.iter().find(|one| one.folded == folded) {
815                return Some((table, trigger));
816            }
817        }
818        None
819    }
820
821    /// Returns every table of every attached database.
822    ///
823    /// It exists so [`CatalogView::find_trigger`] can have one implementation
824    /// rather than one per catalog: a trigger search is the same walk whatever
825    /// the tables are stored in.
826    fn every_table(&self) -> Vec<&TableInfo>;
827
828    /// Returns every table of one attached database, in no particular order.
829    fn tables_of(&self, database: usize) -> Vec<&TableInfo>;
830
831    /// Returns the schema cookie of an attached database, which a prepared
832    /// statement records so it can tell whether the schema moved under it.
833    fn schema_cookie(&self, database: usize) -> u32;
834
835    /// Returns the generation of the whole snapshot.
836    fn generation(&self) -> u64;
837}
838
839/// A catalog held in memory, which is what a test binds against and what the
840/// loader produces once it has read `sqlite_schema`.
841#[derive(Clone, Debug, Default, PartialEq, Eq)]
842pub struct StaticCatalog {
843    /// The attached databases, in attachment order, with their cookies.
844    pub databases: Vec<(Vec<u8>, u32)>,
845    /// Every table, in no particular order.
846    ///
847    /// **Shared rather than owned, because binding a statement used to clone
848    /// one.** `BoundSource.table` was a `TableInfo` by value, so every table
849    /// reference in every statement deep-copied the catalog's entry: two name
850    /// vectors, a `ColumnInfo` per column each with its own heap fields, the
851    /// full `CREATE` text, and an `IndexInfo` per index with its own column
852    /// vector. Forty-odd allocations to bind one `WHERE id = ?1`, measured at
853    /// 2,938 ns of `prepare.point`'s 6,093 - 48% of the statement's whole
854    /// compile. An `Rc` makes it a refcount bump.
855    pub tables: Vec<std::rc::Rc<TableInfo>>,
856    /// The eponymous virtual tables the connection's modules provide.
857    ///
858    /// `generate_series`, `json_each`, `json_tree`, `pragma_table_info`: the
859    /// name *is* the table, so they belong to no database and have no
860    /// `sqlite_schema` row. They are searched **last**, so a real table called
861    /// `generate_series` shadows the module rather than the other way round -
862    /// which is SQLite's order and the only safe one, because the file was
863    /// there first.
864    ///
865    /// Filled by the engine from its module registry on every catalog refresh.
866    /// Nothing used to fill it, and the eponymous form did not exist:
867    /// `FROM generate_series(1,10)` was `no such table`, which also left
868    /// `json_each` unreachable from SQL by any route, because `JsonWalkModule`
869    /// refuses `CREATE VIRTUAL TABLE` outright.
870    pub eponymous: Vec<std::rc::Rc<TableInfo>>,
871    /// The generation of this snapshot.
872    pub generation: u64,
873}
874
875impl StaticCatalog {
876    /// Returns a catalog with one `main` database and no objects.
877    pub fn empty() -> StaticCatalog {
878        StaticCatalog {
879            databases: vec![(b"main".to_vec(), 0)],
880            tables: Vec::new(),
881            eponymous: Vec::new(),
882            generation: 0,
883        }
884    }
885
886    /// Adds an eponymous virtual table, returning the catalog.
887    ///
888    /// @param table - the module's table, as its declaration describes it
889    pub fn with_eponymous(mut self, table: TableInfo) -> StaticCatalog {
890        self.eponymous.push(std::rc::Rc::new(table));
891        self
892    }
893
894    /// Adds an eponymous virtual table its caller already shares, returning the
895    /// catalog.
896    ///
897    /// The engine keeps one list of its eponymous tables per session and builds
898    /// a new catalog from it at every open and every schema change, so a
899    /// shared table is one count added where [`StaticCatalog::with_eponymous`]
900    /// would copy every column.
901    ///
902    /// @param table - the module's table
903    pub fn with_shared_eponymous(mut self, table: std::rc::Rc<TableInfo>) -> StaticCatalog {
904        self.eponymous.push(table);
905        self
906    }
907
908    /// Adds a table, returning the catalog, for building fixtures.
909    /// Returns one table by folded name, searching every database.
910    ///
911    /// Attachment order, `main` first, which is the order an unqualified name
912    /// resolves in. A module asking about a name it was given as an argument
913    /// wants the same table the statement that named it would have found.
914    ///
915    /// @param folded - the table's ASCII-folded name
916    pub fn table_named(&self, folded: &[u8]) -> Option<&TableInfo> {
917        self.tables
918            .iter()
919            .map(std::rc::Rc::as_ref)
920            .find(|table| table.folded == folded)
921    }
922
923    /// Returns this catalog with one more table in it.
924    ///
925    /// @param table - the table to add
926    pub fn with_table(mut self, table: TableInfo) -> StaticCatalog {
927        self.tables.push(std::rc::Rc::new(table));
928        self
929    }
930}
931
932/// Returns the name a schema qualified table name is looked up under.
933///
934/// **`temp.sqlite_schema` and `temp.sqlite_master` are the temporary
935/// catalog**, as SQLite answers them. The temporary catalog is registered only
936/// as `sqlite_temp_schema` and `sqlite_temp_master`, because an unqualified
937/// `sqlite_schema` searches `temp` first and has to mean `main`'s. A qualified
938/// name has no search, so it is mapped here, and after `CREATE TEMP TABLE
939/// scratch (x)` all four names answer `scratch`.
940///
941/// @param database - the qualifier the statement wrote
942/// @param folded - the table's folded name
943fn qualified_catalog_name<'a>(database: &[u8], folded: &'a [u8]) -> &'a [u8] {
944    if !database.eq_ignore_ascii_case(b"temp") {
945        return folded;
946    }
947    match folded {
948        b"sqlite_schema" => b"sqlite_temp_schema",
949        b"sqlite_master" => b"sqlite_temp_master",
950        other => other,
951    }
952}
953
954impl CatalogView for StaticCatalog {
955    /// Returns a table as a shared pointer; the trait method's override.
956    ///
957    /// @param database - the schema qualifier, if the statement wrote one
958    /// @param folded - the table's folded name
959    fn shared_table(
960        &self,
961        database: Option<&[u8]>,
962        folded: &[u8],
963    ) -> Option<std::rc::Rc<TableInfo>> {
964        if let Some(database) = database {
965            let index = self.database_index(database)?;
966            let folded = qualified_catalog_name(database, folded);
967            return self
968                .tables
969                .iter()
970                .find(|table| table.database == index && table.folded == folded)
971                .map(std::rc::Rc::clone);
972        }
973        for index in self.search_order() {
974            if let Some(found) = self
975                .tables
976                .iter()
977                .find(|table| table.database == index && table.folded == folded)
978            {
979                return Some(std::rc::Rc::clone(found));
980            }
981        }
982        self.eponymous
983            .iter()
984            .find(|table| table.folded == folded)
985            .map(std::rc::Rc::clone)
986    }
987
988    /// Returns the number of attached databases.
989    fn database_count(&self) -> usize {
990        self.databases.len()
991    }
992
993    /// Returns the name of an attached database by index.
994    fn database_name(&self, index: usize) -> &[u8] {
995        self.databases.get(index).map_or(&[], |(name, _)| name)
996    }
997
998    /// Returns the index of an attached database by folded name.
999    fn database_index(&self, folded: &[u8]) -> Option<usize> {
1000        self.databases
1001            .iter()
1002            .position(|(name, _)| name.eq_ignore_ascii_case(folded))
1003    }
1004
1005    /// Returns a table by name, searching in SQLite's own order.
1006    fn find_table(&self, database: Option<&[u8]>, folded: &[u8]) -> Option<&TableInfo> {
1007        if let Some(database) = database {
1008            let index = self.database_index(database)?;
1009            let folded = qualified_catalog_name(database, folded);
1010            return self
1011                .tables
1012                .iter()
1013                .find(|table| table.database == index && table.folded == folded)
1014                .map(std::rc::Rc::as_ref);
1015        }
1016        for index in self.search_order() {
1017            if let Some(found) = self
1018                .tables
1019                .iter()
1020                .find(|table| table.database == index && table.folded == folded)
1021            {
1022                return Some(found.as_ref());
1023            }
1024        }
1025        // Last, so a real table of the same name shadows the module.
1026        self.eponymous
1027            .iter()
1028            .find(|table| table.folded == folded)
1029            .map(std::rc::Rc::as_ref)
1030    }
1031
1032    /// Returns the table an index belongs to, and the index.
1033    fn every_table(&self) -> Vec<&TableInfo> {
1034        self.tables.iter().map(std::rc::Rc::as_ref).collect()
1035    }
1036
1037    fn find_index(
1038        &self,
1039        database: Option<&[u8]>,
1040        folded: &[u8],
1041    ) -> Option<(&TableInfo, &IndexInfo)> {
1042        let wanted = database.and_then(|name| self.database_index(name));
1043        for table in &self.tables {
1044            if wanted.is_some_and(|index| index != table.database) {
1045                continue;
1046            }
1047            if let Some(index) = table.indexes.iter().find(|index| index.folded == folded) {
1048                return Some((table, index));
1049            }
1050        }
1051        None
1052    }
1053
1054    /// Returns every table of one attached database.
1055    fn tables_of(&self, database: usize) -> Vec<&TableInfo> {
1056        self.tables
1057            .iter()
1058            .filter(|table| table.database == database)
1059            .map(std::rc::Rc::as_ref)
1060            .collect()
1061    }
1062
1063    /// Returns the schema cookie of an attached database.
1064    fn schema_cookie(&self, database: usize) -> u32 {
1065        self.databases
1066            .get(database)
1067            .map_or(0, |(_, cookie)| *cookie)
1068    }
1069
1070    /// Returns the generation of the snapshot.
1071    fn generation(&self) -> u64 {
1072        self.generation
1073    }
1074}
1075
1076impl StaticCatalog {
1077    /// Returns the database indexes in the order an unqualified name searches.
1078    fn search_order(&self) -> Vec<usize> {
1079        let mut order: Vec<usize> = Vec::with_capacity(self.databases.len());
1080        if let Some(temp) = self
1081            .databases
1082            .iter()
1083            .position(|(name, _)| name.eq_ignore_ascii_case(b"temp"))
1084        {
1085            order.push(temp);
1086        }
1087        for (index, _) in self.databases.iter().enumerate() {
1088            if !order.contains(&index) {
1089                order.push(index);
1090            }
1091        }
1092        order
1093    }
1094}
1095
1096#[cfg(test)]
1097mod tests {
1098    use super::*;
1099
1100    /// Builds a one-column table for the tests below.
1101    fn table(name: &[u8], database: usize) -> TableInfo {
1102        TableInfo {
1103            name: name.to_vec(),
1104            folded: name.to_ascii_lowercase(),
1105            database,
1106            root: 2,
1107            columns: vec![ColumnInfo {
1108                name: b"a".to_vec(),
1109                folded: b"a".to_vec(),
1110                declared_type: Vec::new(),
1111                affinity: Affinity::Blob,
1112                collation: b"binary".to_vec(),
1113                not_null: false,
1114                not_null_conflict: None,
1115                primary_key_conflict: None,
1116                default_sql: None,
1117                primary_key_position: None,
1118                hidden: false,
1119                generated: false,
1120                stored: false,
1121                generated_sql: None,
1122            }],
1123            rowid_alias: None,
1124            without_rowid: false,
1125            strict: false,
1126            autoincrement: false,
1127            kind: TableKind::Table,
1128            create_sql: Vec::new(),
1129            indexes: Vec::new(),
1130            view: None,
1131            triggers: Vec::new(),
1132            analysed_rows: None,
1133            checks: Vec::new(),
1134            foreign_keys: Vec::new(),
1135            foreign_key_triggers: Vec::new(),
1136            module: None,
1137        }
1138    }
1139
1140    /// An unqualified name finds `temp` before `main`, which is the rule that
1141    /// lets a temp table shadow a real one.
1142    #[test]
1143    fn temp_is_searched_before_main() {
1144        let catalog = StaticCatalog {
1145            databases: vec![(b"main".to_vec(), 1), (b"temp".to_vec(), 2)],
1146            tables: vec![
1147                std::rc::Rc::new(table(b"t", 0)),
1148                std::rc::Rc::new(table(b"t", 1)),
1149            ],
1150            eponymous: Vec::new(),
1151            generation: 7,
1152        };
1153        let found = catalog.find_table(None, b"t").expect("it resolves");
1154        assert_eq!(found.database, 1);
1155        let qualified = catalog
1156            .find_table(Some(b"main"), b"t")
1157            .expect("it resolves");
1158        assert_eq!(qualified.database, 0);
1159    }
1160
1161    /// The three rowid spellings resolve, and a real column of that name wins.
1162    #[test]
1163    fn the_rowid_spellings_resolve_unless_shadowed() {
1164        let mut plain = table(b"t", 0);
1165        assert!(plain.is_rowid_name(b"rowid"));
1166        assert!(plain.is_rowid_name(b"_rowid_"));
1167        assert!(plain.is_rowid_name(b"oid"));
1168        assert!(!plain.is_rowid_name(b"id"));
1169
1170        if let Some(column) = plain.columns.first_mut() {
1171            column.name = b"oid".to_vec();
1172            column.folded = b"oid".to_vec();
1173        }
1174        assert!(!plain.is_rowid_name(b"oid"));
1175        assert!(plain.is_rowid_name(b"rowid"));
1176
1177        let mut without = table(b"t", 0);
1178        without.without_rowid = true;
1179        assert!(!without.is_rowid_name(b"rowid"));
1180    }
1181
1182    /// A missing database or table is `None`, never a panic.
1183    #[test]
1184    fn a_missing_name_is_none() {
1185        let catalog = StaticCatalog::empty();
1186        assert!(catalog.find_table(None, b"nope").is_none());
1187        assert!(catalog.find_table(Some(b"nodb"), b"t").is_none());
1188        assert_eq!(catalog.database_name(99), b"");
1189        assert_eq!(catalog.schema_cookie(99), 0);
1190    }
1191}