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 a table, returning the catalog, for building fixtures.
895 /// Returns one table by folded name, searching every database.
896 ///
897 /// Attachment order, `main` first, which is the order an unqualified name
898 /// resolves in. A module asking about a name it was given as an argument
899 /// wants the same table the statement that named it would have found.
900 ///
901 /// @param folded - the table's ASCII-folded name
902 pub fn table_named(&self, folded: &[u8]) -> Option<&TableInfo> {
903 self.tables
904 .iter()
905 .map(std::rc::Rc::as_ref)
906 .find(|table| table.folded == folded)
907 }
908
909 /// Returns this catalog with one more table in it.
910 ///
911 /// @param table - the table to add
912 pub fn with_table(mut self, table: TableInfo) -> StaticCatalog {
913 self.tables.push(std::rc::Rc::new(table));
914 self
915 }
916}
917
918/// Returns the name a schema qualified table name is looked up under.
919///
920/// **`temp.sqlite_schema` and `temp.sqlite_master` are the temporary
921/// catalog**, as SQLite answers them. The temporary catalog is registered only
922/// as `sqlite_temp_schema` and `sqlite_temp_master`, because an unqualified
923/// `sqlite_schema` searches `temp` first and has to mean `main`'s. A qualified
924/// name has no search, so it is mapped here, and after `CREATE TEMP TABLE
925/// scratch (x)` all four names answer `scratch`.
926///
927/// @param database - the qualifier the statement wrote
928/// @param folded - the table's folded name
929fn qualified_catalog_name<'a>(database: &[u8], folded: &'a [u8]) -> &'a [u8] {
930 if !database.eq_ignore_ascii_case(b"temp") {
931 return folded;
932 }
933 match folded {
934 b"sqlite_schema" => b"sqlite_temp_schema",
935 b"sqlite_master" => b"sqlite_temp_master",
936 other => other,
937 }
938}
939
940impl CatalogView for StaticCatalog {
941 /// Returns a table as a shared pointer; the trait method's override.
942 ///
943 /// @param database - the schema qualifier, if the statement wrote one
944 /// @param folded - the table's folded name
945 fn shared_table(
946 &self,
947 database: Option<&[u8]>,
948 folded: &[u8],
949 ) -> Option<std::rc::Rc<TableInfo>> {
950 if let Some(database) = database {
951 let index = self.database_index(database)?;
952 let folded = qualified_catalog_name(database, folded);
953 return self
954 .tables
955 .iter()
956 .find(|table| table.database == index && table.folded == folded)
957 .map(std::rc::Rc::clone);
958 }
959 for index in self.search_order() {
960 if let Some(found) = self
961 .tables
962 .iter()
963 .find(|table| table.database == index && table.folded == folded)
964 {
965 return Some(std::rc::Rc::clone(found));
966 }
967 }
968 self.eponymous
969 .iter()
970 .find(|table| table.folded == folded)
971 .map(std::rc::Rc::clone)
972 }
973
974 /// Returns the number of attached databases.
975 fn database_count(&self) -> usize {
976 self.databases.len()
977 }
978
979 /// Returns the name of an attached database by index.
980 fn database_name(&self, index: usize) -> &[u8] {
981 self.databases.get(index).map_or(&[], |(name, _)| name)
982 }
983
984 /// Returns the index of an attached database by folded name.
985 fn database_index(&self, folded: &[u8]) -> Option<usize> {
986 self.databases
987 .iter()
988 .position(|(name, _)| name.eq_ignore_ascii_case(folded))
989 }
990
991 /// Returns a table by name, searching in SQLite's own order.
992 fn find_table(&self, database: Option<&[u8]>, folded: &[u8]) -> Option<&TableInfo> {
993 if let Some(database) = database {
994 let index = self.database_index(database)?;
995 let folded = qualified_catalog_name(database, folded);
996 return self
997 .tables
998 .iter()
999 .find(|table| table.database == index && table.folded == folded)
1000 .map(std::rc::Rc::as_ref);
1001 }
1002 for index in self.search_order() {
1003 if let Some(found) = self
1004 .tables
1005 .iter()
1006 .find(|table| table.database == index && table.folded == folded)
1007 {
1008 return Some(found.as_ref());
1009 }
1010 }
1011 // Last, so a real table of the same name shadows the module.
1012 self.eponymous
1013 .iter()
1014 .find(|table| table.folded == folded)
1015 .map(std::rc::Rc::as_ref)
1016 }
1017
1018 /// Returns the table an index belongs to, and the index.
1019 fn every_table(&self) -> Vec<&TableInfo> {
1020 self.tables.iter().map(std::rc::Rc::as_ref).collect()
1021 }
1022
1023 fn find_index(
1024 &self,
1025 database: Option<&[u8]>,
1026 folded: &[u8],
1027 ) -> Option<(&TableInfo, &IndexInfo)> {
1028 let wanted = database.and_then(|name| self.database_index(name));
1029 for table in &self.tables {
1030 if wanted.is_some_and(|index| index != table.database) {
1031 continue;
1032 }
1033 if let Some(index) = table.indexes.iter().find(|index| index.folded == folded) {
1034 return Some((table, index));
1035 }
1036 }
1037 None
1038 }
1039
1040 /// Returns every table of one attached database.
1041 fn tables_of(&self, database: usize) -> Vec<&TableInfo> {
1042 self.tables
1043 .iter()
1044 .filter(|table| table.database == database)
1045 .map(std::rc::Rc::as_ref)
1046 .collect()
1047 }
1048
1049 /// Returns the schema cookie of an attached database.
1050 fn schema_cookie(&self, database: usize) -> u32 {
1051 self.databases
1052 .get(database)
1053 .map_or(0, |(_, cookie)| *cookie)
1054 }
1055
1056 /// Returns the generation of the snapshot.
1057 fn generation(&self) -> u64 {
1058 self.generation
1059 }
1060}
1061
1062impl StaticCatalog {
1063 /// Returns the database indexes in the order an unqualified name searches.
1064 fn search_order(&self) -> Vec<usize> {
1065 let mut order: Vec<usize> = Vec::with_capacity(self.databases.len());
1066 if let Some(temp) = self
1067 .databases
1068 .iter()
1069 .position(|(name, _)| name.eq_ignore_ascii_case(b"temp"))
1070 {
1071 order.push(temp);
1072 }
1073 for (index, _) in self.databases.iter().enumerate() {
1074 if !order.contains(&index) {
1075 order.push(index);
1076 }
1077 }
1078 order
1079 }
1080}
1081
1082#[cfg(test)]
1083mod tests {
1084 use super::*;
1085
1086 /// Builds a one-column table for the tests below.
1087 fn table(name: &[u8], database: usize) -> TableInfo {
1088 TableInfo {
1089 name: name.to_vec(),
1090 folded: name.to_ascii_lowercase(),
1091 database,
1092 root: 2,
1093 columns: vec![ColumnInfo {
1094 name: b"a".to_vec(),
1095 folded: b"a".to_vec(),
1096 declared_type: Vec::new(),
1097 affinity: Affinity::Blob,
1098 collation: b"binary".to_vec(),
1099 not_null: false,
1100 not_null_conflict: None,
1101 primary_key_conflict: None,
1102 default_sql: None,
1103 primary_key_position: None,
1104 hidden: false,
1105 generated: false,
1106 stored: false,
1107 generated_sql: None,
1108 }],
1109 rowid_alias: None,
1110 without_rowid: false,
1111 strict: false,
1112 autoincrement: false,
1113 kind: TableKind::Table,
1114 create_sql: Vec::new(),
1115 indexes: Vec::new(),
1116 view: None,
1117 triggers: Vec::new(),
1118 analysed_rows: None,
1119 checks: Vec::new(),
1120 foreign_keys: Vec::new(),
1121 foreign_key_triggers: Vec::new(),
1122 module: None,
1123 }
1124 }
1125
1126 /// An unqualified name finds `temp` before `main`, which is the rule that
1127 /// lets a temp table shadow a real one.
1128 #[test]
1129 fn temp_is_searched_before_main() {
1130 let catalog = StaticCatalog {
1131 databases: vec![(b"main".to_vec(), 1), (b"temp".to_vec(), 2)],
1132 tables: vec![
1133 std::rc::Rc::new(table(b"t", 0)),
1134 std::rc::Rc::new(table(b"t", 1)),
1135 ],
1136 eponymous: Vec::new(),
1137 generation: 7,
1138 };
1139 let found = catalog.find_table(None, b"t").expect("it resolves");
1140 assert_eq!(found.database, 1);
1141 let qualified = catalog
1142 .find_table(Some(b"main"), b"t")
1143 .expect("it resolves");
1144 assert_eq!(qualified.database, 0);
1145 }
1146
1147 /// The three rowid spellings resolve, and a real column of that name wins.
1148 #[test]
1149 fn the_rowid_spellings_resolve_unless_shadowed() {
1150 let mut plain = table(b"t", 0);
1151 assert!(plain.is_rowid_name(b"rowid"));
1152 assert!(plain.is_rowid_name(b"_rowid_"));
1153 assert!(plain.is_rowid_name(b"oid"));
1154 assert!(!plain.is_rowid_name(b"id"));
1155
1156 if let Some(column) = plain.columns.first_mut() {
1157 column.name = b"oid".to_vec();
1158 column.folded = b"oid".to_vec();
1159 }
1160 assert!(!plain.is_rowid_name(b"oid"));
1161 assert!(plain.is_rowid_name(b"rowid"));
1162
1163 let mut without = table(b"t", 0);
1164 without.without_rowid = true;
1165 assert!(!without.is_rowid_name(b"rowid"));
1166 }
1167
1168 /// A missing database or table is `None`, never a panic.
1169 #[test]
1170 fn a_missing_name_is_none() {
1171 let catalog = StaticCatalog::empty();
1172 assert!(catalog.find_table(None, b"nope").is_none());
1173 assert!(catalog.find_table(Some(b"nodb"), b"t").is_none());
1174 assert_eq!(catalog.database_name(99), b"");
1175 assert_eq!(catalog.schema_cookie(99), 0);
1176 }
1177}