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 let declared = self.declared_type.to_ascii_lowercase();
342 let rest = declared.strip_prefix(b"vector".as_slice())?;
343 let inside: Vec<u8> = rest
344 .iter()
345 .copied()
346 .skip_while(|byte| byte.is_ascii_whitespace())
347 .collect();
348 let inside = inside.strip_prefix(b"(".as_slice())?;
349 let inside = inside.strip_suffix(b")".as_slice())?;
350 let text = std::str::from_utf8(inside).ok()?.trim();
351 let width: usize = text.parse().ok()?;
352 (width > 0).then_some(width)
353 }
354}
355
356impl TriggerInfo {
357 /// Returns whether this trigger fires for one event on one column set.
358 ///
359 /// `changed` is the folded names an UPDATE assigns, and is empty for the
360 /// other two events. `UPDATE OF a, b` fires only when the statement writes
361 /// `a` or `b` - which SQLite decides from the *statement*, not from whether
362 /// the value actually differs.
363 pub fn fires_for(&self, event: &TriggerEventInfo, changed: &[Vec<u8>]) -> bool {
364 match (&self.event, event) {
365 (TriggerEventInfo::Insert, TriggerEventInfo::Insert) => true,
366 (TriggerEventInfo::Delete, TriggerEventInfo::Delete) => true,
367 (TriggerEventInfo::Update(of), TriggerEventInfo::Update(_)) => {
368 of.is_empty() || of.iter().any(|name| changed.contains(name))
369 }
370 _ => false,
371 }
372 }
373}
374
375/// A table, view or virtual table.
376#[derive(Clone, Debug, PartialEq, Eq)]
377pub struct TableInfo {
378 /// The name as declared.
379 pub name: Vec<u8>,
380 /// The ASCII-folded lookup key.
381 pub folded: Vec<u8>,
382 /// Which attached database it belongs to.
383 pub database: usize,
384 /// The root page of the table B-tree, or zero for a view.
385 pub root: u32,
386 /// The columns, in declaration order.
387 pub columns: Vec<ColumnInfo>,
388 /// The column that is an alias for the rowid, when there is one.
389 pub rowid_alias: Option<u16>,
390 /// Whether the table is `WITHOUT ROWID`.
391 pub without_rowid: bool,
392 /// Whether the table is `STRICT`.
393 pub strict: bool,
394 /// Whether the rowid alias was declared `AUTOINCREMENT`.
395 ///
396 /// It changes where a new rowid comes from: an ordinary table reuses the
397 /// numbers its deleted rows had, and an `AUTOINCREMENT` one never does,
398 /// because it remembers the largest it has ever handed out in
399 /// `sqlite_sequence`.
400 pub autoincrement: bool,
401 /// What kind of object this is.
402 pub kind: TableKind,
403 /// The `CREATE` text as stored in `sqlite_schema`.
404 pub create_sql: Vec<u8>,
405 /// The indexes over this table.
406 pub indexes: Vec<IndexInfo>,
407 /// The parsed body, when this is a view.
408 pub view: Option<Box<ViewBody>>,
409 /// The triggers attached to this table or view, in schema order.
410 pub triggers: Vec<TriggerInfo>,
411 /// How many rows `ANALYZE` counted, when it has run.
412 pub analysed_rows: Option<i64>,
413 /// The triggers this table's writes fire because of a foreign key.
414 ///
415 /// Both directions are here, because both are things that happen when
416 /// *this* table is written: the checks its own keys need when a row
417 /// arrives, and the actions the keys pointing at it need when a row
418 /// leaves. They are built once when the schema is read rather than once
419 /// per statement, because generating and parsing them is the same work
420 /// every time and the schema is what decides them.
421 pub foreign_key_triggers: Vec<ForeignKeyTrigger>,
422 /// Every foreign key declared on this table, in declaration order.
423 ///
424 /// The child's side of the relationship, which is the side the table
425 /// carries. Finding the keys that point *at* a table means walking the
426 /// database's tables and asking each one, which is what
427 /// `CatalogView::foreign_keys_referencing` does - and is what SQLite does
428 /// too, because nothing in the file records the reverse direction.
429 pub foreign_keys: Vec<ForeignKeyInfo>,
430 /// Every `CHECK` constraint, as the source text it was written as.
431 ///
432 /// The text rather than a bound expression, for the same reason
433 /// `default_sql` is text: the catalog is below the binder, so it cannot
434 /// bind anything, and a constraint that had been half-interpreted on the
435 /// way through would be a second source of truth beside the `CREATE`
436 /// statement the file actually stores.
437 pub checks: Vec<CheckInfo>,
438 /// The module a virtual table is implemented by, and its arguments.
439 ///
440 /// The catalog records the question and the session fills in the answer:
441 /// what columns the table has is the module's to say, not the file's, so a
442 /// virtual table arrives here with a module and no columns and leaves the
443 /// connection's schema load with both.
444 pub module: Option<crate::vtab::ModuleRef>,
445}
446
447/// One trigger a foreign key implies, or the reason there is not one.
448#[derive(Clone, Debug, PartialEq, Eq)]
449pub struct ForeignKeyTrigger {
450 /// Whether it refuses a write rather than repairing one.
451 ///
452 /// Only a check can be deferred. An action is what the constraint *does*,
453 /// and doing it at commit time instead would leave the rows in between
454 /// visible to the statements that come after.
455 pub is_check: bool,
456 /// Whether the key it enforces was declared `INITIALLY DEFERRED`.
457 pub deferred: bool,
458 /// The trigger, or `None` when the key cannot be enforced at all.
459 pub trigger: Option<TriggerInfo>,
460 /// Why it cannot be, when it cannot.
461 ///
462 /// A key whose parent table is missing, or whose parent columns are not a
463 /// key of the parent, is legal to declare: SQLite reports it when
464 /// something writes, not when the schema is read, so that a schema can be
465 /// loaded in any order. The message is kept here and reported then.
466 pub fault: Vec<u8>,
467 /// Whether the key's child table and its parent table are the same table.
468 ///
469 /// **Read by `DROP TABLE`'s implicit delete (task-1979, F6).** That delete
470 /// removes every row of one table, so a key whose child is that same table
471 /// cannot be violated once the statement has finished - the rows that would
472 /// be left pointing at nothing are themselves gone. SQLite reaches the same
473 /// answer a different way: its immediate foreign keys are a counter checked
474 /// at the end of the statement, so the violation deleting the first row
475 /// creates is cancelled by deleting the row that made it.
476 pub self_referencing: bool,
477}
478
479/// One foreign key, from the child table that declares it.
480#[derive(Clone, Debug, PartialEq, Eq)]
481pub struct ForeignKeyInfo {
482 /// The constraint's position in its table, counting from zero.
483 ///
484 /// `PRAGMA foreign_key_list` reports it, and it is how a diagnostic names
485 /// a constraint that was written without a name - which is most of them.
486 pub id: u32,
487 /// The child columns, in the order they were written.
488 pub columns: Vec<u16>,
489 /// The parent table's name as written.
490 pub parent: Vec<u8>,
491 /// The parent table's folded name.
492 pub parent_folded: Vec<u8>,
493 /// The parent columns as written, or empty when the clause named none.
494 ///
495 /// Empty means the parent's primary key, and it stays empty rather than
496 /// being resolved here: the catalog builds one table at a time and the
497 /// parent may not have been read yet - or may not exist, which is legal
498 /// until something writes a row.
499 pub parent_columns: Vec<Vec<u8>>,
500 /// What happens to the child rows when a parent row is deleted.
501 pub on_delete: ReferentialAction,
502 /// What happens to the child rows when a parent key changes.
503 pub on_update: ReferentialAction,
504 /// The `MATCH` clause as written, which SQLite parses and ignores.
505 pub match_clause: Vec<u8>,
506 /// Whether `DEFERRABLE` was written.
507 pub deferrable: bool,
508 /// Whether `INITIALLY DEFERRED` was written.
509 pub initially_deferred: bool,
510 /// Whether following this key can lead back to the table that declares it.
511 ///
512 /// A tree with `ON DELETE CASCADE` on its parent column is the everyday
513 /// case, and it is the one case an action cannot simply be inlined into
514 /// the statement that fires it: the body would have to appear once per
515 /// level the data happens to be deep, which is not known when the
516 /// statement is compiled. A cyclic key's action is applied by repeating it
517 /// until nothing changes instead, and this is what says which keys need
518 /// that.
519 pub cyclic: bool,
520}
521
522impl ForeignKeyInfo {
523 /// Reports whether the constraint's checks wait until the transaction
524 /// commits.
525 pub fn is_deferred(&self) -> bool {
526 self.deferrable && self.initially_deferred
527 }
528}
529
530/// One `CHECK` constraint.
531#[derive(Clone, Debug, PartialEq, Eq)]
532pub struct CheckInfo {
533 /// The constraint's name, when one was written.
534 pub name: Option<Vec<u8>>,
535 /// The predicate, as the source text between its parentheses.
536 pub expr_sql: Vec<u8>,
537 /// The `ON CONFLICT` clause a table-level `CHECK` was written with.
538 ///
539 /// **Recorded and not acted on**, because that is what the reference does:
540 /// SQLite's grammar accepts `CHECK (expr) onconf` on a table constraint and
541 /// its builder never reads the clause, so such a constraint aborts like any
542 /// other. It is kept here so the derivation is a full account of the text
543 /// rather than a lossy one, and so the next reader finds the measurement
544 /// instead of the question.
545 pub conflict: Option<ConflictAction>,
546}
547
548impl TableInfo {
549 /// Returns the position of a column by its folded name.
550 pub fn column_position(&self, folded: &[u8]) -> Option<u16> {
551 self.columns
552 .iter()
553 .position(|column| column.folded == folded)
554 .map(|index| index as u16)
555 }
556
557 /// Returns a column by position.
558 pub fn column(&self, position: u16) -> Option<&ColumnInfo> {
559 self.columns.get(position as usize)
560 }
561
562 /// Returns whether the table has a rowid a query may refer to.
563 pub fn has_rowid(&self) -> bool {
564 // A virtual table has one unless its module declared otherwise: FTS5
565 // and the R-Tree both key their rows by it, and `SELECT rowid FROM t`
566 // is how an application joins to them.
567 matches!(self.kind, TableKind::Table | TableKind::Virtual) && !self.without_rowid
568 }
569
570 /// Returns a table that stands for an eponymous module.
571 ///
572 /// A module reached as a name rather than through `CREATE VIRTUAL TABLE` -
573 /// `generate_series`, `json_each`, `pragma_table_info` - belongs to no
574 /// database and has no `sqlite_schema` row, so everything a stored table
575 /// carries is absent and only the module's declaration remains.
576 ///
577 /// @param name - the module's name, which is also the table's
578 /// @param columns - the columns the module declared
579 /// @param module - the module reference the executor resolves it by
580 /// @param without_rowid - whether the module declared no rowid
581 pub fn eponymous(
582 name: Vec<u8>,
583 columns: Vec<ColumnInfo>,
584 module: crate::vtab::ModuleRef,
585 without_rowid: bool,
586 ) -> TableInfo {
587 let folded = name.to_ascii_lowercase();
588 TableInfo {
589 name,
590 folded,
591 database: 0,
592 root: 0,
593 columns,
594 rowid_alias: None,
595 without_rowid,
596 strict: false,
597 autoincrement: false,
598 kind: TableKind::Virtual,
599 create_sql: Vec::new(),
600 foreign_keys: Vec::new(),
601 foreign_key_triggers: Vec::new(),
602 module: Some(module),
603 view: None,
604 triggers: Vec::new(),
605 analysed_rows: None,
606 indexes: Vec::new(),
607 checks: Vec::new(),
608 }
609 }
610
611 /// Returns a table that stands for a nested query's result.
612 ///
613 /// The column list is the block's result columns: their names are what a
614 /// reference to the subquery resolves against, and their affinity and
615 /// collation are the ones the expressions behind them carry, so a
616 /// comparison against a subquery column applies the same rules it would
617 /// have applied one level down.
618 pub fn subquery(name: Vec<u8>, database: usize, columns: Vec<ColumnInfo>) -> TableInfo {
619 let folded = name.to_ascii_lowercase();
620 TableInfo {
621 name,
622 folded,
623 database,
624 root: 0,
625 columns,
626 rowid_alias: None,
627 without_rowid: true,
628 strict: false,
629 autoincrement: false,
630 kind: TableKind::Subquery,
631 create_sql: Vec::new(),
632 foreign_keys: Vec::new(),
633 foreign_key_triggers: Vec::new(),
634 module: None,
635 view: None,
636 triggers: Vec::new(),
637 analysed_rows: None,
638 indexes: Vec::new(),
639 checks: Vec::new(),
640 }
641 }
642
643 /// Returns the record slot a column's value lives in, when it has one.
644 ///
645 /// `VIRTUAL` generated columns take no slot, so the slots of the columns
646 /// after them shift down. Every read of a stored column has to go through
647 /// this rather than through the column's declared position, and a `VIRTUAL`
648 /// column has no slot at all - it is computed.
649 pub fn record_slot(&self, column: u16) -> Option<usize> {
650 if self.without_rowid {
651 return self
652 .record_order()
653 .iter()
654 .position(|stored| *stored == column);
655 }
656 let mut slot = 0usize;
657 for (position, info) in self.columns.iter().enumerate() {
658 if info.generated && !info.stored {
659 if position == usize::from(column) {
660 return None;
661 }
662 continue;
663 }
664 if position == usize::from(column) {
665 return Some(slot);
666 }
667 slot = slot.saturating_add(1);
668 }
669 None
670 }
671
672 /// Returns the primary key's columns, in key order.
673 ///
674 /// Key order, not declaration order: `PRIMARY KEY(b, a)` is ordered by `b`
675 /// and then `a` however the columns were declared, and for a `WITHOUT
676 /// ROWID` table that order also decides where in the record they sit.
677 pub fn primary_key(&self) -> Vec<u16> {
678 let mut keys: Vec<(u16, u16)> = self
679 .columns
680 .iter()
681 .enumerate()
682 .filter_map(|(position, column)| {
683 column
684 .primary_key_position
685 .map(|key| (key, position as u16))
686 })
687 .collect();
688 keys.sort_by_key(|(key, _)| *key);
689 keys.into_iter().map(|(_, position)| position).collect()
690 }
691
692 /// Returns the columns a record holds, in the order it holds them.
693 ///
694 /// A rowid table stores its columns as declared. A `WITHOUT ROWID` table's
695 /// B-tree is an index whose key is the primary key, so its record is the
696 /// key columns first, in key order, and then everything else as declared -
697 /// verified against a file the pinned build wrote: `PRIMARY KEY(b, a)` over
698 /// `(a, b, c)` stores `(b, a, c)`.
699 pub fn record_order(&self) -> Vec<u16> {
700 let stored = |position: usize| {
701 self.columns
702 .get(position)
703 .is_some_and(|column| !column.generated || column.stored)
704 };
705 if !self.without_rowid {
706 return (0..self.columns.len())
707 .filter(|position| stored(*position))
708 .map(|position| position as u16)
709 .collect();
710 }
711 let keys = self.primary_key();
712 let mut order = keys.clone();
713 for position in 0..self.columns.len() {
714 if keys.contains(&(position as u16)) || !stored(position) {
715 continue;
716 }
717 order.push(position as u16);
718 }
719 order
720 }
721
722 /// Returns whether a name is one of the rowid's three spellings and is not
723 /// shadowed by a real column.
724 ///
725 /// SQLite's rule is exactly this: `rowid`, `_rowid_` and `oid` name the
726 /// rowid *unless* the table declares a column with that name, in which case
727 /// the column wins. A table without a rowid has none of the three.
728 pub fn is_rowid_name(&self, folded: &[u8]) -> bool {
729 if !self.has_rowid() {
730 return false;
731 }
732 let spelled = folded == b"rowid" || folded == b"_rowid_" || folded == b"oid";
733 spelled && self.column_position(folded).is_none()
734 }
735}
736
737/// The read-only schema the binder resolves names against.
738pub trait CatalogView {
739 /// Returns the number of attached databases.
740 fn database_count(&self) -> usize;
741
742 /// Returns the name of an attached database by index.
743 fn database_name(&self, index: usize) -> &[u8];
744
745 /// Returns the index of an attached database by folded name.
746 fn database_index(&self, folded: &[u8]) -> Option<usize>;
747
748 /// Returns a table, view or virtual table by name.
749 ///
750 /// With no qualifier the search follows SQLite's order: `temp`, then
751 /// `main`, then every other attached database in attachment order.
752 fn find_table(&self, database: Option<&[u8]>, folded: &[u8]) -> Option<&TableInfo>;
753
754 /// Returns a table as a shared pointer, for a caller that has to keep it.
755 ///
756 /// A binder keeps what it finds for the life of the bound statement.
757 /// [`CatalogView::find_table`] hands back a borrow, so keeping it meant
758 /// cloning a `TableInfo` - two name vectors, a `ColumnInfo` per column with
759 /// its own heap fields, the `CREATE` text and an `IndexInfo` per index -
760 /// for every table reference in every statement. Measured at 2,938 ns of
761 /// `prepare.point`'s 6,093 ns compile.
762 ///
763 /// The default is that clone, so an implementor that has nothing to share
764 /// keeps working and is merely no faster. `StaticCatalog` shares.
765 ///
766 /// @param database - the schema qualifier, if the statement wrote one
767 /// @param folded - the table's folded name
768 fn shared_table(
769 &self,
770 database: Option<&[u8]>,
771 folded: &[u8],
772 ) -> Option<std::rc::Rc<TableInfo>> {
773 self.find_table(database, folded)
774 .map(|table| std::rc::Rc::new(table.clone()))
775 }
776
777 /// Returns the table an index belongs to, together with the index.
778 ///
779 /// Index names live in the same namespace as table names in SQLite, but
780 /// the catalog stores an index inside the table it indexes - which is
781 /// where every reader of one wants it. `DROP INDEX` is the caller that
782 /// has only the name, so the search lives here rather than being written
783 /// out again wherever a name has to be resolved.
784 fn find_index(
785 &self,
786 database: Option<&[u8]>,
787 folded: &[u8],
788 ) -> Option<(&TableInfo, &IndexInfo)>;
789
790 /// Returns the table a trigger is attached to, together with the trigger.
791 ///
792 /// Triggers share the name namespace with tables and indexes and are stored
793 /// on the object they fire for, so this is `find_index` again for the other
794 /// kind of attached object: `DROP TRIGGER` and `CREATE TRIGGER` both have
795 /// only the name.
796 fn find_trigger(
797 &self,
798 database: Option<&[u8]>,
799 folded: &[u8],
800 ) -> Option<(&TableInfo, &TriggerInfo)> {
801 let wanted = database.and_then(|name| self.database_index(name));
802 for table in self.every_table() {
803 if wanted.is_some_and(|index| index != table.database) {
804 continue;
805 }
806 if let Some(trigger) = table.triggers.iter().find(|one| one.folded == folded) {
807 return Some((table, trigger));
808 }
809 }
810 None
811 }
812
813 /// Returns every table of every attached database.
814 ///
815 /// It exists so [`CatalogView::find_trigger`] can have one implementation
816 /// rather than one per catalog: a trigger search is the same walk whatever
817 /// the tables are stored in.
818 fn every_table(&self) -> Vec<&TableInfo>;
819
820 /// Returns every table of one attached database, in no particular order.
821 fn tables_of(&self, database: usize) -> Vec<&TableInfo>;
822
823 /// Returns the schema cookie of an attached database, which a prepared
824 /// statement records so it can tell whether the schema moved under it.
825 fn schema_cookie(&self, database: usize) -> u32;
826
827 /// Returns the generation of the whole snapshot.
828 fn generation(&self) -> u64;
829}
830
831/// A catalog held in memory, which is what a test binds against and what the
832/// loader produces once it has read `sqlite_schema`.
833#[derive(Clone, Debug, Default, PartialEq, Eq)]
834pub struct StaticCatalog {
835 /// The attached databases, in attachment order, with their cookies.
836 pub databases: Vec<(Vec<u8>, u32)>,
837 /// Every table, in no particular order.
838 ///
839 /// **Shared rather than owned, because binding a statement used to clone
840 /// one.** `BoundSource.table` was a `TableInfo` by value, so every table
841 /// reference in every statement deep-copied the catalog's entry: two name
842 /// vectors, a `ColumnInfo` per column each with its own heap fields, the
843 /// full `CREATE` text, and an `IndexInfo` per index with its own column
844 /// vector. Forty-odd allocations to bind one `WHERE id = ?1`, measured at
845 /// 2,938 ns of `prepare.point`'s 6,093 - 48% of the statement's whole
846 /// compile. An `Rc` makes it a refcount bump.
847 pub tables: Vec<std::rc::Rc<TableInfo>>,
848 /// The eponymous virtual tables the connection's modules provide.
849 ///
850 /// `generate_series`, `json_each`, `json_tree`, `pragma_table_info`: the
851 /// name *is* the table, so they belong to no database and have no
852 /// `sqlite_schema` row. They are searched **last**, so a real table called
853 /// `generate_series` shadows the module rather than the other way round -
854 /// which is SQLite's order and the only safe one, because the file was
855 /// there first.
856 ///
857 /// Filled by the engine from its module registry on every catalog refresh.
858 /// Nothing used to fill it, and the eponymous form did not exist:
859 /// `FROM generate_series(1,10)` was `no such table`, which also left
860 /// `json_each` unreachable from SQL by any route, because `JsonWalkModule`
861 /// refuses `CREATE VIRTUAL TABLE` outright.
862 pub eponymous: Vec<std::rc::Rc<TableInfo>>,
863 /// The generation of this snapshot.
864 pub generation: u64,
865}
866
867impl StaticCatalog {
868 /// Returns a catalog with one `main` database and no objects.
869 pub fn empty() -> StaticCatalog {
870 StaticCatalog {
871 databases: vec![(b"main".to_vec(), 0)],
872 tables: Vec::new(),
873 eponymous: Vec::new(),
874 generation: 0,
875 }
876 }
877
878 /// Adds an eponymous virtual table, returning the catalog.
879 ///
880 /// @param table - the module's table, as its declaration describes it
881 pub fn with_eponymous(mut self, table: TableInfo) -> StaticCatalog {
882 self.eponymous.push(std::rc::Rc::new(table));
883 self
884 }
885
886 /// Adds a table, returning the catalog, for building fixtures.
887 /// Returns one table by folded name, searching every database.
888 ///
889 /// Attachment order, `main` first, which is the order an unqualified name
890 /// resolves in. A module asking about a name it was given as an argument
891 /// wants the same table the statement that named it would have found.
892 ///
893 /// @param folded - the table's ASCII-folded name
894 pub fn table_named(&self, folded: &[u8]) -> Option<&TableInfo> {
895 self.tables
896 .iter()
897 .map(std::rc::Rc::as_ref)
898 .find(|table| table.folded == folded)
899 }
900
901 /// Returns this catalog with one more table in it.
902 ///
903 /// @param table - the table to add
904 pub fn with_table(mut self, table: TableInfo) -> StaticCatalog {
905 self.tables.push(std::rc::Rc::new(table));
906 self
907 }
908}
909
910/// Returns the name a schema qualified table name is looked up under.
911///
912/// **`temp.sqlite_schema` and `temp.sqlite_master` are the temporary
913/// catalog**, as SQLite answers them. The temporary catalog is registered only
914/// as `sqlite_temp_schema` and `sqlite_temp_master`, because an unqualified
915/// `sqlite_schema` searches `temp` first and has to mean `main`'s. A qualified
916/// name has no search, so it is mapped here, and after `CREATE TEMP TABLE
917/// scratch (x)` all four names answer `scratch`.
918///
919/// @param database - the qualifier the statement wrote
920/// @param folded - the table's folded name
921fn qualified_catalog_name<'a>(database: &[u8], folded: &'a [u8]) -> &'a [u8] {
922 if !database.eq_ignore_ascii_case(b"temp") {
923 return folded;
924 }
925 match folded {
926 b"sqlite_schema" => b"sqlite_temp_schema",
927 b"sqlite_master" => b"sqlite_temp_master",
928 other => other,
929 }
930}
931
932impl CatalogView for StaticCatalog {
933 /// Returns a table as a shared pointer; the trait method's override.
934 ///
935 /// @param database - the schema qualifier, if the statement wrote one
936 /// @param folded - the table's folded name
937 fn shared_table(
938 &self,
939 database: Option<&[u8]>,
940 folded: &[u8],
941 ) -> Option<std::rc::Rc<TableInfo>> {
942 if let Some(database) = database {
943 let index = self.database_index(database)?;
944 let folded = qualified_catalog_name(database, folded);
945 return self
946 .tables
947 .iter()
948 .find(|table| table.database == index && table.folded == folded)
949 .map(std::rc::Rc::clone);
950 }
951 for index in self.search_order() {
952 if let Some(found) = self
953 .tables
954 .iter()
955 .find(|table| table.database == index && table.folded == folded)
956 {
957 return Some(std::rc::Rc::clone(found));
958 }
959 }
960 self.eponymous
961 .iter()
962 .find(|table| table.folded == folded)
963 .map(std::rc::Rc::clone)
964 }
965
966 /// Returns the number of attached databases.
967 fn database_count(&self) -> usize {
968 self.databases.len()
969 }
970
971 /// Returns the name of an attached database by index.
972 fn database_name(&self, index: usize) -> &[u8] {
973 self.databases.get(index).map_or(&[], |(name, _)| name)
974 }
975
976 /// Returns the index of an attached database by folded name.
977 fn database_index(&self, folded: &[u8]) -> Option<usize> {
978 self.databases
979 .iter()
980 .position(|(name, _)| name.eq_ignore_ascii_case(folded))
981 }
982
983 /// Returns a table by name, searching in SQLite's own order.
984 fn find_table(&self, database: Option<&[u8]>, folded: &[u8]) -> Option<&TableInfo> {
985 if let Some(database) = database {
986 let index = self.database_index(database)?;
987 let folded = qualified_catalog_name(database, folded);
988 return self
989 .tables
990 .iter()
991 .find(|table| table.database == index && table.folded == folded)
992 .map(std::rc::Rc::as_ref);
993 }
994 for index in self.search_order() {
995 if let Some(found) = self
996 .tables
997 .iter()
998 .find(|table| table.database == index && table.folded == folded)
999 {
1000 return Some(found.as_ref());
1001 }
1002 }
1003 // Last, so a real table of the same name shadows the module.
1004 self.eponymous
1005 .iter()
1006 .find(|table| table.folded == folded)
1007 .map(std::rc::Rc::as_ref)
1008 }
1009
1010 /// Returns the table an index belongs to, and the index.
1011 fn every_table(&self) -> Vec<&TableInfo> {
1012 self.tables.iter().map(std::rc::Rc::as_ref).collect()
1013 }
1014
1015 fn find_index(
1016 &self,
1017 database: Option<&[u8]>,
1018 folded: &[u8],
1019 ) -> Option<(&TableInfo, &IndexInfo)> {
1020 let wanted = database.and_then(|name| self.database_index(name));
1021 for table in &self.tables {
1022 if wanted.is_some_and(|index| index != table.database) {
1023 continue;
1024 }
1025 if let Some(index) = table.indexes.iter().find(|index| index.folded == folded) {
1026 return Some((table, index));
1027 }
1028 }
1029 None
1030 }
1031
1032 /// Returns every table of one attached database.
1033 fn tables_of(&self, database: usize) -> Vec<&TableInfo> {
1034 self.tables
1035 .iter()
1036 .filter(|table| table.database == database)
1037 .map(std::rc::Rc::as_ref)
1038 .collect()
1039 }
1040
1041 /// Returns the schema cookie of an attached database.
1042 fn schema_cookie(&self, database: usize) -> u32 {
1043 self.databases
1044 .get(database)
1045 .map_or(0, |(_, cookie)| *cookie)
1046 }
1047
1048 /// Returns the generation of the snapshot.
1049 fn generation(&self) -> u64 {
1050 self.generation
1051 }
1052}
1053
1054impl StaticCatalog {
1055 /// Returns the database indexes in the order an unqualified name searches.
1056 fn search_order(&self) -> Vec<usize> {
1057 let mut order: Vec<usize> = Vec::with_capacity(self.databases.len());
1058 if let Some(temp) = self
1059 .databases
1060 .iter()
1061 .position(|(name, _)| name.eq_ignore_ascii_case(b"temp"))
1062 {
1063 order.push(temp);
1064 }
1065 for (index, _) in self.databases.iter().enumerate() {
1066 if !order.contains(&index) {
1067 order.push(index);
1068 }
1069 }
1070 order
1071 }
1072}
1073
1074#[cfg(test)]
1075mod tests {
1076 use super::*;
1077
1078 /// Builds a one-column table for the tests below.
1079 fn table(name: &[u8], database: usize) -> TableInfo {
1080 TableInfo {
1081 name: name.to_vec(),
1082 folded: name.to_ascii_lowercase(),
1083 database,
1084 root: 2,
1085 columns: vec![ColumnInfo {
1086 name: b"a".to_vec(),
1087 folded: b"a".to_vec(),
1088 declared_type: Vec::new(),
1089 affinity: Affinity::Blob,
1090 collation: b"binary".to_vec(),
1091 not_null: false,
1092 not_null_conflict: None,
1093 primary_key_conflict: None,
1094 default_sql: None,
1095 primary_key_position: None,
1096 hidden: false,
1097 generated: false,
1098 stored: false,
1099 generated_sql: None,
1100 }],
1101 rowid_alias: None,
1102 without_rowid: false,
1103 strict: false,
1104 autoincrement: false,
1105 kind: TableKind::Table,
1106 create_sql: Vec::new(),
1107 indexes: Vec::new(),
1108 view: None,
1109 triggers: Vec::new(),
1110 analysed_rows: None,
1111 checks: Vec::new(),
1112 foreign_keys: Vec::new(),
1113 foreign_key_triggers: Vec::new(),
1114 module: None,
1115 }
1116 }
1117
1118 /// An unqualified name finds `temp` before `main`, which is the rule that
1119 /// lets a temp table shadow a real one.
1120 #[test]
1121 fn temp_is_searched_before_main() {
1122 let catalog = StaticCatalog {
1123 databases: vec![(b"main".to_vec(), 1), (b"temp".to_vec(), 2)],
1124 tables: vec![
1125 std::rc::Rc::new(table(b"t", 0)),
1126 std::rc::Rc::new(table(b"t", 1)),
1127 ],
1128 eponymous: Vec::new(),
1129 generation: 7,
1130 };
1131 let found = catalog.find_table(None, b"t").expect("it resolves");
1132 assert_eq!(found.database, 1);
1133 let qualified = catalog
1134 .find_table(Some(b"main"), b"t")
1135 .expect("it resolves");
1136 assert_eq!(qualified.database, 0);
1137 }
1138
1139 /// The three rowid spellings resolve, and a real column of that name wins.
1140 #[test]
1141 fn the_rowid_spellings_resolve_unless_shadowed() {
1142 let mut plain = table(b"t", 0);
1143 assert!(plain.is_rowid_name(b"rowid"));
1144 assert!(plain.is_rowid_name(b"_rowid_"));
1145 assert!(plain.is_rowid_name(b"oid"));
1146 assert!(!plain.is_rowid_name(b"id"));
1147
1148 if let Some(column) = plain.columns.first_mut() {
1149 column.name = b"oid".to_vec();
1150 column.folded = b"oid".to_vec();
1151 }
1152 assert!(!plain.is_rowid_name(b"oid"));
1153 assert!(plain.is_rowid_name(b"rowid"));
1154
1155 let mut without = table(b"t", 0);
1156 without.without_rowid = true;
1157 assert!(!without.is_rowid_name(b"rowid"));
1158 }
1159
1160 /// A missing database or table is `None`, never a panic.
1161 #[test]
1162 fn a_missing_name_is_none() {
1163 let catalog = StaticCatalog::empty();
1164 assert!(catalog.find_table(None, b"nope").is_none());
1165 assert!(catalog.find_table(Some(b"nodb"), b"t").is_none());
1166 assert_eq!(catalog.database_name(99), b"");
1167 assert_eq!(catalog.schema_cookie(99), 0);
1168 }
1169}