Skip to main content

turso_core/
schema.rs

1use crate::alloc::vec;
2use crate::alloc::TursoFromIterator;
3use crate::alloc::*;
4use crate::function::{Deterministic, Func, ScalarFunc};
5use crate::incremental::view::IncrementalView;
6use crate::incremental::{compiler::DBSP_CIRCUIT_VERSION, operator::create_dbsp_state_index};
7use crate::index_method::{IndexMethodAttachment, IndexMethodConfiguration};
8use crate::return_if_io;
9use crate::stats::AnalyzeStats;
10use crate::sync::RwLock;
11use crate::translate::emitter::Resolver;
12use crate::translate::expr::{
13    bind_and_rewrite_expr, walk_expr, walk_expr_mut, BindingBehavior, WalkControl,
14};
15use crate::translate::index::{resolve_index_method_parameters, resolve_sorted_columns};
16use crate::translate::planner::ROWID_STRS;
17use crate::types::{IOResult, ImmutableRecord};
18use crate::util::{exprs_are_equivalent, normalize_ident};
19use crate::vdbe::affinity::Affinity;
20use crate::vdbe::CursorID;
21use crate::{turso_assert, turso_debug_assert};
22use smallvec::SmallVec;
23use turso_macros::AtomicEnum;
24
25#[derive(Debug, Clone, AtomicEnum)]
26pub enum ViewState {
27    Ready,
28    InProgress,
29}
30
31/// Simple view structure for non-materialized views
32#[derive(Debug)]
33pub struct View {
34    pub name: String,
35    pub sql: String,
36    pub select_stmt: ast::Select,
37    pub columns: Vec<Column>,
38    pub state: AtomicViewState,
39}
40
41impl View {
42    fn new(name: String, sql: String, select_stmt: ast::Select, columns: Vec<Column>) -> Self {
43        Self {
44            name,
45            sql,
46            select_stmt,
47            columns,
48            state: AtomicViewState::new(ViewState::Ready),
49        }
50    }
51
52    pub fn process(&self) -> Result<()> {
53        let state = self.state.get();
54        match state {
55            ViewState::InProgress => {
56                bail_parse_error!("view {} is circularly defined", self.name)
57            }
58            ViewState::Ready => {
59                self.state.set(ViewState::InProgress);
60                Ok(())
61            }
62        }
63    }
64
65    pub fn done(&self) {
66        let state = self.state.get();
67        match state {
68            ViewState::InProgress => {
69                self.state.set(ViewState::Ready);
70            }
71            ViewState::Ready => {}
72        }
73    }
74}
75
76impl Clone for View {
77    fn clone(&self) -> Self {
78        Self {
79            name: self.name.clone(),
80            sql: self.sql.clone(),
81            select_stmt: self.select_stmt.clone(),
82            columns: self.columns.clone(),
83            state: AtomicViewState::new(ViewState::Ready),
84        }
85    }
86}
87
88/// Type alias for regular views collection
89pub type ViewsMap = HashMap<String, Arc<View>>;
90
91/// Trigger structure
92#[derive(Debug, Clone)]
93pub struct Trigger {
94    pub name: String,
95    pub sql: String,
96    pub table_name: String,
97    pub time: turso_parser::ast::TriggerTime,
98    pub event: turso_parser::ast::TriggerEvent,
99    pub for_each_row: bool,
100    pub when_clause: Option<turso_parser::ast::Expr>,
101    pub commands: std::vec::Vec<turso_parser::ast::TriggerCmd>,
102    pub temporary: bool,
103    /// For temp triggers that target a table in a specific database.
104    /// - `None` — the trigger was created without a db qualifier and
105    ///   targets a table in its own schema (or, if it's a temp trigger
106    ///   and no temp shadow exists, the parent schema's table).
107    /// - `Some(MAIN_DB_ID | TEMP_DB_ID | <attached_id>)` — resolved
108    ///   qualifier.
109    /// - `Some(crate::INVALID_DB_ID)` — the qualifier referenced an
110    ///   attached db name that could not be resolved at parse time
111    ///   (e.g. reloading `CREATE TEMP TRIGGER ... ON aux.x` when
112    ///   `aux` is not attached). The trigger never fires against a
113    ///   real db, which is the correct fail-safe behaviour.
114    pub target_database_id: Option<usize>,
115}
116
117impl Trigger {
118    #[allow(clippy::too_many_arguments)]
119    pub fn new(
120        name: String,
121        sql: String,
122        table_name: String,
123        time: Option<turso_parser::ast::TriggerTime>,
124        event: turso_parser::ast::TriggerEvent,
125        for_each_row: bool,
126        when_clause: Option<turso_parser::ast::Expr>,
127        commands: std::vec::Vec<turso_parser::ast::TriggerCmd>,
128        temporary: bool,
129        target_database_id: Option<usize>,
130    ) -> Self {
131        Self {
132            name,
133            sql,
134            table_name,
135            time: time.unwrap_or(turso_parser::ast::TriggerTime::Before),
136            event,
137            for_each_row,
138            when_clause,
139            commands,
140            temporary,
141            target_database_id,
142        }
143    }
144}
145
146use crate::storage::btree::{BTreeCursor, CursorTrait};
147use crate::sync::Arc;
148use crate::sync::Mutex;
149use crate::translate::collate::CollationSeq;
150use crate::translate::plan::{BitSet, ColumnMask, Plan, TableReferences};
151use crate::util::{
152    module_args_from_sql, module_name_from_sql, type_from_name, UnparsedFromSqlIndex,
153};
154use crate::Result;
155use crate::{bail_parse_error, LimboError, MvCursor, Pager, SymbolTable, ValueRef, VirtualTable};
156use bitflags::bitflags;
157use core::fmt;
158use rustc_hash::{FxBuildHasher, FxHashMap as HashMap, FxHashSet as HashSet};
159use std::collections::VecDeque;
160use std::sync::OnceLock;
161use tracing::trace;
162use turso_parser::ast::{
163    self, ColumnDefinition, Expr, InitDeferredPred, Literal, Name, RefAct, ResolveType, SortOrder,
164    TableInternalId, TypeOperator,
165};
166use turso_parser::{
167    ast::{Cmd, CreateTableBody, ResultColumn, Stmt},
168    parser::Parser,
169};
170
171pub const SCHEMA_TABLE_NAME: &str = "sqlite_schema";
172pub const SCHEMA_TABLE_NAME_ALT: &str = "sqlite_master";
173pub const TEMP_SCHEMA_TABLE_NAME: &str = "sqlite_temp_schema";
174pub const TEMP_SCHEMA_TABLE_NAME_ALT: &str = "sqlite_temp_master";
175pub const SQLITE_SEQUENCE_TABLE_NAME: &str = "sqlite_sequence";
176pub const TURSO_TYPES_TABLE_NAME: &str = "__turso_internal_types";
177pub const DBSP_TABLE_PREFIX: &str = "__turso_internal_dbsp_state_v";
178pub const TURSO_INTERNAL_PREFIX: &str = "__turso_internal_";
179pub const SEQ_BACKING_TABLE_PREFIX: &str = "__turso_internal_seq_";
180// Prefix for the hidden sequence *name* owned by an AUTOINCREMENT table.
181// This is not itself a table name. Its physical backing table is still named
182// by applying SEQ_BACKING_TABLE_PREFIX to the full sequence name.
183pub const AUTOINCREMENT_SEQ_PREFIX: &str = "__turso_internal_autoincrement_";
184
185/// Name of the hidden sequence owned by an AUTOINCREMENT table.
186pub fn autoincrement_sequence_name(table_name: &str) -> String {
187    String::from(AUTOINCREMENT_SEQ_PREFIX) + table_name
188}
189
190struct SequenceBackingTableSource {
191    sequence_name: String,
192    root_page: i64,
193    num_columns: usize,
194}
195
196struct SequenceMetadata {
197    // is_called intentionally omitted from descriptor reconstruction —
198    // the runtime watermark (including is_called) is always read from
199    // the backing-table row at nextval time, not seeded from schema.
200    start: i64,
201    increment: i64,
202    min: i64,
203    max: i64,
204    cycle: bool,
205}
206
207use crate::util::quote_identifier as quote_ident;
208
209/// Recursively rewrite `Expr::Id("value")` (case-insensitive) to `Expr::Id(col_name)`.
210pub fn rewrite_value_to_column(expr: &ast::Expr, col_name: &str) -> Box<ast::Expr> {
211    let mut cloned = expr.clone();
212    let _ = walk_expr_mut(&mut cloned, &mut |e| {
213        if let ast::Expr::Id(name) = e {
214            if name.as_str().eq_ignore_ascii_case("value") {
215                *e = ast::Expr::Id(ast::Name::exact(col_name.to_string()));
216            }
217        }
218        Ok(WalkControl::Continue)
219    });
220    Box::new(cloned)
221}
222
223/// Field definition within a StructDef.
224#[derive(Debug, Clone)]
225pub struct StructFieldDef {
226    pub name: String,
227    pub base_affinity: Affinity,
228    pub type_name: String,
229}
230
231/// Definition for a STRUCT composite type.
232#[derive(Debug, Clone)]
233pub struct StructDef {
234    pub fields: Vec<StructFieldDef>,
235}
236
237/// Variant definition within a UnionDef.
238#[derive(Debug, Clone)]
239pub struct UnionVariantDef {
240    pub tag_name: String,
241    pub tag_index: u8,
242    pub base_affinity: Affinity,
243    pub type_name: String,
244}
245
246/// Definition for a UNION discriminated union type.
247#[derive(Debug, Clone)]
248pub struct UnionDef {
249    pub variants: Vec<UnionVariantDef>,
250    /// Cached variant tag names for `UnionTag` instructions.
251    /// Built once at type registration time so we don't rebuild per-instruction.
252    pub tag_names: Arc<[String]>,
253}
254
255/// The kind-specific payload of a custom type.
256#[derive(Debug, Clone)]
257pub enum TypeDefKind {
258    Custom {
259        params: std::vec::Vec<ast::TypeParam>,
260        base: String,
261        encode: Option<Box<ast::Expr>>,
262        decode: Option<Box<ast::Expr>>,
263        operators: std::vec::Vec<TypeOperator>,
264        default: Option<Box<ast::Expr>>,
265    },
266    Struct(StructDef),
267    Union(UnionDef),
268}
269
270/// Custom type definition, loaded from sqlite_turso_types
271#[derive(Debug, Clone)]
272/// A fully-resolved custom type: the chain of TypeDefs from the named type
273/// up to the ultimate primitive, plus the primitive name itself.
274pub struct ResolvedType {
275    /// The ultimate primitive type name (e.g., "integer", "text", "blob").
276    pub primitive: String,
277    /// TypeDefs from child (the named type) to ancestor (closest to primitive).
278    pub chain: Vec<Arc<TypeDef>>,
279}
280
281impl ResolvedType {
282    /// The leaf (directly named) type definition.
283    pub fn leaf(&self) -> &TypeDef {
284        &self.chain[0]
285    }
286
287    /// Whether the leaf type is a domain.
288    pub fn is_domain(&self) -> bool {
289        self.chain[0].is_domain
290    }
291
292    /// Find the first DEFAULT expression in the type chain (child first, then ancestors).
293    /// Matches PostgreSQL: a child domain inherits the parent's DEFAULT when it
294    /// doesn't declare its own.
295    pub fn default_expr(&self) -> Option<&ast::Expr> {
296        self.chain.iter().find_map(|td| td.default_expr())
297    }
298}
299
300#[derive(Debug, Clone)]
301pub struct TypeDef {
302    pub name: String,
303    pub is_builtin: bool,
304    pub not_null: bool,
305    /// Whether this is a domain (CREATE DOMAIN) vs a custom type (CREATE TYPE).
306    pub is_domain: bool,
307    /// Original SQL for round-trip persistence. Stored verbatim from creation.
308    pub sql: String,
309    /// CHECK constraints from CREATE DOMAIN, stored as first-class data.
310    /// Empty for regular CREATE TYPE definitions.
311    pub domain_checks: std::vec::Vec<ast::DomainConstraint>,
312    pub kind: TypeDefKind,
313}
314
315impl TypeDef {
316    /// Returns true if this is a STRUCT type.
317    pub fn is_struct(&self) -> bool {
318        matches!(self.kind, TypeDefKind::Struct(_))
319    }
320
321    /// Returns true if this is a UNION type.
322    pub fn is_union(&self) -> bool {
323        matches!(self.kind, TypeDefKind::Union(_))
324    }
325
326    /// Returns the StructDef if this is a STRUCT type.
327    pub fn struct_def(&self) -> Option<&StructDef> {
328        match &self.kind {
329            TypeDefKind::Struct(sd) => Some(sd),
330            _ => None,
331        }
332    }
333
334    /// Returns the UnionDef if this is a UNION type.
335    pub fn union_def(&self) -> Option<&UnionDef> {
336        match &self.kind {
337            TypeDefKind::Union(ud) => Some(ud),
338            _ => None,
339        }
340    }
341
342    /// Returns the encode expression (Custom types only).
343    pub fn encode(&self) -> Option<&ast::Expr> {
344        match &self.kind {
345            TypeDefKind::Custom { encode, .. } => encode.as_deref(),
346            _ => None,
347        }
348    }
349
350    /// Returns the decode expression (Custom types only).
351    pub fn decode(&self) -> Option<&ast::Expr> {
352        match &self.kind {
353            TypeDefKind::Custom { decode, .. } => decode.as_deref(),
354            _ => None,
355        }
356    }
357
358    /// Returns the base type name.
359    pub fn base(&self) -> &str {
360        match &self.kind {
361            TypeDefKind::Custom { base, .. } => base,
362            TypeDefKind::Struct(_) | TypeDefKind::Union(_) => "blob",
363        }
364    }
365
366    /// Returns the params (Custom types only, empty for Struct/Union).
367    pub fn params(&self) -> &[ast::TypeParam] {
368        match &self.kind {
369            TypeDefKind::Custom { params, .. } => params,
370            _ => &[],
371        }
372    }
373
374    /// Returns the operators (Custom types only, empty for Struct/Union).
375    pub fn operators(&self) -> &[TypeOperator] {
376        match &self.kind {
377            TypeDefKind::Custom { operators, .. } => operators,
378            _ => &[],
379        }
380    }
381
382    /// Returns the default expression (Custom types only).
383    pub fn default_expr(&self) -> Option<&ast::Expr> {
384        match &self.kind {
385            TypeDefKind::Custom { default, .. } => default.as_deref(),
386            _ => None,
387        }
388    }
389
390    /// Find a struct field by name. Returns (field_index, &StructFieldDef).
391    pub fn find_struct_field(&self, name: &str) -> Option<(usize, &StructFieldDef)> {
392        self.struct_def().and_then(|sd| {
393            sd.fields
394                .iter()
395                .enumerate()
396                .find(|(_, f)| f.name.eq_ignore_ascii_case(name))
397        })
398    }
399
400    /// Resolve a tag name to its numeric index within this union type.
401    /// Returns None if this is not a union or the variant doesn't exist.
402    pub fn resolve_union_tag_index(&self, tag_name: &str) -> Option<u8> {
403        self.find_union_variant(tag_name).map(|(idx, _)| idx)
404    }
405
406    /// Find a union variant by tag name. Returns (tag_index, &UnionVariantDef).
407    pub fn find_union_variant(&self, name: &str) -> Option<(u8, &UnionVariantDef)> {
408        self.union_def().and_then(|ud| {
409            ud.variants
410                .iter()
411                .find(|v| v.tag_name.eq_ignore_ascii_case(name))
412                .map(|v| (v.tag_index, v))
413        })
414    }
415
416    /// Construct a TypeDef from a parsed CREATE TYPE statement.
417    pub fn from_create_type(
418        type_name: &str,
419        body: &ast::CreateTypeBody,
420        is_builtin: bool,
421        sql: String,
422    ) -> crate::Result<Self> {
423        Ok(match body {
424            ast::CreateTypeBody::CustomType {
425                params,
426                base,
427                encode,
428                decode,
429                operators,
430                default,
431            } => Self {
432                name: type_name.to_string(),
433                is_builtin,
434                not_null: false,
435                is_domain: false,
436                sql,
437                domain_checks: std::vec::Vec::new(),
438                kind: TypeDefKind::Custom {
439                    params: params.clone(),
440                    base: base.clone(),
441                    encode: encode.clone(),
442                    decode: decode.clone(),
443                    operators: operators.clone(),
444                    default: default.clone(),
445                },
446            },
447            ast::CreateTypeBody::Struct(fields) => {
448                let struct_fields: Vec<StructFieldDef> = fields
449                    .iter()
450                    .map(|f| StructFieldDef {
451                        name: f.name.to_string(),
452                        base_affinity: Affinity::affinity(&f.field_type.name),
453                        type_name: f.field_type.name.clone(),
454                    })
455                    .try_collect()?;
456                Self {
457                    name: type_name.to_string(),
458                    is_builtin,
459                    not_null: false,
460                    is_domain: false,
461                    sql,
462                    domain_checks: std::vec::Vec::new(),
463                    kind: TypeDefKind::Struct(StructDef {
464                        fields: struct_fields,
465                    }),
466                }
467            }
468            ast::CreateTypeBody::Union(fields) => {
469                if fields.len() > 256 {
470                    return Err(crate::LimboError::ParseError(format!(
471                        "UNION type cannot have more than 256 variants (got {})",
472                        fields.len()
473                    )));
474                }
475                let variants: Vec<UnionVariantDef> = fields
476                    .iter()
477                    .enumerate()
478                    .map(|(i, f)| UnionVariantDef {
479                        tag_name: f.name.to_string(),
480                        tag_index: i as u8,
481                        base_affinity: Affinity::affinity(&f.field_type.name),
482                        type_name: f.field_type.name.clone(),
483                    })
484                    .try_collect()?;
485                Self {
486                    name: type_name.to_string(),
487                    is_builtin,
488                    not_null: false,
489                    is_domain: false,
490                    sql,
491                    domain_checks: std::vec::Vec::new(),
492                    kind: TypeDefKind::Union(UnionDef {
493                        // Arc<[T]> is a shared-pointer boundary: collect directly,
494                        // skipping the intermediate allocator Vec.
495                        tag_names: variants.iter().map(|v| v.tag_name.clone()).collect(),
496                        variants,
497                    }),
498                }
499            }
500        })
501    }
502
503    /// Construct a TypeDef from a parsed CREATE DOMAIN statement.
504    /// Stores constraints as first-class data for propagation to table CHECK constraints.
505    pub fn from_domain(
506        domain_name: &str,
507        base_type: &str,
508        not_null: bool,
509        constraints: &[ast::DomainConstraint],
510        default: Option<Box<ast::Expr>>,
511        sql: String,
512    ) -> Self {
513        Self {
514            name: domain_name.to_string(),
515            is_builtin: false,
516            not_null,
517            is_domain: true,
518            sql,
519            domain_checks: constraints.to_vec(),
520            kind: TypeDefKind::Custom {
521                params: std::vec::Vec::new(),
522                base: base_type.to_string(),
523                encode: None,
524                decode: None,
525                operators: std::vec::Vec::new(),
526                default,
527            },
528        }
529    }
530
531    /// The expected input type for `value` in this custom type.
532    /// Looks for a `value` parameter with a type annotation.
533    /// Falls back to base type if `value` is not declared.
534    pub fn value_input_type(&self) -> &str {
535        for p in self.params() {
536            if p.name.eq_ignore_ascii_case("value") {
537                return p.ty.as_deref().unwrap_or_else(|| self.base());
538            }
539        }
540        self.base()
541    }
542
543    /// The non-value params (user-provided at column declaration time).
544    pub fn user_params(&self) -> impl Iterator<Item = &turso_parser::ast::TypeParam> {
545        self.params()
546            .iter()
547            .filter(|p| !p.name.eq_ignore_ascii_case("value"))
548    }
549
550    /// Returns the original SQL used to create this type or domain.
551    pub fn to_sql(&self) -> &str {
552        &self.sql
553    }
554}
555
556/// Accumulators for schema loading - kept separate to avoid moving through state variants
557struct MakeFromBtreeAccumulators {
558    from_sql_indexes: Vec<UnparsedFromSqlIndex>,
559    automatic_indices: HashMap<String, Vec<(String, i64)>>,
560    /// Store DBSP state table root pages: view_name -> dbsp_state_root_page
561    dbsp_state_roots: HashMap<String, i64>,
562    /// Store DBSP state table index root pages: view_name -> dbsp_state_index_root_page
563    dbsp_state_index_roots: HashMap<String, i64>,
564    /// Store materialized view info (SQL and root page) for later creation
565    materialized_view_info: HashMap<String, (String, i64)>,
566}
567
568/// Phase tracking for async schema loading
569#[derive(Default, Debug)]
570pub enum MakeFromBtreePhase {
571    #[default]
572    Init,
573    Rewinding,
574    FetchingRecord,
575    Advancing,
576    /// After the sqlite_schema scan completes we walk each sequence's
577    /// backing table to reconstruct its descriptor (start / inc / min /
578    /// max / cycle). These two phases drive that scan via the
579    /// `sequence_cursor` field on the state, yielding `IOResult::IO` on
580    /// each cursor I/O — the previous implementation called the
581    /// synchronous `populate_sequences(pager)` helper at the EOF of
582    /// `FetchingRecord`, which blocked the pager inside an async state
583    /// machine (and inside whatever vdbe-level state machine was
584    /// driving the schema reparse).
585    PopulatingSequencesRewind,
586    PopulatingSequencesFetch,
587    Done,
588}
589
590/// State machine for async schema loading - passed by caller, not stored on Schema
591pub struct MakeFromBtreeState {
592    phase: MakeFromBtreePhase,
593    cursor: Option<BTreeCursor>,
594    accumulators: Option<MakeFromBtreeAccumulators>,
595    read_tx_active: bool,
596    /// Backing tables left to walk during the
597    /// `PopulatingSequencesRewind`/`PopulatingSequencesFetch` phases.
598    sequence_sources: Vec<SequenceBackingTableSource>,
599    /// Cursor for the source currently being scanned (the back of
600    /// `sequence_sources` is popped onto this when entering Rewind).
601    sequence_cursor: Option<BTreeCursor>,
602}
603
604impl Default for MakeFromBtreeState {
605    fn default() -> Self {
606        Self::new()
607    }
608}
609
610impl MakeFromBtreeState {
611    pub fn new() -> Self {
612        Self {
613            phase: MakeFromBtreePhase::Init,
614            cursor: None,
615            accumulators: None,
616            read_tx_active: false,
617            sequence_sources: vec![],
618            sequence_cursor: None,
619        }
620    }
621
622    /// Cleanup on error - ensures end_read_tx is called
623    pub fn cleanup(&mut self, pager: &Pager) {
624        if self.read_tx_active {
625            pager.end_read_tx();
626            self.read_tx_active = false;
627        }
628        self.cursor = None;
629        self.accumulators = None;
630    }
631}
632
633/// Used to refer to the implicit rowid column in tables without an alias during UPDATE
634pub const ROWID_SENTINEL: usize = usize::MAX;
635
636/// The Position in Table for indexes which are arbitrary expressions (index.expr.is_some())
637pub const EXPR_INDEX_SENTINEL: usize = usize::MAX;
638
639/// Internal table prefixes that should be protected from CREATE/DROP
640pub const RESERVED_TABLE_PREFIXES: [&str; 2] = ["sqlite_", "__turso_internal_"];
641
642/// Check if a table name refers to a system table that should be protected from direct writes
643pub fn is_system_table(table_name: &str) -> bool {
644    RESERVED_TABLE_PREFIXES
645        .iter()
646        .any(|prefix| table_name.to_lowercase().starts_with(prefix))
647}
648
649pub fn allow_user_dml(table_name: &str) -> bool {
650    const NAMES: [&str; 2] = [SCHEMA_TABLE_NAME, SCHEMA_TABLE_NAME_ALT];
651    !(NAMES.iter().any(|n| n.eq_ignore_ascii_case(table_name))
652        || table_name.starts_with(TURSO_INTERNAL_PREFIX)) // internal name wouldn't be uppercase
653}
654
655// Sequence persistence design
656// ===========================
657//
658// Every sequence — user-created (CREATE SEQUENCE) and implicit
659// (AUTOINCREMENT) — is backed by a B-tree table
660// `__turso_internal_seq_<name>` with schema (value INTEGER PRIMARY KEY,
661// is_called, start, inc, min, max, cycle). The runtime watermark IS the
662// disk state: there is no in-memory counter. Every nextval/setval reads
663// the current watermark row inside the executing transaction, computes
664// the new value, and writes it back — nextval INSERTs a new row;
665// setval DELETEs every row then INSERTs one at the requested value.
666//
667// At commit time the backing table is compacted to one row at MAX(value)
668// for ascending sequences or MIN(value) for descending. AUTOINCREMENT
669// sequences additionally mirror their watermark into `sqlite_sequence` so
670// the high-water mark is readable by SQLite-compatible tools.
671//
672// Rollback semantics fall out of bundling the backing-table writes with
673// the user's transaction:
674//   * Commit → the sequence advance is on disk.
675//   * Rollback → the sequence advance is not on disk.
676// A value emitted only by rolled-back transactions may be re-emitted by a
677// later nextval — there is no allocator state retained outside the
678// committed row. This matches SQLite AUTOINCREMENT's behavior and does
679// not match PostgreSQL's "permanently burned" semantics; consumers
680// needing globally unique ids should pair nextval with an INSERT in the
681// same transaction.
682//
683// Cross-process correctness comes for free from the disk-only model:
684// under WAL the write lock serializes processes so the next holder
685// observes the latest committed watermark.
686/// Schema descriptor for a sequence. Pure data — the runtime state lives
687/// in the backing table `__turso_internal_seq_<name>` and is read from
688/// disk by `Insn::SequenceComputeNext` + surrounding cursor bytecode on
689/// every nextval/setval call. See `core/translate/sequence.rs` and the
690/// disk-only design notes above.
691///
692/// `Clone` is implemented so `Arc::make_mut` can in-place edit the `name`
693/// field during `ALTER TABLE … RENAME TO …` on an AUTOINCREMENT table —
694/// keeping the sequence's identity in sync with the parent table's new
695/// name without forcing a schema reparse.
696#[derive(Debug, Clone)]
697pub struct Sequence {
698    pub name: String,
699    pub start_value: i64,
700    pub increment_by: i64,
701    pub min_value: i64,
702    pub max_value: i64,
703    pub cycle: bool,
704}
705
706impl Sequence {
707    pub fn new(
708        name: String,
709        start: Option<i64>,
710        increment: Option<i64>,
711        min_value: Option<i64>,
712        max_value: Option<i64>,
713        cycle: bool,
714    ) -> crate::Result<Self> {
715        let increment_by = increment.unwrap_or(1);
716        if increment_by == 0 {
717            return Err(crate::LimboError::ParseError(
718                "INCREMENT must not be zero".to_string(),
719            ));
720        }
721        let min_val = min_value.unwrap_or(if increment_by > 0 { 1 } else { i64::MIN });
722        let max_val = max_value.unwrap_or(if increment_by > 0 { i64::MAX } else { -1 });
723        if min_val >= max_val {
724            return Err(crate::LimboError::ParseError(format!(
725                "MINVALUE ({min_val}) must be less than MAXVALUE ({max_val})"
726            )));
727        }
728        let start_val = start.unwrap_or(if increment_by > 0 { min_val } else { max_val });
729        if start_val < min_val {
730            return Err(crate::LimboError::ParseError(format!(
731                "START value ({start_val}) cannot be less than MINVALUE ({min_val})"
732            )));
733        }
734        if start_val > max_val {
735            return Err(crate::LimboError::ParseError(format!(
736                "START value ({start_val}) cannot be greater than MAXVALUE ({max_val})"
737            )));
738        }
739        Ok(Self {
740            name,
741            start_value: start_val,
742            increment_by,
743            min_value: min_val,
744            max_value: max_val,
745            cycle,
746        })
747    }
748}
749
750/// Type of schema object for conflict checking
751#[derive(Debug, Clone, Copy, PartialEq, Eq)]
752pub enum SchemaObjectType {
753    Table,
754    View,
755    Index,
756}
757
758#[derive(Debug)]
759pub struct Schema {
760    pub tables: HashMap<String, Arc<Table>>,
761    #[cfg(feature = "conn_raw_api")]
762    pub(crate) table_names_by_root_page: HashMap<i64, String>,
763
764    /// Track which tables are actually materialized views
765    pub materialized_view_names: HashSet<String>,
766    /// Store original SQL for materialized views (for .schema command)
767    pub materialized_view_sql: HashMap<String, String>,
768    /// The incremental view objects (DBSP circuits)
769    pub incremental_views: HashMap<String, Arc<Mutex<IncrementalView>>>,
770
771    pub views: ViewsMap,
772
773    /// table_name to list of triggers
774    pub triggers: HashMap<String, VecDeque<Arc<Trigger>>>,
775
776    /// table_name to list of indexes for the table
777    pub indexes: HashMap<String, VecDeque<Arc<Index>>>,
778    pub has_indexes: HashSet<String>,
779    pub schema_version: u32,
780    /// Statistics collected via ANALYZE for regular B-tree tables and indexes.
781    pub analyze_stats: AnalyzeStats,
782
783    /// Mapping from table names to the materialized views that depend on them
784    pub table_to_materialized_views: HashMap<String, Vec<String>>,
785
786    /// Track views that exist but have incompatible versions
787    pub incompatible_views: HashSet<String>,
788
789    /// View rows in sqlite_schema whose stored SQL failed to parse (e.g.
790    /// older versions wrote view column lists without identifier quoting).
791    /// The rows are tolerated at load time so the database stays usable;
792    /// tracking the names lets DROP VIEW remove them.
793    pub broken_views: HashSet<String>,
794
795    /// Root pages of tables/indexes that have been dropped but not yet checkpointed.
796    /// In MVCC mode, when a table is dropped, the btree pages are not freed until checkpoint.
797    /// integrity_check needs to know about these pages to avoid false positives about "page never used".
798    pub dropped_root_pages: HashSet<i64>,
799
800    /// Custom type registry, loaded from sqlite_turso_types
801    pub type_registry: HashMap<String, Arc<TypeDef>>,
802
803    pub generated_columns_enabled: bool,
804    /// Named sequences (CREATE SEQUENCE)
805    pub sequences: HashMap<String, Arc<Sequence>>,
806}
807
808impl Default for Schema {
809    fn default() -> Self {
810        Self::new()
811    }
812}
813
814fn bootstrap_builtin_types(registry: &mut HashMap<String, Arc<TypeDef>>) -> crate::Result<()> {
815    use turso_parser::ast::{Cmd, Stmt};
816    use turso_parser::parser::Parser;
817
818    let type_sqls: &[&str] = &[
819        #[cfg(feature = "uuid")]
820        "CREATE TYPE uuid(value text) BASE blob ENCODE uuid_blob(value) DECODE uuid_str(value) DEFAULT uuid4_str() OPERATOR '<'",
821        "CREATE TYPE boolean(value any) BASE integer ENCODE boolean_to_int(value) DECODE CASE WHEN value THEN 1 ELSE 0 END OPERATOR '<'",
822        #[cfg(feature = "json")]
823        "CREATE TYPE json(value text) BASE text ENCODE json(value) DECODE value",
824        #[cfg(feature = "json")]
825        "CREATE TYPE jsonb(value text) BASE blob ENCODE jsonb(value) DECODE json(value)",
826        "CREATE TYPE varchar(value text, maxlen integer) BASE text ENCODE CASE WHEN length(value) <= maxlen THEN value ELSE RAISE(ABORT, 'value too long for varchar') END DECODE value OPERATOR '<'",
827        "CREATE TYPE date(value text) BASE text ENCODE CASE WHEN value IS NULL THEN NULL WHEN date(value) IS NULL THEN RAISE(ABORT, 'invalid date value') ELSE date(value) END DECODE value OPERATOR '<'",
828        // ENCODE preserves sub-second precision through strftime + a rtrim pair
829        // that strips trailing zeros and the dangling dot, matching PostgreSQL's
830        // text format: whole seconds render as `HH:MM:SS` (no .000), trailing
831        // zeros are dropped (`.500` -> `.5`), and the dot is removed when no
832        // fractional digits remain. `time(...)` / `datetime(...)` would truncate
833        // the fraction outright, silently dropping precision on insert.
834        // Caveat: Turso's `%f` directive is millisecond resolution, so PG's
835        // microsecond inputs are clamped to 3 digits (`.123456` -> `.123`).
836        "CREATE TYPE time(value text) BASE text ENCODE CASE WHEN value IS NULL THEN NULL WHEN time(value) IS NULL THEN RAISE(ABORT, 'invalid time value') ELSE rtrim(rtrim(strftime('%H:%M:%f', value), '0'), '.') END DECODE value OPERATOR '<'",
837        "CREATE TYPE timestamp(value text) BASE text ENCODE CASE WHEN value IS NULL THEN NULL WHEN datetime(value) IS NULL THEN RAISE(ABORT, 'invalid timestamp value') ELSE rtrim(rtrim(strftime('%Y-%m-%d %H:%M:%f', value), '0'), '.') END DECODE value OPERATOR '<'",
838        "CREATE TYPE smallint(value integer) BASE integer ENCODE CASE WHEN value BETWEEN -32768 AND 32767 THEN value ELSE RAISE(ABORT, 'integer out of range for smallint') END DECODE value OPERATOR '<'",
839        "CREATE TYPE bigint(value integer) BASE integer",
840        "CREATE TYPE inet(value text) BASE text ENCODE validate_ipaddr(value) DECODE value",
841        "CREATE TYPE bytea(value blob) BASE blob OPERATOR '<'",
842        "CREATE TYPE numeric(value any, precision integer, scale integer) BASE blob ENCODE numeric_encode(value, precision, scale) DECODE numeric_decode(value) OPERATOR '+' numeric_add OPERATOR '-' numeric_sub OPERATOR '*' numeric_mul OPERATOR '/' numeric_div OPERATOR '<' numeric_lt OPERATOR '=' numeric_eq",
843    ];
844
845    for sql in type_sqls {
846        let mut parser = Parser::new(sql.as_bytes());
847        let Ok(Some(Cmd::Stmt(Stmt::CreateType {
848            type_name, body, ..
849        }))) = parser.next_cmd()
850        else {
851            return Err(crate::LimboError::InternalError(format!(
852                "failed to parse built-in type SQL: {sql}"
853            )));
854        };
855
856        let type_def = TypeDef::from_create_type(&type_name, &body, true, sql.to_string())?;
857        registry.insert(type_name.to_lowercase(), Arc::new(type_def));
858    }
859
860    // Register aliases
861    let aliases: &[(&str, &str)] = &[
862        ("bool", "boolean"),
863        ("int2", "smallint"),
864        ("int8", "bigint"),
865    ];
866    for (alias, target) in aliases {
867        if let Some(type_def) = registry.get(*target).cloned() {
868            registry.insert(alias.to_string(), type_def);
869        }
870    }
871    Ok(())
872}
873
874impl Schema {
875    fn normalize_table_lookup_name(&self, name: &str) -> String {
876        let name = normalize_ident(name);
877        if name.eq(SCHEMA_TABLE_NAME_ALT)
878            || name.eq(TEMP_SCHEMA_TABLE_NAME)
879            || name.eq(TEMP_SCHEMA_TABLE_NAME_ALT)
880        {
881            SCHEMA_TABLE_NAME.to_string()
882        } else {
883            name
884        }
885    }
886
887    /// Create a schema with custom types enabled.
888    ///
889    /// Panics if a hardcoded built-in type definition is malformed (programmer
890    /// bug). Production code that opens user databases should prefer
891    /// [`Schema::with_options`] which returns `Result`.
892    pub fn new() -> Self {
893        Self::with_options(true).expect("built-in type definitions are malformed")
894    }
895
896    pub fn with_options(enable_custom_types: bool) -> crate::Result<Self> {
897        let mut tables: HashMap<String, Arc<Table>> = HashMap::default();
898        #[cfg(feature = "conn_raw_api")]
899        let mut table_names_by_root_page = HashMap::default();
900        let has_indexes = HashSet::default();
901        let indexes: HashMap<String, VecDeque<Arc<Index>>> = HashMap::default();
902        #[allow(clippy::arc_with_non_send_sync)]
903        tables.insert(
904            SCHEMA_TABLE_NAME.to_string(),
905            Arc::new(Table::BTree(sqlite_schema_table()?.into())),
906        );
907        #[cfg(feature = "conn_raw_api")]
908        table_names_by_root_page.insert(1, SCHEMA_TABLE_NAME.to_string());
909        let materialized_view_names = HashSet::default();
910        let materialized_view_sql = HashMap::default();
911        let incremental_views = HashMap::default();
912        let views: ViewsMap = HashMap::default();
913        let triggers = HashMap::default();
914        let table_to_materialized_views: HashMap<String, Vec<String>> = HashMap::default();
915        let incompatible_views = HashSet::default();
916        let mut type_registry = HashMap::default();
917        if enable_custom_types {
918            bootstrap_builtin_types(&mut type_registry)?;
919        }
920        let mut schema = Self {
921            tables,
922            #[cfg(feature = "conn_raw_api")]
923            table_names_by_root_page,
924            materialized_view_names,
925            materialized_view_sql,
926            incremental_views,
927            views,
928            triggers,
929            indexes,
930            has_indexes,
931            schema_version: 0,
932            analyze_stats: AnalyzeStats::default(),
933            table_to_materialized_views,
934            incompatible_views,
935            broken_views: HashSet::default(),
936            dropped_root_pages: HashSet::default(),
937            type_registry,
938            generated_columns_enabled: false,
939            sequences: HashMap::default(),
940        };
941        crate::dialect::sqlite::register_builtin_catalog(&mut schema, enable_custom_types)?;
942        Ok(schema)
943    }
944
945    /// Add an `InternalVirtualTable` to the schema's catalog. The wrapped
946    /// table appears under the name returned by its `name()` method and is
947    /// queryable like any other table. Returns the name actually inserted.
948    ///
949    /// Intended for callers that want to surface state as a queryable table
950    /// without going through `CREATE VIRTUAL TABLE` — for example, extensions
951    /// that contribute metadata tables or alternative-dialect catalogs.
952    pub fn register_internal_vtab<T>(&mut self, table: T) -> crate::Result<String>
953    where
954        T: crate::vtab::InternalVirtualTable + 'static,
955    {
956        let vtab = crate::vtab::VirtualTable::wrap_internal_table(table)?;
957        let name = vtab.name.clone();
958        let lookup_name = normalize_ident(&name);
959        self.tables.insert(
960            lookup_name,
961            Arc::new(Table::Virtual(Arc::new((*vtab).clone()))),
962        );
963        Ok(name)
964    }
965
966    /// Look up a custom type definition by name.
967    /// Custom types are only valid on STRICT tables; pass `is_strict` from the
968    /// owning table so that non-STRICT tables never resolve a custom type.
969    pub fn get_type_def(&self, type_name: &str, is_strict: bool) -> Option<&Arc<TypeDef>> {
970        if !is_strict {
971            return None;
972        }
973        self.type_registry.get(&type_name.to_lowercase())
974    }
975
976    /// Look up a custom type definition by name without a strictness check.
977    /// Only use this for operations that aren't column-scoped (e.g. DROP TYPE,
978    /// CREATE TABLE validation, CAST).
979    pub fn get_type_def_unchecked(&self, type_name: &str) -> Option<&Arc<TypeDef>> {
980        self.type_registry.get(&type_name.to_lowercase())
981    }
982
983    /// Resolve a custom type fully: look it up (with strictness gate) and chase
984    /// the base-type chain to the ultimate primitive.
985    /// Returns `Ok(None)` if the type is not registered (or the table isn't strict).
986    pub fn resolve_type(
987        &self,
988        type_name: &str,
989        is_strict: bool,
990    ) -> crate::Result<Option<ResolvedType>> {
991        if !is_strict {
992            return Ok(None);
993        }
994        self.resolve_type_unchecked(type_name)
995    }
996
997    /// Resolve a custom type fully without a strictness check.
998    /// Returns `Ok(None)` if the type is not in the registry.
999    pub fn resolve_type_unchecked(&self, type_name: &str) -> crate::Result<Option<ResolvedType>> {
1000        let key = type_name.to_lowercase();
1001        if !self.type_registry.contains_key(&key) {
1002            return Ok(None);
1003        }
1004        let (primitive, chain) = self.resolve_base_type_chain(type_name)?;
1005        Ok(Some(ResolvedType { primitive, chain }))
1006    }
1007
1008    pub fn remove_type(&mut self, type_name: &str) {
1009        self.type_registry.remove(&type_name.to_lowercase());
1010    }
1011
1012    /// Chase the base type chain: domain_a → domain_b → integer
1013    /// Returns (ultimate_primitive, ordered_chain_of_TypeDefs)
1014    /// The chain is ordered from child to ancestor.
1015    /// Errors on cycles or missing intermediate types.
1016    pub fn resolve_base_type_chain(
1017        &self,
1018        type_name: &str,
1019    ) -> crate::Result<(String, Vec<Arc<TypeDef>>)> {
1020        let mut chain = vec![];
1021        let mut visited = std::collections::HashSet::new();
1022        let mut current = type_name.to_lowercase();
1023
1024        loop {
1025            if !visited.insert(current.clone()) {
1026                return Err(crate::LimboError::ParseError(format!(
1027                    "circular type dependency detected: {current}"
1028                )));
1029            }
1030            match self.type_registry.get(&current) {
1031                Some(td) => {
1032                    chain.try_push(Arc::clone(td))?;
1033                    current = td.base().to_lowercase();
1034                }
1035                None => {
1036                    // current is not in the registry — it's a primitive
1037                    return Ok((current, chain));
1038                }
1039            }
1040        }
1041    }
1042
1043    /// Parse a CREATE TYPE SQL string and add the type to the in-memory registry.
1044    pub fn add_type_from_sql(&mut self, sql: &str) -> crate::Result<()> {
1045        use turso_parser::ast::{Cmd, Stmt};
1046        use turso_parser::parser::Parser;
1047
1048        let mut parser = Parser::new(sql.as_bytes());
1049        let cmd = parser.next_cmd();
1050        match cmd {
1051            Ok(Some(Cmd::Stmt(Stmt::CreateType {
1052                type_name, body, ..
1053            }))) => {
1054                let type_def =
1055                    TypeDef::from_create_type(&type_name, &body, false, sql.to_string())?;
1056                self.type_registry
1057                    .insert(type_name.to_lowercase(), Arc::new(type_def));
1058            }
1059            Ok(Some(Cmd::Stmt(Stmt::CreateDomain {
1060                domain_name,
1061                base_type,
1062                default,
1063                not_null,
1064                constraints,
1065                ..
1066            }))) => {
1067                let type_def = TypeDef::from_domain(
1068                    &domain_name,
1069                    &base_type,
1070                    not_null,
1071                    &constraints,
1072                    default,
1073                    sql.to_string(),
1074                );
1075                self.type_registry
1076                    .insert(domain_name.to_lowercase(), Arc::new(type_def));
1077            }
1078            _ => {
1079                return Err(crate::LimboError::ParseError(format!(
1080                    "invalid type sql: {sql}"
1081                )));
1082            }
1083        }
1084        Ok(())
1085    }
1086
1087    /// Load type definitions from CREATE TYPE SQL strings and resolve custom
1088    /// type affinities on all STRICT tables. This is the shared entry point
1089    /// used by both initial database open and schema reparse.
1090    pub fn load_type_definitions(&mut self, type_sqls: &[String]) -> crate::Result<()> {
1091        for sql in type_sqls {
1092            self.add_type_from_sql(sql)?;
1093        }
1094        self.resolve_all_custom_type_affinities()?;
1095        Ok(())
1096    }
1097
1098    /// Resolve custom type affinities for all STRICT tables in the schema.
1099    /// Call this after loading user-defined types from __turso_internal_types
1100    /// so that columns declared with custom types use the BASE type's affinity.
1101    pub fn resolve_all_custom_type_affinities(&mut self) -> Result<()> {
1102        let mut tables: SmallVec<[(String, Arc<Table>); 8]> = SmallVec::with_capacity(8);
1103        for (name, table) in self.tables.iter().filter(|(_, t)| {
1104            t.is_strict()
1105                && t.btree().is_some_and(|bt| {
1106                    bt.columns
1107                        .iter()
1108                        .any(|c| self.get_type_def_unchecked(&c.ty_str).is_some())
1109                })
1110        }) {
1111            let bt = table.btree().expect("checked btree table");
1112            let mut modified = (*bt).clone();
1113            modified.resolve_custom_type_affinities(self);
1114            modified.propagate_domain_constraints(self)?;
1115            tables.push((name.clone(), Arc::new(Table::BTree(Arc::new(modified)))));
1116        }
1117        for (name, table) in tables {
1118            self.tables.insert(name, table);
1119        }
1120        Ok(())
1121    }
1122
1123    pub fn is_unique_idx_name(&self, name: &str) -> bool {
1124        !self
1125            .indexes
1126            .iter()
1127            .any(|idx| idx.1.iter().any(|i| i.name == name))
1128    }
1129
1130    pub fn add_materialized_view(&mut self, view: IncrementalView, table: Arc<Table>, sql: String) {
1131        let name = normalize_ident(view.name());
1132
1133        // Add to tables (so it appears as a regular table)
1134        #[cfg(feature = "conn_raw_api")]
1135        self.register_table_root_page(&name, table.as_ref());
1136        self.tables.insert(name.clone(), table);
1137
1138        // Track that this is a materialized view
1139        self.materialized_view_names.insert(name.clone());
1140        self.materialized_view_sql.insert(name.clone(), sql);
1141
1142        // Store the incremental view (DBSP circuit)
1143        self.incremental_views
1144            .insert(name, Arc::new(Mutex::new(view)));
1145    }
1146
1147    pub fn get_materialized_view(&self, name: &str) -> Option<Arc<Mutex<IncrementalView>>> {
1148        let name = normalize_ident(name);
1149        self.incremental_views.get(&name).cloned()
1150    }
1151
1152    /// Check if DBSP state table exists with the current version
1153    pub fn has_compatible_dbsp_state_table(&self, view_name: &str) -> bool {
1154        let view_name = normalize_ident(view_name);
1155        let expected_table_name = format!("{DBSP_TABLE_PREFIX}{DBSP_CIRCUIT_VERSION}_{view_name}");
1156
1157        // Check if a table with the expected versioned name exists
1158        self.tables.contains_key(&expected_table_name)
1159    }
1160
1161    pub fn is_materialized_view(&self, name: &str) -> bool {
1162        let name = normalize_ident(name);
1163        self.materialized_view_names.contains(&name)
1164    }
1165
1166    /// Apply a function to a table's incompatible dependent materialized views
1167    pub fn with_incompatible_dependent_views<F, T>(&self, table_name: &str, f: F) -> T
1168    where
1169        F: FnOnce(&[&String]) -> T,
1170    {
1171        let table_name = normalize_ident(table_name);
1172        let mut views: SmallVec<[&String; 8]> = SmallVec::with_capacity(8);
1173
1174        // Get all materialized views that depend on this table
1175        if let Some(v) = self.table_to_materialized_views.get(&table_name) {
1176            v.iter()
1177                .filter(|name| self.incompatible_views.contains(&**name))
1178                .for_each(|n| views.push(n));
1179        }
1180        f(&views)
1181    }
1182
1183    pub fn remove_view(&mut self, name: &str) -> Result<()> {
1184        let name = normalize_ident(name);
1185
1186        if self.views.contains_key(&name) {
1187            self.views.remove(&name);
1188            Ok(())
1189        } else if self.materialized_view_names.contains(&name) {
1190            // Remove from tables
1191            self.remove_table(&name);
1192
1193            // Remove DBSP state table and its indexes from in-memory schema
1194            let dbsp_table_name = format!("{DBSP_TABLE_PREFIX}{DBSP_CIRCUIT_VERSION}_{name}");
1195            self.remove_table(&dbsp_table_name);
1196            self.remove_indices_for_table(&dbsp_table_name);
1197
1198            // Remove from materialized view tracking
1199            self.materialized_view_names.remove(&name);
1200            self.materialized_view_sql.remove(&name);
1201            self.incremental_views.remove(&name);
1202
1203            // Remove from table_to_materialized_views dependencies
1204            for views in self.table_to_materialized_views.values_mut() {
1205                views.retain(|v| v != &name);
1206            }
1207
1208            Ok(())
1209        } else {
1210            Err(crate::LimboError::ParseError(format!(
1211                "no such view: {name}"
1212            )))
1213        }
1214    }
1215
1216    /// Register that a materialized view depends on a table
1217    pub fn add_materialized_view_dependency(&mut self, table_name: &str, view_name: &str) {
1218        let table_name = normalize_ident(table_name);
1219        let view_name = normalize_ident(view_name);
1220
1221        self.table_to_materialized_views
1222            .entry(table_name)
1223            .or_insert_with(|| vec![])
1224            .push(view_name);
1225    }
1226
1227    /// Get all materialized views that depend on a given table
1228    pub fn get_dependent_materialized_views(&self, table_name: &str) -> Vec<String> {
1229        if self.table_to_materialized_views.is_empty() {
1230            return vec![];
1231        }
1232        let table_name = normalize_ident(table_name);
1233        self.table_to_materialized_views
1234            .get(&table_name)
1235            .cloned()
1236            .unwrap_or_else(|| vec![])
1237    }
1238
1239    /// Add a regular (non-materialized) view
1240    pub fn add_view(&mut self, view: View) -> Result<()> {
1241        self.check_object_name_conflict(&view.name)?;
1242        let name = normalize_ident(&view.name);
1243        self.views.insert(name, Arc::new(view));
1244        Ok(())
1245    }
1246
1247    /// Get a regular view by name
1248    pub fn get_view(&self, name: &str) -> Option<Arc<View>> {
1249        let name = normalize_ident(name);
1250        self.views.get(&name).cloned()
1251    }
1252
1253    pub fn add_trigger(&mut self, trigger: Trigger, table_name: &str) -> Result<()> {
1254        // Triggers have their own namespace and duplicate trigger names
1255        // are checked in `translate_create_trigger`
1256        let table_name = normalize_ident(table_name);
1257
1258        // See [Schema::add_index] for why we push to the front of the deque.
1259        self.triggers
1260            .entry(table_name)
1261            .or_default()
1262            .push_front(Arc::new(trigger));
1263
1264        Ok(())
1265    }
1266
1267    pub fn remove_trigger(&mut self, name: &str) -> Result<()> {
1268        let name = normalize_ident(name);
1269
1270        let mut removed = false;
1271        for triggers_list in self.triggers.values_mut() {
1272            for i in 0..triggers_list.len() {
1273                let trigger = &triggers_list[i];
1274                if normalize_ident(&trigger.name) == name {
1275                    removed = true;
1276                    triggers_list.remove(i);
1277                    break;
1278                }
1279            }
1280            if removed {
1281                break;
1282            }
1283        }
1284        if !removed {
1285            return Err(crate::LimboError::ParseError(format!(
1286                "no such trigger: {name}"
1287            )));
1288        }
1289        Ok(())
1290    }
1291    pub fn remove_triggers_for_table(&mut self, table_name: &str) {
1292        let table_name = normalize_ident(table_name);
1293        self.triggers.remove(&table_name);
1294    }
1295
1296    /// Like [`remove_triggers_for_table`] but only removes triggers whose
1297    /// `target_database_id` matches `target_db` (or is `None`, meaning
1298    /// "targets the parent schema's table of this name", which also
1299    /// applies). Used from `DROP TABLE main.t` to clean up temp triggers
1300    /// without accidentally removing ones that target `temp.t` or
1301    /// `aux.t` (the plain `remove_triggers_for_table` keys only on
1302    /// table name).
1303    pub fn remove_triggers_for_table_with_db(&mut self, table_name: &str, target_db: usize) {
1304        let table_name = normalize_ident(table_name);
1305        let Some(bucket) = self.triggers.get_mut(&table_name) else {
1306            return;
1307        };
1308        // Check once whether this schema has a table with the same name.
1309        // If it does, unqualified triggers resolve to that local table,
1310        // not to the one being dropped in `target_db`.
1311        let has_shadow_table = self.tables.contains_key(&table_name);
1312        bucket.retain(|trigger| {
1313            match trigger.target_database_id {
1314                Some(db) => db != target_db,
1315                // Unqualified triggers resolve to the local schema's table
1316                // first. Only remove when no local table shadows the name.
1317                None => has_shadow_table,
1318            }
1319        });
1320        if bucket.is_empty() {
1321            self.triggers.remove(&table_name);
1322        }
1323    }
1324
1325    pub fn get_trigger_for_table(&self, table_name: &str, name: &str) -> Option<Arc<Trigger>> {
1326        let table_name = normalize_ident(table_name);
1327        let name = normalize_ident(name);
1328        self.triggers
1329            .get(&table_name)
1330            .and_then(|triggers| triggers.iter().find(|t| t.name == name).cloned())
1331    }
1332
1333    pub fn get_triggers_for_table(
1334        &self,
1335        table_name: &str,
1336    ) -> impl Iterator<Item = &Arc<Trigger>> + Clone {
1337        let table_name = normalize_ident(table_name);
1338        self.triggers
1339            .get(&table_name)
1340            .map(|triggers| triggers.iter())
1341            .unwrap_or_default()
1342    }
1343
1344    pub fn get_trigger(&self, name: &str) -> Option<Arc<Trigger>> {
1345        let name = normalize_ident(name);
1346        self.triggers
1347            .values()
1348            .flatten()
1349            .find(|t| t.name == name)
1350            .cloned()
1351    }
1352
1353    pub fn add_btree_table(&mut self, table: Arc<BTreeTable>) -> Result<()> {
1354        self.check_object_name_conflict(&table.name)?;
1355        let name = normalize_ident(&table.name);
1356        #[cfg(feature = "conn_raw_api")]
1357        self.table_names_by_root_page
1358            .insert(table.root_page, name.clone());
1359        self.tables.insert(name, Table::BTree(table).into());
1360        Ok(())
1361    }
1362
1363    pub fn add_virtual_table(&mut self, table: Arc<VirtualTable>) -> Result<()> {
1364        self.check_object_name_conflict(&table.name)?;
1365        let name = normalize_ident(&table.name);
1366        self.tables.insert(name, Table::Virtual(table).into());
1367        Ok(())
1368    }
1369
1370    pub fn get_table(&self, name: &str) -> Option<Arc<Table>> {
1371        let name = self.normalize_table_lookup_name(name);
1372        self.tables.get(&name).cloned()
1373    }
1374
1375    #[cfg(feature = "conn_raw_api")]
1376    pub fn table_name_for_root_page(&self, root_page: i64) -> Option<&str> {
1377        self.table_names_by_root_page
1378            .get(&root_page)
1379            .map(String::as_str)
1380    }
1381
1382    pub fn remove_table(&mut self, table_name: &str) {
1383        let name = normalize_ident(table_name);
1384        #[cfg(feature = "conn_raw_api")]
1385        {
1386            if let Some(table) = self.tables.remove(&name) {
1387                self.unregister_table_root_page(&table);
1388            }
1389        }
1390        #[cfg(not(feature = "conn_raw_api"))]
1391        {
1392            self.tables.remove(&name);
1393        }
1394        self.analyze_stats.remove_table(&name);
1395
1396        // If this was a materialized view, also clean up the metadata
1397        if self.materialized_view_names.remove(&name) {
1398            self.incremental_views.remove(&name);
1399            self.materialized_view_sql.remove(&name);
1400        }
1401    }
1402
1403    #[cfg(feature = "conn_raw_api")]
1404    pub fn register_table_root_page(&mut self, name: &str, table: &Table) {
1405        if let Table::BTree(table) = table {
1406            self.table_names_by_root_page
1407                .insert(table.root_page, normalize_ident(name));
1408        }
1409    }
1410
1411    #[cfg(feature = "conn_raw_api")]
1412    pub fn unregister_table_root_page(&mut self, table: &Table) {
1413        if let Table::BTree(table) = table {
1414            self.table_names_by_root_page.remove(&table.root_page);
1415        }
1416    }
1417
1418    pub fn get_btree_table(&self, name: &str) -> Option<Arc<BTreeTable>> {
1419        let name = self.normalize_table_lookup_name(name);
1420        if let Some(table) = self.tables.get(&name) {
1421            table.btree()
1422        } else {
1423            None
1424        }
1425    }
1426
1427    pub fn add_index(&mut self, index: Arc<Index>) -> Result<()> {
1428        self.check_object_name_conflict(&index.name)?;
1429        let table_name = normalize_ident(&index.table_name);
1430        // We must add the new index to the front of the deque, because SQLite stores index definitions as a linked list
1431        // where the newest parsed index entry is at the head of list. If we would add it to the back of a regular Vec for example,
1432        // then we would evaluate ON CONFLICT DO UPDATE clauses in the wrong index iteration order and UPDATE the wrong row.
1433        // Additionally, REPLACE indexes must go after all the non-REPLACE indexes so that
1434        // non-mutating conflict resolutions all happen before mutating ones, ensuring that
1435        // no half-committed state is left behind.
1436        let is_replace = index.on_conflict == Some(ResolveType::Replace);
1437        let indexes_for_table = self.indexes.entry(table_name).or_default();
1438        if is_replace {
1439            // REPLACE indexes sort newest-first among themselves.
1440            let first_replace = indexes_for_table
1441                .iter()
1442                .position(|idx| idx.on_conflict == Some(ResolveType::Replace));
1443            let pos = first_replace.unwrap_or(indexes_for_table.len());
1444            indexes_for_table.insert(pos, index);
1445        } else {
1446            // Non-REPLACE indexes go at the front, newest first.
1447            indexes_for_table.push_front(index);
1448        }
1449        turso_debug_assert!(
1450            indexes_for_table
1451                .iter()
1452                .position(|idx| idx.on_conflict == Some(ResolveType::Replace))
1453                .is_none_or(|first_replace| {
1454                    indexes_for_table
1455                        .iter()
1456                        .skip(first_replace)
1457                        .all(|idx| idx.on_conflict == Some(ResolveType::Replace))
1458                }),
1459            "REPLACE indexes must form a contiguous suffix"
1460        );
1461        Ok(())
1462    }
1463
1464    pub fn get_indices(&self, table_name: &str) -> impl Iterator<Item = &Arc<Index>> {
1465        let name = normalize_ident(table_name);
1466        self.indexes
1467            .get(&name)
1468            .map(|v| v.iter())
1469            .unwrap_or_default()
1470            .filter(|i| !i.is_backing_btree_index())
1471    }
1472
1473    #[cfg(all(feature = "fts", not(target_family = "wasm")))]
1474    pub fn has_fts_index(&self, table_name: &str) -> bool {
1475        self.get_indices(table_name).any(|idx| {
1476            idx.index_method.as_ref().is_some_and(|m| {
1477                m.definition().method_name == crate::index_method::fts::FTS_INDEX_METHOD_NAME
1478            })
1479        })
1480    }
1481
1482    pub fn get_index(&self, table_name: &str, index_name: &str) -> Option<&Arc<Index>> {
1483        let name = normalize_ident(table_name);
1484        self.indexes
1485            .get(&name)?
1486            .iter()
1487            .find(|index| index.name == index_name)
1488    }
1489
1490    pub fn remove_indices_for_table(&mut self, table_name: &str) {
1491        let name = normalize_ident(table_name);
1492        self.indexes.remove(&name);
1493        self.analyze_stats.remove_table(&name);
1494    }
1495
1496    pub fn remove_index(&mut self, idx: &Index) {
1497        let name = normalize_ident(&idx.table_name);
1498        self.indexes
1499            .get_mut(&name)
1500            .expect("Must have the index")
1501            .retain_mut(|other_idx| other_idx.name != idx.name);
1502        self.analyze_stats.remove_index(&name, &idx.name);
1503    }
1504
1505    pub fn table_has_indexes(&self, table_name: &str) -> bool {
1506        let name = normalize_ident(table_name);
1507        self.has_indexes.contains(&name)
1508    }
1509
1510    pub fn table_set_has_index(&mut self, table_name: &str) {
1511        self.has_indexes.insert(table_name.to_string());
1512    }
1513
1514    /// Update [Schema] by scanning the first root page (sqlite_schema)
1515    /// Returns Result<IOResult<()>> to allow async operation with external IO loop
1516    pub fn make_from_btree(
1517        &mut self,
1518        state: &mut MakeFromBtreeState,
1519        mv_cursor: Option<Arc<RwLock<MvCursor>>>,
1520        pager: &Arc<Pager>,
1521        syms: &SymbolTable,
1522    ) -> Result<IOResult<()>> {
1523        let result = self.make_from_btree_internal(state, mv_cursor, pager, syms);
1524        if result.is_err() {
1525            state.cleanup(pager);
1526        } else if let Ok(IOResult::Done(..)) = result {
1527            turso_assert!(
1528                !state.read_tx_active,
1529                "make_from_btree must properly cleanup internal state in case of success"
1530            );
1531        }
1532        result
1533    }
1534
1535    fn make_from_btree_internal(
1536        &mut self,
1537        state: &mut MakeFromBtreeState,
1538        mv_cursor: Option<Arc<RwLock<MvCursor>>>,
1539        pager: &Arc<Pager>,
1540        syms: &SymbolTable,
1541    ) -> Result<IOResult<()>> {
1542        loop {
1543            tracing::debug!("make_from_btree: state.phase={:?}", state.phase);
1544            match &state.phase {
1545                MakeFromBtreePhase::Init => {
1546                    if mv_cursor.is_some() {
1547                        return Err(crate::LimboError::ParseError(
1548                            "MVCC is not supported for make_from_btree schema recovery".to_string(),
1549                        ));
1550                    }
1551
1552                    state.cursor = Some(BTreeCursor::new_table(Arc::clone(pager), 1, 10));
1553                    pager.begin_read_tx()?;
1554                    state.read_tx_active = true;
1555
1556                    state.accumulators = Some(MakeFromBtreeAccumulators {
1557                        from_sql_indexes: Vec::try_with_capacity_ext(10)?,
1558                        automatic_indices: HashMap::with_capacity_and_hasher(10, FxBuildHasher),
1559                        dbsp_state_roots: HashMap::default(),
1560                        dbsp_state_index_roots: HashMap::default(),
1561                        materialized_view_info: HashMap::default(),
1562                    });
1563
1564                    state.phase = MakeFromBtreePhase::Rewinding;
1565                }
1566
1567                MakeFromBtreePhase::Rewinding => {
1568                    let cursor = state
1569                        .cursor
1570                        .as_mut()
1571                        .expect("cursor must be initialized in Init phase");
1572                    return_if_io!(cursor.rewind());
1573                    state.phase = MakeFromBtreePhase::FetchingRecord;
1574                }
1575
1576                MakeFromBtreePhase::FetchingRecord => {
1577                    let cursor = state
1578                        .cursor
1579                        .as_mut()
1580                        .expect("cursor must be initialized in Init phase");
1581                    let row = return_if_io!(cursor.record());
1582
1583                    let Some(row) = row else {
1584                        // EOF on the sqlite_schema scan. Hand off to the
1585                        // async sequence-descriptor walk — pulled out of
1586                        // the prior synchronous `populate_sequences(pager)`
1587                        // call so the schema state machine can yield
1588                        // `IOResult::IO` on each cursor read instead of
1589                        // blocking the pager. The Rewind/Fetch phases
1590                        // below pop the back of `state.sequence_sources`,
1591                        // install the descriptor, drop the cursor, and
1592                        // repeat until empty.
1593                        state.sequence_sources = self.sequence_backing_tables();
1594                        state.cursor = None;
1595                        state.phase = MakeFromBtreePhase::PopulatingSequencesRewind;
1596                        continue;
1597                    };
1598
1599                    // Process the row (no IO - CPU only)
1600                    // sqlite schema table has 5 columns: type, name, tbl_name, rootpage, sql
1601                    let ty_value = row.get_value(0)?;
1602                    let ValueRef::Text(ty) = ty_value else {
1603                        return Err(LimboError::ConversionError("Expected text value".into()));
1604                    };
1605                    let ValueRef::Text(name) = row.get_value(1)? else {
1606                        return Err(LimboError::ConversionError("Expected text value".into()));
1607                    };
1608                    let table_name_value = row.get_value(2)?;
1609                    let ValueRef::Text(table_name) = table_name_value else {
1610                        return Err(LimboError::ConversionError("Expected text value".into()));
1611                    };
1612                    let root_page_value = row.get_value(3)?;
1613                    let ValueRef::Numeric(crate::numeric::Numeric::Integer(root_page)) =
1614                        root_page_value
1615                    else {
1616                        return Err(LimboError::ConversionError("Expected integer value".into()));
1617                    };
1618                    let sql_value = row.get_value(4)?;
1619                    let sql_textref = match sql_value {
1620                        ValueRef::Text(sql) => Some(sql),
1621                        _ => None,
1622                    };
1623                    let sql = sql_textref.map(|s| s.as_str());
1624
1625                    let acc = state
1626                        .accumulators
1627                        .as_mut()
1628                        .expect("accumulators must be initialized in Init phase");
1629                    // `make_from_btree` is called during database open before
1630                    // any connection exists, so there is no attached catalog
1631                    // to consult. Any `CREATE TEMP TRIGGER ... ON aux.x` row
1632                    // maps to `Some(INVALID_DB_ID)` until a connection-scoped
1633                    // reparse runs with a real resolver.
1634                    self.handle_schema_row(
1635                        &ty,
1636                        &name,
1637                        &table_name,
1638                        root_page,
1639                        sql,
1640                        syms,
1641                        &mut acc.from_sql_indexes,
1642                        &mut acc.automatic_indices,
1643                        &mut acc.dbsp_state_roots,
1644                        &mut acc.dbsp_state_index_roots,
1645                        &mut acc.materialized_view_info,
1646                        &|_| None,
1647                    )?;
1648
1649                    state.phase = MakeFromBtreePhase::Advancing;
1650                }
1651
1652                MakeFromBtreePhase::Advancing => {
1653                    let cursor = state
1654                        .cursor
1655                        .as_mut()
1656                        .expect("cursor must be initialized in Init phase");
1657                    return_if_io!(cursor.next());
1658                    state.phase = MakeFromBtreePhase::FetchingRecord;
1659                }
1660
1661                MakeFromBtreePhase::PopulatingSequencesRewind => {
1662                    // Either no sources left → finalize, or pop the next
1663                    // source and rewind its cursor.
1664                    if state.sequence_sources.is_empty() {
1665                        pager.end_read_tx();
1666                        state.read_tx_active = false;
1667
1668                        let acc = state
1669                            .accumulators
1670                            .take()
1671                            .expect("accumulators must be initialized in Init phase");
1672                        self.populate_indices(
1673                            syms,
1674                            acc.from_sql_indexes,
1675                            acc.automatic_indices,
1676                            mv_cursor.is_some(),
1677                        )?;
1678                        self.populate_materialized_views(
1679                            acc.materialized_view_info,
1680                            acc.dbsp_state_roots,
1681                            acc.dbsp_state_index_roots,
1682                        )?;
1683
1684                        state.phase = MakeFromBtreePhase::Done;
1685                        return Ok(IOResult::Done(()));
1686                    }
1687                    // Drop any cursor from a previous source before opening
1688                    // the new one (Drop logic on the BTreeCursor releases
1689                    // its page pins).
1690                    state.sequence_cursor = None;
1691                    let source = state
1692                        .sequence_sources
1693                        .last()
1694                        .expect("non-empty checked above");
1695                    // MVCC backing tables that haven't been checkpointed
1696                    // yet carry the negative-root sentinel; the pager
1697                    // can't read them directly. Skip — the SQL fallback
1698                    // (`Connection::populate_sequences_via_sql`) will
1699                    // load them via the MVCC row layer.
1700                    if source.root_page <= 0 {
1701                        state.sequence_sources.pop();
1702                        continue;
1703                    }
1704                    let cursor =
1705                        BTreeCursor::new_table(pager.clone(), source.root_page, source.num_columns);
1706                    state.sequence_cursor = Some(cursor);
1707                    let cursor = state.sequence_cursor.as_mut().expect("just set");
1708                    return_if_io!(cursor.rewind());
1709                    state.phase = MakeFromBtreePhase::PopulatingSequencesFetch;
1710                }
1711
1712                MakeFromBtreePhase::PopulatingSequencesFetch => {
1713                    let cursor = state
1714                        .sequence_cursor
1715                        .as_mut()
1716                        .expect("cursor must be initialized in PopulatingSequencesRewind");
1717                    let record = return_if_io!(cursor.record());
1718                    let source = state.sequence_sources.pop().expect("at least one source");
1719                    let record = record.ok_or_else(|| {
1720                        LimboError::Corrupt(format!(
1721                            "internal sequence backing table for \"{}\" is empty; \
1722                             the descriptor metadata row must always be present",
1723                            source.sequence_name
1724                        ))
1725                    })?;
1726                    let metadata = Self::read_sequence_metadata(record).ok_or_else(|| {
1727                        LimboError::Corrupt(format!(
1728                            "internal sequence backing table for \"{}\" descriptor \
1729                             row is malformed (expected integers for \
1730                             start/inc/min/max/cycle)",
1731                            source.sequence_name
1732                        ))
1733                    })?;
1734                    self.install_sequence_descriptor(&source.sequence_name, metadata)?;
1735                    // Drop the cursor before transitioning back so we
1736                    // release its page pins before the next source's
1737                    // rewind starts.
1738                    state.sequence_cursor = None;
1739                    state.phase = MakeFromBtreePhase::PopulatingSequencesRewind;
1740                }
1741
1742                MakeFromBtreePhase::Done => {
1743                    return Ok(IOResult::Done(()));
1744                }
1745            }
1746        }
1747    }
1748
1749    /// Populate indices parsed from the schema.
1750    /// from_sql_indexes: indices explicitly created with CREATE INDEX
1751    /// automatic_indices: indices created automatically for primary key and unique constraints
1752    pub fn populate_indices(
1753        &mut self,
1754        syms: &SymbolTable,
1755        from_sql_indexes: Vec<UnparsedFromSqlIndex>,
1756        automatic_indices: HashMap<String, Vec<(String, i64)>>,
1757        mvcc_enabled: bool,
1758    ) -> Result<()> {
1759        for unparsed_sql_from_index in from_sql_indexes {
1760            let table = self
1761                .get_btree_table(&unparsed_sql_from_index.table_name)
1762                .ok_or_else(|| {
1763                    LimboError::Corrupt(format!(
1764                        "sqlite_schema contains index for missing table '{}': rootpage={} sql={}",
1765                        unparsed_sql_from_index.table_name,
1766                        unparsed_sql_from_index.root_page,
1767                        unparsed_sql_from_index.sql
1768                    ))
1769                })?;
1770            let index = Index::from_sql(
1771                syms,
1772                &unparsed_sql_from_index.sql,
1773                unparsed_sql_from_index.root_page,
1774                table.as_ref(),
1775            )?;
1776            if mvcc_enabled && index.index_method.is_some() {
1777                crate::bail_parse_error!("Custom index modules are not supported with MVCC");
1778            }
1779            self.add_index(Arc::new(index))?;
1780        }
1781
1782        for automatic_index in automatic_indices {
1783            // Autoindexes must be parsed in definition order.
1784            // The SQL statement parser enforces that the column definitions come first, and compounds are defined after that,
1785            // e.g. CREATE TABLE t (a, b, UNIQUE(a, b)), and you can't do something like CREATE TABLE t (a, b, UNIQUE(a, b), c);
1786            // Hence, we can process the singles first (unique_set.columns.len() == 1), and then the compounds (unique_set.columns.len() > 1).
1787            let table = self.get_btree_table(&automatic_index.0).ok_or_else(|| {
1788                LimboError::Corrupt(format!(
1789                    "sqlite_schema contains automatic index for missing table '{}': indexes={:?}",
1790                    automatic_index.0, automatic_index.1
1791                ))
1792            })?;
1793            let mut automatic_indexes = automatic_index.1;
1794            automatic_indexes.reverse(); // reverse so we can pop() without shifting array elements, while still processing in left-to-right order
1795
1796            // we must process unique_sets in this exact order in order to emit automatic indices schema entries in the same order
1797            let mut pk_index_added = false;
1798            for unique_set in &table.unique_sets {
1799                if unique_set.is_primary_key {
1800                    assert!(
1801                        table.primary_key_columns.len() == unique_set.columns.len(),
1802                        "trying to add a {}-column primary key index for table {}, but the table has {} primary key columns",
1803                        unique_set.columns.len(),
1804                        table.name,
1805                        table.primary_key_columns.len()
1806                    );
1807                    // Add composite primary key index
1808                    assert!(
1809                        !pk_index_added,
1810                        "trying to add a second primary key index for table {}",
1811                        table.name
1812                    );
1813                    pk_index_added = true;
1814
1815                    if unique_set.columns.len() == 1 {
1816                        let col_name = &unique_set.columns.first().unwrap().0;
1817                        let Some((_, column)) = table.get_column(col_name) else {
1818                            return Err(LimboError::ParseError(format!(
1819                                "Column {col_name} not found in table {}",
1820                                table.name
1821                            )));
1822                        };
1823                        if column.is_rowid_alias() {
1824                            // rowid alias, no index needed
1825                            continue;
1826                        }
1827                    }
1828
1829                    if let Some(index_entry) = automatic_indexes.pop() {
1830                        self.add_index(Arc::new(Index::automatic_from_primary_key(
1831                            table.as_ref(),
1832                            index_entry,
1833                            unique_set.columns.len(),
1834                            unique_set.conflict_clause,
1835                            &unique_set.collations,
1836                        )?))?;
1837                    } else if mvcc_enabled {
1838                        // In MVCC mode, automatic indices might not be fully populated yet during recovery
1839                        // Skip creating this index - it will be added later when its schema row is processed
1840                        continue;
1841                    } else {
1842                        return Err(LimboError::InternalError(format!(
1843                            "Missing automatic index entry for primary key on table {}",
1844                            table.name
1845                        )));
1846                    }
1847                } else {
1848                    // Add composite unique index
1849                    let mut column_indices_and_sort_orders =
1850                        Vec::try_with_capacity_ext(unique_set.columns.len())?;
1851                    for (col_name, sort_order) in unique_set.columns.iter() {
1852                        let Some((pos_in_table, _)) = table.get_column(col_name) else {
1853                            return Err(crate::LimboError::ParseError(format!(
1854                                "Column {} not found in table {}",
1855                                col_name, table.name
1856                            )));
1857                        };
1858                        column_indices_and_sort_orders
1859                            .push_within_capacity((pos_in_table, *sort_order))
1860                            .expect("unique columns vector was preallocated to its input length");
1861                    }
1862                    if let Some(index_entry) = automatic_indexes.pop() {
1863                        self.add_index(Arc::new(Index::automatic_from_unique(
1864                            table.as_ref(),
1865                            index_entry,
1866                            column_indices_and_sort_orders,
1867                            unique_set.conflict_clause,
1868                            &unique_set.collations,
1869                        )?))?;
1870                    } else if mvcc_enabled {
1871                        // In MVCC mode, automatic indices might not be fully populated yet during recovery
1872                        // Skip creating this index - it will be added later when its schema row is processed
1873                        continue;
1874                    } else {
1875                        return Err(LimboError::InternalError(format!(
1876                            "Missing automatic index entry for UNIQUE constraint on table {}",
1877                            table.name
1878                        )));
1879                    }
1880                }
1881            }
1882
1883            // In MVCC mode during recovery, not all automatic index schema rows might be visible yet
1884            // during incremental schema reparsing, so we may have extra entries
1885            if !mvcc_enabled {
1886                assert!(
1887                    automatic_indexes.is_empty(),
1888                    "all automatic indexes parsed from sqlite_schema should have been consumed, but {} remain",
1889                    automatic_indexes.len()
1890                );
1891            }
1892        }
1893        Ok(())
1894    }
1895
1896    /// Populate materialized views parsed from the schema.
1897    pub fn populate_materialized_views(
1898        &mut self,
1899        materialized_view_info: HashMap<String, (String, i64)>,
1900        dbsp_state_roots: HashMap<String, i64>,
1901        dbsp_state_index_roots: HashMap<String, i64>,
1902    ) -> Result<()> {
1903        for (view_name, (sql, main_root)) in materialized_view_info {
1904            // Look up the DBSP state root for this view
1905            // If missing, it means version mismatch - skip this view
1906            // Check if we have a compatible DBSP state root
1907            let dbsp_state_root = if let Some(&root) = dbsp_state_roots.get(&view_name) {
1908                root
1909            } else {
1910                tracing::warn!(
1911                    "Materialized view '{}' has incompatible version or missing DBSP state table",
1912                    view_name
1913                );
1914                // Track this as an incompatible view
1915                self.incompatible_views.insert(view_name.clone());
1916                // Use a dummy root page - the view won't be usable anyway
1917                0
1918            };
1919
1920            // Look up the DBSP state index root (may not exist for older schemas)
1921            let dbsp_state_index_root =
1922                dbsp_state_index_roots.get(&view_name).copied().unwrap_or(0);
1923
1924            // Register the DBSP state index so integrity check can account for its pages.
1925            if dbsp_state_index_root > 0 && dbsp_state_root > 0 {
1926                let mut index = create_dbsp_state_index(dbsp_state_index_root);
1927                let dbsp_table_name =
1928                    format!("{DBSP_TABLE_PREFIX}{DBSP_CIRCUIT_VERSION}_{view_name}");
1929                index.name = format!("sqlite_autoindex_{dbsp_table_name}_1");
1930                index.table_name = dbsp_table_name;
1931                if let Err(e) = self.add_index(std::sync::Arc::new(index)) {
1932                    if !e.to_string().contains("already exists") {
1933                        return Err(e);
1934                    }
1935                }
1936            }
1937
1938            // Create the IncrementalView with all root pages
1939            let incremental_view = IncrementalView::from_sql(
1940                &sql,
1941                self,
1942                main_root,
1943                dbsp_state_root,
1944                dbsp_state_index_root,
1945            )?;
1946            let referenced_tables = incremental_view.get_referenced_table_names();
1947
1948            // Create a BTreeTable for the materialized view
1949            let cols = incremental_view.column_schema.flat_columns();
1950            let logical_to_physical_map =
1951                BTreeTable::build_logical_to_physical_map(&cols, &[], true);
1952            let table = Arc::new(Table::BTree(Arc::new(BTreeTable {
1953                name: view_name.clone(),
1954                root_page: main_root,
1955                columns: cols,
1956                primary_key_columns: vec![],
1957                has_rowid: true,
1958                is_strict: false,
1959                has_autoincrement: false,
1960                foreign_keys: vec![],
1961                check_constraints: vec![],
1962                rowid_alias_conflict_clause: None,
1963                unique_sets: vec![],
1964                has_virtual_columns: false,
1965                logical_to_physical_map,
1966                column_dependencies: Default::default(),
1967            })));
1968
1969            // Only add to schema if compatible
1970            if !self.incompatible_views.contains(&view_name) {
1971                self.add_materialized_view(incremental_view, table, sql);
1972            }
1973
1974            // Register dependencies regardless of compatibility
1975            for table_name in referenced_tables {
1976                self.add_materialized_view_dependency(&table_name, &view_name);
1977            }
1978        }
1979        Ok(())
1980    }
1981
1982    /// Yield (backing_table_name, sequence_name) for every backing table
1983    /// currently in the schema. Shared shape for the SQL-based descriptor
1984    /// loader in `Connection` so the prefix-strip lives in one place.
1985    pub fn sequence_backing_table_names(&self) -> Vec<(String, String)> {
1986        self.tables
1987            .keys()
1988            .filter_map(|name| {
1989                let seq_name = name.strip_prefix(SEQ_BACKING_TABLE_PREFIX)?;
1990                Some((name.clone(), seq_name.to_string()))
1991            })
1992            .try_collect()
1993            .expect(crate::alloc::ALLOC_ERR_MSG)
1994    }
1995
1996    fn sequence_backing_tables(&self) -> Vec<SequenceBackingTableSource> {
1997        self.tables
1998            .iter()
1999            .filter_map(|(name, table)| {
2000                let bt = table.btree()?;
2001                let sequence_name = name.strip_prefix(SEQ_BACKING_TABLE_PREFIX)?.to_string();
2002                Some(SequenceBackingTableSource {
2003                    sequence_name,
2004                    root_page: bt.root_page,
2005                    num_columns: bt.columns().len(),
2006                })
2007            })
2008            .try_collect()
2009            .expect(crate::alloc::ALLOC_ERR_MSG)
2010    }
2011
2012    fn read_sequence_metadata(record: &ImmutableRecord) -> Option<SequenceMetadata> {
2013        let mut values = [0i64; 6];
2014        for (i, value) in values.iter_mut().enumerate() {
2015            match record.get_value(i + 1) {
2016                Ok(ValueRef::Numeric(crate::numeric::Numeric::Integer(v))) => {
2017                    *value = v;
2018                }
2019                _ => return None,
2020            }
2021        }
2022        let [_is_called, start, increment, min, max, cycle] = values;
2023        Some(SequenceMetadata {
2024            start,
2025            increment,
2026            min,
2027            max,
2028            cycle: cycle != 0,
2029        })
2030    }
2031
2032    fn install_sequence_descriptor(
2033        &mut self,
2034        sequence_name: &str,
2035        metadata: SequenceMetadata,
2036    ) -> crate::Result<()> {
2037        let seq = Sequence::new(
2038            sequence_name.to_string(),
2039            Some(metadata.start),
2040            Some(metadata.increment),
2041            Some(metadata.min),
2042            Some(metadata.max),
2043            metadata.cycle,
2044        )
2045        .map_err(|err| {
2046            LimboError::Corrupt(format!(
2047                "internal sequence backing table for \"{sequence_name}\" \
2048                 has invalid persisted metadata \
2049                 (start={}, increment={}, min={}, max={}, cycle={}): {err}",
2050                metadata.start, metadata.increment, metadata.min, metadata.max, metadata.cycle,
2051            ))
2052        })?;
2053        self.sequences
2054            .insert(normalize_ident(sequence_name), std::sync::Arc::new(seq));
2055        Ok(())
2056    }
2057
2058    #[allow(clippy::too_many_arguments)]
2059    pub fn handle_schema_row(
2060        &mut self,
2061        ty: &str,
2062        name: &str,
2063        table_name: &str,
2064        root_page: i64,
2065        maybe_sql: Option<&str>,
2066        syms: &SymbolTable,
2067        from_sql_indexes: &mut Vec<UnparsedFromSqlIndex>,
2068        automatic_indices: &mut HashMap<String, Vec<(String, i64)>>,
2069        dbsp_state_roots: &mut HashMap<String, i64>,
2070        dbsp_state_index_roots: &mut HashMap<String, i64>,
2071        materialized_view_info: &mut HashMap<String, (String, i64)>,
2072        // Resolves an attached database name (case-insensitive) to its
2073        // connection-local database id. Used when reparsing temp trigger
2074        // SQL that qualifies its target with an attached db name like
2075        // `CREATE TEMP TRIGGER tr ON aux.x ...`. Callers without a
2076        // connection (tests, offline schema loading) can pass
2077        // `&|_| None`; unresolvable names become `Some(INVALID_DB_ID)`
2078        // so the trigger never fires against a real db.
2079        resolve_attached_db: &dyn Fn(&str) -> Option<usize>,
2080    ) -> Result<()> {
2081        match ty {
2082            "table" => {
2083                let sql = maybe_sql.expect("sql should be present for table");
2084                // Classify the row by parsing its schema SQL, mirroring
2085                // SQLite, where sqlite3InitCallback feeds the sql column to
2086                // the parser and a row becomes a virtual table purely as a
2087                // byproduct of the create_vtab grammar rule.
2088                match Parser::new(sql.as_bytes()).next_cmd()? {
2089                    Some(Cmd::Stmt(Stmt::CreateVirtualTable(_))) => {
2090                        if root_page != 0 {
2091                            return Err(LimboError::Corrupt(format!(
2092                                "sqlite_schema root_page must be 0 for virtual table {name}, got {root_page}"
2093                            )));
2094                        }
2095                        // a virtual table is found in the sqlite_schema, but it's no
2096                        // longer in the in-memory schema. We need to recreate it if
2097                        // the module is loaded in the symbol table.
2098                        let vtab = if let Some(vtab) = syms.vtabs.get(name) {
2099                            vtab.clone()
2100                        } else {
2101                            let mod_name = module_name_from_sql(sql)?;
2102                            crate::VirtualTable::table(
2103                                Some(name),
2104                                mod_name,
2105                                module_args_from_sql(sql)?,
2106                                syms,
2107                            )?
2108                        };
2109                        self.add_virtual_table(vtab)?;
2110                    }
2111                    Some(Cmd::Stmt(Stmt::CreateTable { tbl_name, body, .. })) => {
2112                        let table = create_table(tbl_name.name.as_str(), &body, root_page)?;
2113
2114                        if table.has_virtual_columns && !self.generated_columns_enabled {
2115                            return Err(LimboError::ParseError(format!(
2116                            "table '{}' uses generated columns but the generated_columns feature is not enabled",
2117                            table.name
2118                        )));
2119                        }
2120
2121                        // Detect sequence-backing tables by name prefix.
2122                        // Just add the table (for B-tree access); sequences are created by
2123                        // AddSequence at CREATE time or initialize_sequences at open time.
2124                        if table.name.starts_with(SEQ_BACKING_TABLE_PREFIX) {
2125                            self.add_btree_table(Arc::new(table))?;
2126                            return Ok(());
2127                        }
2128
2129                        // Check if this is a DBSP state table
2130                        if table.name.starts_with(DBSP_TABLE_PREFIX) {
2131                            // Extract version and view name from __turso_internal_dbsp_state_v<version>_<viewname>
2132                            let suffix = table.name.strip_prefix(DBSP_TABLE_PREFIX).unwrap();
2133
2134                            // Parse version and view name (format: "<version>_<viewname>")
2135                            if let Some(underscore_pos) = suffix.find('_') {
2136                                let version_str = &suffix[..underscore_pos];
2137                                let view_name = &suffix[underscore_pos + 1..];
2138
2139                                // Check version compatibility
2140                                if let Ok(stored_version) = version_str.parse::<u32>() {
2141                                    if stored_version == DBSP_CIRCUIT_VERSION {
2142                                        // Version matches, store the root page
2143                                        dbsp_state_roots.insert(view_name.to_string(), root_page);
2144                                    } else {
2145                                        // Version mismatch - DO NOT insert into dbsp_state_roots
2146                                        // This will cause populate_materialized_views to skip this view
2147                                        tracing::warn!(
2148                                        "Skipping materialized view '{}' - has version {} but current version is {}. DROP and recreate the view to use it.",
2149                                        view_name,
2150                                        stored_version,
2151                                        DBSP_CIRCUIT_VERSION
2152                                    );
2153                                        // We can't track incompatible views here since we're in handle_schema_row
2154                                        // which doesn't have mutable access to self
2155                                    }
2156                                }
2157                            }
2158                        }
2159
2160                        let mut table = table;
2161                        table.resolve_custom_type_affinities(self);
2162                        table.propagate_domain_constraints(self)?;
2163                        let has_autoinc = table.has_autoincrement;
2164                        let tbl_name = table.name.clone();
2165                        self.add_btree_table(Arc::new(table))?;
2166
2167                        // Create the hidden sequence object owned by this
2168                        // AUTOINCREMENT table. The `__turso_internal_autoincrement_`
2169                        // prefix is a sequence namespace marker, not a table name;
2170                        // the physical table is the corresponding
2171                        // `__turso_internal_seq_<sequence-name>` backing table.
2172                        if has_autoinc {
2173                            let seq_name = autoincrement_sequence_name(&tbl_name);
2174                            if let std::collections::hash_map::Entry::Vacant(e) =
2175                                self.sequences.entry(normalize_ident(&seq_name))
2176                            {
2177                                let seq = Sequence::new(
2178                                    seq_name.clone(),
2179                                    Some(1),
2180                                    Some(1),
2181                                    None,
2182                                    None,
2183                                    false,
2184                                )?;
2185                                e.insert(Arc::new(seq));
2186                            }
2187                        }
2188                    }
2189                    other => {
2190                        return Err(LimboError::Corrupt(format!(
2191                            "sqlite_schema table row {name} has unexpected SQL {sql:?}: parsed as {other:?}"
2192                        )));
2193                    }
2194                }
2195            }
2196            "index" => {
2197                match maybe_sql {
2198                    Some(sql) => {
2199                        from_sql_indexes.push(UnparsedFromSqlIndex {
2200                            table_name: table_name.to_string(),
2201                            root_page,
2202                            sql: sql.to_string(),
2203                        });
2204                    }
2205                    None => {
2206                        // Automatic index on primary key and/or unique constraint, e.g.
2207                        // table|foo|foo|2|CREATE TABLE foo (a text PRIMARY KEY, b)
2208                        // index|sqlite_autoindex_foo_1|foo|3|
2209                        let index_name = name.to_string();
2210                        let table_name = table_name.to_string();
2211
2212                        // Check if this is an index for a DBSP state table
2213                        if table_name.starts_with(DBSP_TABLE_PREFIX) {
2214                            // Extract version and view name from __turso_internal_dbsp_state_v<version>_<viewname>
2215                            let suffix = table_name.strip_prefix(DBSP_TABLE_PREFIX).unwrap();
2216
2217                            // Parse version and view name (format: "<version>_<viewname>")
2218                            if let Some(underscore_pos) = suffix.find('_') {
2219                                let version_str = &suffix[..underscore_pos];
2220                                let view_name = &suffix[underscore_pos + 1..];
2221
2222                                // Only store index root if version matches
2223                                if let Ok(stored_version) = version_str.parse::<u32>() {
2224                                    if stored_version == DBSP_CIRCUIT_VERSION {
2225                                        dbsp_state_index_roots
2226                                            .insert(view_name.to_string(), root_page);
2227                                    }
2228                                }
2229                            }
2230                        } else {
2231                            match automatic_indices.entry(table_name) {
2232                                std::collections::hash_map::Entry::Vacant(e) => {
2233                                    e.insert(vec![(index_name, root_page)]);
2234                                }
2235                                std::collections::hash_map::Entry::Occupied(mut e) => {
2236                                    e.get_mut().push((index_name, root_page));
2237                                }
2238                            }
2239                        }
2240                    }
2241                }
2242            }
2243            "view" => {
2244                use crate::schema::View;
2245                use turso_parser::ast::{Cmd, Stmt};
2246                use turso_parser::parser::Parser;
2247
2248                let sql = maybe_sql.expect("sql should be present for view");
2249                let view_name = name.to_string();
2250
2251                // Parse the SQL to determine if it's a regular or materialized view
2252                let mut parser = Parser::new(sql.as_bytes());
2253                let parsed = parser.next_cmd();
2254                if !matches!(&parsed, Ok(Some(Cmd::Stmt(_)))) {
2255                    // Tolerate view rows whose stored SQL no longer parses
2256                    // (e.g. older versions wrote view column lists without
2257                    // identifier quoting). The database stays usable; the
2258                    // name is tracked so DROP VIEW can remove the row.
2259                    tracing::warn!(
2260                        "view '{view_name}' has unparseable SQL in sqlite_schema; \
2261                         it is unavailable but can be removed with DROP VIEW: {sql}"
2262                    );
2263                    self.broken_views.insert(view_name);
2264                } else if let Ok(Some(Cmd::Stmt(stmt))) = parsed {
2265                    match stmt {
2266                        Stmt::CreateMaterializedView { .. } => {
2267                            // Store materialized view info for later creation
2268                            // We'll handle reuse logic and create the actual IncrementalView
2269                            // in a later pass when we have both the main root page and DBSP state root
2270                            materialized_view_info
2271                                .insert(view_name.clone(), (sql.to_string(), root_page));
2272
2273                            // Mark the existing view for potential reuse
2274                            if self.incremental_views.contains_key(&view_name) {
2275                                // We'll check for reuse in the third pass
2276                            }
2277                        }
2278                        Stmt::CreateView {
2279                            view_name: _,
2280                            columns: column_names,
2281                            select,
2282                            ..
2283                        } => {
2284                            crate::util::validate_select_for_unsupported_features(&select)?;
2285
2286                            // Extract actual columns from the SELECT statement
2287                            let view_column_schema =
2288                                crate::util::extract_view_columns(&select, self)?;
2289
2290                            // If column names were provided in CREATE VIEW (col1, col2, ...),
2291                            // use them to rename the columns
2292                            let mut final_columns = view_column_schema.flat_columns();
2293                            for (i, indexed_col) in column_names.iter().enumerate() {
2294                                if let Some(col) = final_columns.get_mut(i) {
2295                                    // as_str: Display would render the quoted form,
2296                                    // embedding literal quote characters in the name
2297                                    col.name = Some(indexed_col.col_name.as_str().to_string());
2298                                }
2299                            }
2300
2301                            // Create regular view
2302                            let view =
2303                                View::new(name.to_string(), sql.to_string(), select, final_columns);
2304                            self.add_view(view)?;
2305                        }
2306                        _ => {}
2307                    }
2308                }
2309            }
2310            "trigger" => {
2311                use turso_parser::ast::{Cmd, Stmt};
2312                use turso_parser::parser::Parser;
2313
2314                let sql = maybe_sql.expect("sql should be present for trigger");
2315                let trigger_name = name.to_string();
2316
2317                let mut parser = Parser::new(sql.as_bytes());
2318                let Ok(Some(Cmd::Stmt(Stmt::CreateTrigger {
2319                    temporary,
2320                    if_not_exists: _,
2321                    trigger_name: _,
2322                    time,
2323                    event,
2324                    tbl_name,
2325                    for_each_row,
2326                    when_clause,
2327                    commands,
2328                }))) = parser.next_cmd()
2329                else {
2330                    return Err(crate::LimboError::ParseError(format!(
2331                        "invalid trigger sql: {sql}"
2332                    )));
2333                };
2334                // Resolve the target database from the SQL qualifier:
2335                // CREATE TEMP TRIGGER ... ON main.tbl → target is MAIN_DB_ID
2336                // CREATE TEMP TRIGGER ... ON tbl     → target is None (unqualified)
2337                // CREATE TEMP TRIGGER ... ON aux.tbl → resolve `aux` via the
2338                //     attached catalog; if the name is unknown to this
2339                //     connection use `INVALID_DB_ID` so the trigger never
2340                //     fires on a mismatched db. Using `None` (the old
2341                //     behaviour) would treat an unresolved attached name
2342                //     the same as an unqualified reference, causing the
2343                //     trigger to fire on every table with a matching name.
2344                let target_database_id = tbl_name.db_name.as_ref().map(|db_name| {
2345                    let db = db_name.as_str();
2346                    if db.eq_ignore_ascii_case("main") {
2347                        crate::MAIN_DB_ID
2348                    } else if db.eq_ignore_ascii_case("temp") {
2349                        crate::TEMP_DB_ID
2350                    } else {
2351                        resolve_attached_db(db).unwrap_or(crate::INVALID_DB_ID)
2352                    }
2353                });
2354                self.add_trigger(
2355                    Trigger::new(
2356                        trigger_name,
2357                        sql.to_string(),
2358                        // Store the bare (unquoted) table name. `Name::to_string()`
2359                        // renders the quoted form (`"t1"`), which then fails every
2360                        // schema lookup since `normalize_ident` does not strip quotes.
2361                        // This must match the bucket key used in `add_trigger` below.
2362                        tbl_name.name.as_str().to_string(),
2363                        time,
2364                        event,
2365                        for_each_row,
2366                        when_clause.map(|e| *e),
2367                        commands,
2368                        temporary,
2369                        target_database_id,
2370                    ),
2371                    tbl_name.name.as_str(),
2372                )?;
2373            }
2374            // Types are stored in sqlite_turso_types, not sqlite_schema
2375            _ => {}
2376        };
2377
2378        Ok(())
2379    }
2380
2381    /// Compute all resolved FKs *referencing* `table_name` (arg: `table_name` is the parent).
2382    /// Each item contains the child table, normalized columns/positions, and the parent lookup
2383    /// strategy (rowid vs. UNIQUE index or PK).
2384    pub fn resolved_fks_referencing(&self, table_name: &str) -> Result<Vec<ResolvedFkRef>> {
2385        let target = normalize_ident(table_name);
2386        let parent_tbl = self
2387            .get_btree_table(&target)
2388            .ok_or_else(|| fk_mismatch_err("<unknown>", &target))?;
2389
2390        let mut out = Vec::try_with_capacity_ext(4)?; // arbitrary estimate
2391        for t in self.tables.values() {
2392            let Some(child) = t.btree() else {
2393                continue;
2394            };
2395            for fk in &child.foreign_keys {
2396                if !fk.parent_table.eq_ignore_ascii_case(&target) {
2397                    continue;
2398                }
2399                out.try_push(self.resolve_fk(
2400                    fk,
2401                    &child,
2402                    &parent_tbl,
2403                    /*require_unique=*/ false,
2404                )?)?;
2405            }
2406        }
2407        Ok(out)
2408    }
2409
2410    /// Compute all resolved FKs *declared by* `child_table`.
2411    /// Unlike `resolved_fks_referencing`, this requires every non-rowid parent key
2412    /// to be backed by a non-partial UNIQUE index on exactly those columns.
2413    pub fn resolved_fks_for_child(&self, child_table: &str) -> crate::Result<Vec<ResolvedFkRef>> {
2414        let child_name = normalize_ident(child_table);
2415        let child = self
2416            .get_btree_table(&child_name)
2417            .ok_or_else(|| fk_mismatch_err(&child_name, "<unknown>"))?;
2418
2419        let mut out = Vec::try_with_capacity_ext(child.foreign_keys.len())?;
2420        for fk in &child.foreign_keys {
2421            let parent_name = normalize_ident(&fk.parent_table);
2422            let parent_tbl = self
2423                .get_btree_table(&parent_name)
2424                .ok_or_else(|| fk_mismatch_err(&child.name, &parent_name))?;
2425            out.push_within_capacity(self.resolve_fk(
2426                fk,
2427                &child,
2428                &parent_tbl,
2429                /*require_unique=*/ true,
2430            )?)
2431            .expect("resolved FK vector was preallocated to child.foreign_keys.len()");
2432        }
2433        Ok(out)
2434    }
2435
2436    /// Resolve a single FK declared on `child` referencing `parent_tbl`.
2437    /// When `require_unique` is set, a non-rowid parent key must be backed by
2438    /// a non-partial UNIQUE index on exactly those columns.
2439    fn resolve_fk(
2440        &self,
2441        fk: &Arc<ForeignKey>,
2442        child: &Arc<BTreeTable>,
2443        parent_tbl: &Arc<BTreeTable>,
2444        require_unique: bool,
2445    ) -> Result<ResolvedFkRef> {
2446        // child_columns is validated non-empty at parse time, but keep a defensive check
2447        // because schema can be loaded from user-provided sqlite files.
2448        if fk.child_columns.is_empty() {
2449            return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
2450        }
2451
2452        let mut child_pos: Vec<usize> = Vec::try_with_capacity_ext(fk.child_columns.len())?;
2453        for cname in fk.child_columns.iter() {
2454            let (i, _) = child
2455                .get_column(cname)
2456                .ok_or_else(|| fk_mismatch_err(&child.name, &parent_tbl.name))?;
2457            child_pos
2458                .push_within_capacity(i)
2459                .expect("child FK position vector was preallocated to fk.child_columns.len()");
2460        }
2461
2462        // Resolve parent columns: explicit list, or default to parent's PK columns.
2463        let parent_cols: Box<[String]> = if fk.parent_columns.is_empty() {
2464            if parent_tbl.primary_key_columns.is_empty() {
2465                return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
2466            }
2467            parent_tbl
2468                .primary_key_columns
2469                .iter()
2470                .map(|(col, _)| col.clone())
2471                .try_collect()?
2472        } else {
2473            fk.parent_columns.clone()
2474        };
2475
2476        if parent_cols.len() != fk.child_columns.len() {
2477            return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
2478        }
2479
2480        let mut parent_pos: Vec<usize> = Vec::try_with_capacity_ext(parent_cols.len())?;
2481        for pc in parent_cols.iter() {
2482            let pos = parent_tbl.get_column(pc).map(|(i, _)| i).or_else(|| {
2483                ROWID_STRS
2484                    .iter()
2485                    .any(|r| pc.eq_ignore_ascii_case(r))
2486                    .then_some(0)
2487            });
2488            let Some(p) = pos else {
2489                return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
2490            };
2491            parent_pos
2492                .push_within_capacity(p)
2493                .expect("parent FK position vector was preallocated to parent_cols.len()");
2494        }
2495
2496        // A single-column parent key is the rowid when it names rowid/_rowid_/oid
2497        // or points at an INTEGER PRIMARY KEY rowid alias.
2498        let parent_uses_rowid = parent_cols.len() == 1 && {
2499            let pc = parent_cols[0].as_str();
2500            ROWID_STRS.iter().any(|r| pc.eq_ignore_ascii_case(r))
2501                || parent_tbl.columns.iter().any(|col| {
2502                    col.is_rowid_alias()
2503                        && col
2504                            .name
2505                            .as_deref()
2506                            .is_some_and(|n| n.eq_ignore_ascii_case(pc))
2507                })
2508        };
2509
2510        let parent_unique_index = if parent_uses_rowid {
2511            None
2512        } else {
2513            let found = self
2514                .get_indices(&parent_tbl.name)
2515                .find(|idx| {
2516                    idx.unique
2517                        && idx.where_clause.is_none()
2518                        && idx.columns.len() == parent_cols.len()
2519                        && idx
2520                            .columns
2521                            .iter()
2522                            .zip(parent_cols.iter())
2523                            .all(|(ic, pc)| ic.name.eq_ignore_ascii_case(pc))
2524                })
2525                .cloned();
2526            if require_unique && found.is_none() {
2527                return Err(fk_mismatch_err(&child.name, &parent_tbl.name));
2528            }
2529            found
2530        };
2531
2532        fk.validate()?;
2533        Ok(ResolvedFkRef {
2534            child_table: Arc::clone(child),
2535            fk: Arc::clone(fk),
2536            parent_cols,
2537            child_pos: child_pos.into_boxed_slice(),
2538            parent_pos: parent_pos.into_boxed_slice(),
2539            parent_uses_rowid,
2540            parent_unique_index,
2541        })
2542    }
2543
2544    /// Returns if any table declares a FOREIGN KEY whose parent is `table_name`.
2545    pub fn any_resolved_fks_referencing(&self, table_name: &str) -> bool {
2546        self.tables.values().any(|t| {
2547            let Some(bt) = t.btree() else {
2548                return false;
2549            };
2550            bt.foreign_keys
2551                .iter()
2552                .any(|fk| fk.parent_table == table_name)
2553        })
2554    }
2555
2556    /// Returns true if `table_name` declares any FOREIGN KEYs
2557    pub fn has_child_fks(&self, table_name: &str) -> bool {
2558        self.get_table(table_name)
2559            .and_then(|t| t.btree())
2560            .is_some_and(|t| !t.foreign_keys.is_empty())
2561    }
2562
2563    fn check_object_name_conflict(&self, name: &str) -> Result<()> {
2564        if let Some(object_type) = self.get_object_type(name) {
2565            let type_str = match object_type {
2566                SchemaObjectType::Table => "table",
2567                SchemaObjectType::View => "view",
2568                SchemaObjectType::Index => "index",
2569            };
2570            return Err(crate::LimboError::ParseError(format!(
2571                "{type_str} \"{name}\" already exists"
2572            )));
2573        }
2574        Ok(())
2575    }
2576
2577    pub fn get_sequence(&self, name: &str) -> Option<&Arc<Sequence>> {
2578        self.sequences.get(&normalize_ident(name))
2579    }
2580
2581    /// Remove a sequence and its backing table from the in-memory schema.
2582    pub fn remove_sequence(&mut self, name: &str) {
2583        let normalized = normalize_ident(name);
2584        self.sequences.remove(&normalized);
2585        let backing_table = crate::translate::sequence::sequence_backing_table_name(&normalized);
2586        self.tables.remove(&backing_table);
2587    }
2588
2589    /// Returns the type of schema object with the given name, if one exists.
2590    /// Checks tables, views, and indexes.
2591    pub fn get_object_type(&self, name: &str) -> Option<SchemaObjectType> {
2592        let normalized_name = self.normalize_table_lookup_name(name);
2593
2594        if self.tables.contains_key(&normalized_name) {
2595            return Some(SchemaObjectType::Table);
2596        }
2597
2598        if self.views.contains_key(&normalized_name) {
2599            return Some(SchemaObjectType::View);
2600        }
2601
2602        for index_list in self.indexes.values() {
2603            if index_list.iter().any(|i| i.name.eq_ignore_ascii_case(name)) {
2604                return Some(SchemaObjectType::Index);
2605            }
2606        }
2607
2608        None
2609    }
2610}
2611
2612impl TryClone for UniqueSet {
2613    type Error = TryReserveError;
2614
2615    fn try_clone(&self) -> Result<Self, Self::Error> {
2616        Ok(Self {
2617            columns: self.columns.try_clone()?,
2618            collations: self.collations.try_clone()?,
2619            is_primary_key: self.is_primary_key,
2620            conflict_clause: self.conflict_clause,
2621        })
2622    }
2623}
2624
2625// Copy enums stored as `Vec` elements: their clone cannot allocate.
2626crate::alloc::impl_try_clone_via_clone!(
2627    turso_parser::ast::SortOrder,
2628    crate::translate::collate::CollationSeq,
2629);
2630
2631// Std-pinned schema element types: every owned field allocates through the
2632// std global allocator (Strings, `std::vec::Vec`, boxed parser AST), so
2633// forwarding to `Clone` is correct today. TODO(alloc): give these real
2634// fallible impls when their fields become allocator-aware.
2635crate::alloc::impl_try_clone_via_clone!(Column, IndexColumn, CheckConstraint);
2636
2637impl Schema {
2638    #[turso_macros::allocation_site(crate::alloc::SchemaAllocationSite::MakeMut)]
2639    pub(crate) fn try_make_mut(schema: &mut Arc<Self>) -> Result<&mut Self, TryReserveError> {
2640        if Arc::get_mut(schema).is_none() {
2641            *schema = Arc::new(schema.as_ref().try_clone()?);
2642        }
2643        Ok(Arc::get_mut(schema).expect("schema was made unique above"))
2644    }
2645}
2646
2647impl TryClone for View {
2648    type Error = TryReserveError;
2649
2650    fn try_clone(&self) -> Result<Self, Self::Error> {
2651        Ok(Self {
2652            name: self.name.clone(),
2653            sql: self.sql.clone(),
2654            select_stmt: self.select_stmt.clone(),
2655            columns: self.columns.try_clone()?,
2656            state: AtomicViewState::new(ViewState::Ready),
2657        })
2658    }
2659}
2660
2661impl TryClone for VirtualTable {
2662    type Error = TryReserveError;
2663
2664    fn try_clone(&self) -> Result<Self, Self::Error> {
2665        Ok(Self {
2666            name: self.name.clone(),
2667            columns: self.columns.try_clone()?,
2668            kind: self.kind,
2669            vtab_type: self.vtab_type.clone(),
2670            vtab_id: self.vtab_id,
2671            innocuous: self.innocuous,
2672        })
2673    }
2674}
2675
2676impl TryClone for BTreeTable {
2677    type Error = TryReserveError;
2678
2679    fn try_clone(&self) -> Result<Self, Self::Error> {
2680        Ok(Self {
2681            root_page: self.root_page,
2682            name: self.name.clone(),
2683            primary_key_columns: self.primary_key_columns.try_clone()?,
2684            columns: self.columns.try_clone()?,
2685            has_rowid: self.has_rowid,
2686            is_strict: self.is_strict,
2687            has_autoincrement: self.has_autoincrement,
2688            unique_sets: self.unique_sets.try_clone()?,
2689            foreign_keys: self.foreign_keys.try_clone()?,
2690            check_constraints: self.check_constraints.try_clone()?,
2691            rowid_alias_conflict_clause: self.rowid_alias_conflict_clause,
2692            has_virtual_columns: self.has_virtual_columns,
2693            logical_to_physical_map: self.logical_to_physical_map.try_clone()?,
2694            column_dependencies: Default::default(),
2695        })
2696    }
2697}
2698
2699impl TryClone for FromClauseSubquery {
2700    type Error = TryReserveError;
2701
2702    fn try_clone(&self) -> Result<Self, Self::Error> {
2703        Ok(Self {
2704            name: self.name.clone(),
2705            plan: self.plan.clone(),
2706            columns: self.columns.try_clone()?,
2707            result_columns_start_reg: self.result_columns_start_reg,
2708            materialized_cursor_id: self.materialized_cursor_id,
2709            cte: self.cte,
2710        })
2711    }
2712}
2713
2714impl TryClone for Table {
2715    type Error = TryReserveError;
2716
2717    fn try_clone(&self) -> Result<Self, Self::Error> {
2718        Ok(match self {
2719            Table::BTree(table) => Table::BTree(Arc::new(table.as_ref().try_clone()?)),
2720            Table::Virtual(table) => Table::Virtual(Arc::new(table.as_ref().try_clone()?)),
2721            Table::FromClauseSubquery(from_clause_subquery) => {
2722                Table::FromClauseSubquery(Arc::new(from_clause_subquery.as_ref().try_clone()?))
2723            }
2724        })
2725    }
2726}
2727
2728impl TryClone for Index {
2729    type Error = TryReserveError;
2730
2731    fn try_clone(&self) -> Result<Self, Self::Error> {
2732        Ok(Self {
2733            name: self.name.clone(),
2734            table_name: self.table_name.clone(),
2735            root_page: self.root_page,
2736            columns: self.columns.try_clone()?,
2737            unique: self.unique,
2738            ephemeral: self.ephemeral,
2739            has_rowid: self.has_rowid,
2740            where_clause: self.where_clause.clone(),
2741            index_method: self.index_method.clone(),
2742            on_conflict: self.on_conflict,
2743        })
2744    }
2745}
2746
2747impl TryClone for Schema {
2748    /// Copying a `Schema` requires deep cloning of all internal tables and indexes, even though they are wrapped in `Arc`.
2749    /// Simply copying the `Arc` pointers would result in multiple `Schema` instances sharing the same underlying tables and indexes,
2750    /// which could lead to panics or data races if any instance attempts to modify them.
2751    /// To ensure each `Schema` is independent and safe to modify, we clone the underlying data for all tables and indexes.
2752    type Error = TryReserveError;
2753
2754    fn try_clone(&self) -> Result<Self, Self::Error> {
2755        let tables = self
2756            .tables
2757            .iter()
2758            .map(|(name, table)| {
2759                Ok::<_, TryReserveError>((name.clone(), Arc::new(table.as_ref().try_clone()?)))
2760            })
2761            .try_collect::<Result<_, TryReserveError>>()??;
2762        let indexes = self
2763            .indexes
2764            .iter()
2765            .map(|(name, indexes)| {
2766                let indexes = indexes
2767                    .iter()
2768                    .map(|index| index.as_ref().try_clone().map(Arc::new))
2769                    .try_collect::<Result<VecDeque<_>, TryReserveError>>()??;
2770                Ok::<_, TryReserveError>((name.clone(), indexes))
2771            })
2772            .try_collect::<Result<_, TryReserveError>>()??;
2773        let materialized_view_names = self.materialized_view_names.try_clone()?;
2774        let materialized_view_sql = self.materialized_view_sql.try_clone()?;
2775        let incremental_views = self
2776            .incremental_views
2777            .iter()
2778            .map(|(name, view)| (name.clone(), view.clone()))
2779            .try_collect()?;
2780        let views = self
2781            .views
2782            .iter()
2783            .map(|(name, view)| {
2784                Ok::<_, TryReserveError>((name.clone(), Arc::new(view.as_ref().try_clone()?)))
2785            })
2786            .try_collect::<Result<_, TryReserveError>>()??;
2787        let triggers = self
2788            .triggers
2789            .iter()
2790            .map(|(table_name, triggers)| {
2791                Ok::<_, TryReserveError>((
2792                    table_name.clone(),
2793                    triggers
2794                        .iter()
2795                        .map(|t| Arc::new((**t).clone()))
2796                        .try_collect()?,
2797                ))
2798            })
2799            .try_collect::<Result<_, TryReserveError>>()??;
2800        let incompatible_views = self.incompatible_views.try_clone()?;
2801        Ok(Self {
2802            tables,
2803            #[cfg(feature = "conn_raw_api")]
2804            table_names_by_root_page: self.table_names_by_root_page.try_clone()?,
2805            materialized_view_names,
2806            materialized_view_sql,
2807            incremental_views,
2808            views,
2809            triggers,
2810            indexes,
2811            has_indexes: self.has_indexes.try_clone()?,
2812            schema_version: self.schema_version,
2813            analyze_stats: self.analyze_stats.clone(),
2814            table_to_materialized_views: self.table_to_materialized_views.try_clone()?,
2815            incompatible_views,
2816            broken_views: self.broken_views.try_clone()?,
2817            dropped_root_pages: self.dropped_root_pages.try_clone()?,
2818            type_registry: self.type_registry.try_clone()?,
2819            generated_columns_enabled: self.generated_columns_enabled,
2820            sequences: self.sequences.try_clone()?,
2821        })
2822    }
2823}
2824
2825/// Maps schema column indices to register offsets for DML operations.
2826//TODO this should be integrated into a Columns domain type
2827// This type should also replace BTreeTable::has_virtual_columns
2828#[derive(Debug, Clone)]
2829pub enum ColumnLayout {
2830    Identity {
2831        column_count: usize,
2832    },
2833    Mapped {
2834        // col_index -> offset
2835        offsets: Vec<usize>,
2836        non_virtual_col_count: usize,
2837    },
2838}
2839
2840impl ColumnLayout {
2841    pub fn from_table(table: &Table) -> Result<Self, TryReserveError> {
2842        match table {
2843            Table::BTree(btree) => Self::from_btree(btree),
2844            Table::Virtual(vtable) => Ok(Self::Identity {
2845                column_count: vtable.as_ref().columns.len(),
2846            }),
2847            Table::FromClauseSubquery(subquery) => Ok(Self::Identity {
2848                column_count: subquery.columns.len(),
2849            }),
2850        }
2851    }
2852
2853    pub fn from_btree(btree: &BTreeTable) -> Result<Self, TryReserveError> {
2854        let total = btree.columns.len();
2855        let non_virtual_col_count = btree
2856            .columns
2857            .iter()
2858            .filter(|c| !c.is_virtual_generated())
2859            .count();
2860        let offsets = btree.logical_to_physical_map.try_clone()?;
2861        let is_identity = non_virtual_col_count == total && offsets.iter().copied().eq(0..total);
2862        if is_identity {
2863            Ok(Self::Identity {
2864                column_count: total,
2865            })
2866        } else {
2867            Ok(Self::Mapped {
2868                offsets,
2869                non_virtual_col_count,
2870            })
2871        }
2872    }
2873
2874    pub fn from_columns(columns: &[Column]) -> Result<Self, TryReserveError> {
2875        let total = columns.len();
2876        let non_virtual_col_count = columns.iter().filter(|c| !c.is_virtual_generated()).count();
2877        if non_virtual_col_count == total {
2878            return Ok(Self::Identity {
2879                column_count: total,
2880            });
2881        }
2882        let mut offsets = try_vec![0usize; total]?;
2883        let mut nv_idx = 0;
2884        let mut v_idx = non_virtual_col_count;
2885        for (i, col) in columns.iter().enumerate() {
2886            if col.is_virtual_generated() {
2887                offsets[i] = v_idx;
2888                v_idx += 1;
2889            } else {
2890                offsets[i] = nv_idx;
2891                nv_idx += 1;
2892            }
2893        }
2894        Ok(Self::Mapped {
2895            offsets,
2896            non_virtual_col_count,
2897        })
2898    }
2899
2900    /// Map a schema column index to its register offset.
2901    #[inline(always)]
2902    pub fn to_reg_offset(&self, col_idx: usize) -> usize {
2903        match self {
2904            Self::Identity { .. } => col_idx,
2905            Self::Mapped { offsets, .. } => offsets[col_idx],
2906        }
2907    }
2908
2909    /// Resolve schema column index to an absolute register.
2910    #[inline(always)]
2911    pub fn to_register(&self, base: usize, schema_idx: usize) -> usize {
2912        base + self.to_reg_offset(schema_idx)
2913    }
2914
2915    #[inline(always)]
2916    pub fn num_non_virtual_cols(&self) -> usize {
2917        match self {
2918            Self::Identity {
2919                column_count: total,
2920            } => *total,
2921            Self::Mapped {
2922                non_virtual_col_count,
2923                ..
2924            } => *non_virtual_col_count,
2925        }
2926    }
2927
2928    #[inline(always)]
2929    pub fn column_count(&self) -> usize {
2930        match self {
2931            Self::Identity {
2932                column_count: total,
2933            } => *total,
2934            Self::Mapped { offsets, .. } => offsets.len(),
2935        }
2936    }
2937
2938    pub fn column_idx_for_offset(&self, offset: usize) -> Option<usize> {
2939        match self {
2940            Self::Identity { column_count } => {
2941                if offset < *column_count {
2942                    Some(offset)
2943                } else {
2944                    None
2945                }
2946            }
2947            Self::Mapped { offsets, .. } => offsets.iter().position(|&s| s == offset),
2948        }
2949    }
2950}
2951
2952#[derive(Clone, Debug)]
2953pub enum Table {
2954    BTree(Arc<BTreeTable>),
2955    Virtual(Arc<VirtualTable>),
2956    FromClauseSubquery(Arc<FromClauseSubquery>),
2957}
2958
2959impl Table {
2960    pub fn get_root_page(&self) -> crate::Result<i64> {
2961        match self {
2962            Table::BTree(table) => Ok(table.root_page),
2963            Table::Virtual(_) => Err(crate::LimboError::InternalError(
2964                "Virtual tables do not have a root page".to_string(),
2965            )),
2966            Table::FromClauseSubquery(_) => Err(crate::LimboError::InternalError(
2967                "FROM clause subqueries do not have a root page".to_string(),
2968            )),
2969        }
2970    }
2971
2972    pub fn get_name(&self) -> &str {
2973        match self {
2974            Self::BTree(table) => &table.name,
2975            Self::Virtual(table) => &table.name,
2976            Self::FromClauseSubquery(from_clause_subquery) => &from_clause_subquery.name,
2977        }
2978    }
2979
2980    pub fn get_column_at(&self, index: usize) -> Option<&Column> {
2981        match self {
2982            Self::BTree(table) => table.columns.get(index),
2983            Self::Virtual(table) => table.columns.get(index),
2984            Self::FromClauseSubquery(from_clause_subquery) => {
2985                from_clause_subquery.columns.get(index)
2986            }
2987        }
2988    }
2989
2990    /// Returns the column position and column for a given column name.
2991    pub fn get_column_by_name(&self, name: &str) -> Option<(usize, &Column)> {
2992        match self {
2993            Self::BTree(table) => table.get_column(name),
2994            Self::Virtual(table) => table.columns.iter().enumerate().find(|(_, col)| {
2995                col.name
2996                    .as_ref()
2997                    .is_some_and(|n| n.eq_ignore_ascii_case(name))
2998            }),
2999            Self::FromClauseSubquery(from_clause_subquery) => from_clause_subquery
3000                .columns
3001                .iter()
3002                .enumerate()
3003                .find(|(_, col)| {
3004                    col.name
3005                        .as_ref()
3006                        .is_some_and(|n| n.eq_ignore_ascii_case(name))
3007                }),
3008        }
3009    }
3010
3011    pub fn columns(&self) -> &[Column] {
3012        match self {
3013            Self::BTree(table) => &table.columns,
3014            Self::Virtual(table) => &table.columns,
3015            Self::FromClauseSubquery(from_clause_subquery) => &from_clause_subquery.columns,
3016        }
3017    }
3018
3019    pub fn is_strict(&self) -> bool {
3020        match self {
3021            Self::BTree(table) => table.is_strict,
3022            Self::Virtual(_) => false,
3023            Self::FromClauseSubquery(_) => false,
3024        }
3025    }
3026
3027    pub fn btree(&self) -> Option<Arc<BTreeTable>> {
3028        match self {
3029            Self::BTree(table) => Some(table.clone()),
3030            Self::Virtual(_) => None,
3031            Self::FromClauseSubquery(_) => None,
3032        }
3033    }
3034
3035    /// Like `btree()` but returns an error instead of None.
3036    pub fn require_btree(&self) -> crate::Result<Arc<BTreeTable>> {
3037        self.btree().ok_or_else(|| {
3038            crate::LimboError::InternalError(
3039                "operation requires a btree table, not a virtual table".into(),
3040            )
3041        })
3042    }
3043
3044    pub fn btree_mut(&mut self) -> Option<&mut Arc<BTreeTable>> {
3045        match self {
3046            Self::BTree(table) => Some(table),
3047            Self::Virtual(_) => None,
3048            Self::FromClauseSubquery(_) => None,
3049        }
3050    }
3051
3052    pub fn virtual_table(&self) -> Option<Arc<VirtualTable>> {
3053        match self {
3054            Self::Virtual(table) => Some(table.clone()),
3055            _ => None,
3056        }
3057    }
3058}
3059
3060impl PartialEq for Table {
3061    fn eq(&self, other: &Self) -> bool {
3062        match (self, other) {
3063            (Self::BTree(a), Self::BTree(b)) => Arc::ptr_eq(a, b),
3064            (Self::Virtual(a), Self::Virtual(b)) => Arc::ptr_eq(a, b),
3065            _ => false,
3066        }
3067    }
3068}
3069
3070#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
3071pub struct UniqueSet {
3072    pub columns: Vec<(String, SortOrder)>,
3073    /// Per-column collation overrides from the constraint definition,
3074    /// e.g. `PRIMARY KEY(a COLLATE NOCASE)`. Parallel to `columns`; `None`
3075    /// falls back to the column definition's collation.
3076    pub collations: Vec<Option<CollationSeq>>,
3077    pub is_primary_key: bool,
3078    pub conflict_clause: Option<ResolveType>,
3079}
3080
3081#[derive(Clone, Debug)]
3082pub struct CheckConstraint {
3083    /// Optional constraint name
3084    pub name: Option<String>,
3085    /// CHECK expression
3086    pub expr: ast::Expr,
3087    /// Column name if this is a column-level CHECK constraint (defined inline with the column).
3088    /// None if this is a table-level CHECK constraint.
3089    pub column: Option<String>,
3090}
3091
3092impl CheckConstraint {
3093    pub fn new(name: Option<&ast::Name>, expr: &ast::Expr, column: Option<&str>) -> Self {
3094        Self {
3095            name: name.map(|n| n.as_str().to_string()),
3096            expr: expr.clone(),
3097            column: column.map(|s| s.to_string()),
3098        }
3099    }
3100
3101    /// Returns the SQL representation of this CHECK constraint (e.g. `CHECK(x > 0)`).
3102    pub fn sql(&self) -> String {
3103        format!("CHECK({})", self.expr)
3104    }
3105}
3106
3107/// RAII wrapper that resets its inner value when cloned.
3108#[derive(Debug, Default)]
3109pub struct ResetOnClone<T: Default>(T);
3110
3111impl<T: Default> Clone for ResetOnClone<T> {
3112    fn clone(&self) -> Self {
3113        Self(T::default())
3114    }
3115}
3116
3117bitflags! {
3118    #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3119    pub struct BTreeCharacteristics: u8 {
3120        /// Table has a rowid column (i.e. not `WITHOUT ROWID`).
3121        const HAS_ROWID         = 0b0000_0001;
3122        /// Table is declared `STRICT`.
3123        const STRICT            = 0b0000_0010;
3124        /// Table has an `AUTOINCREMENT` column.
3125        const HAS_AUTOINCREMENT = 0b0000_0100;
3126    }
3127}
3128
3129#[derive(Debug)]
3130pub(crate) struct GeneratedColGraph {
3131    /// `dependencies[j]` = columns `j` transitively reads from (excludes `j`).
3132    dependencies: Vec<ColumnMask>,
3133    /// `dependents[i]` = columns that transitively read from `i` (excludes `i`).
3134    dependents: Vec<ColumnMask>,
3135    /// Column indices in topological (dependency) order. Contains all columns.
3136    topological_sort: Vec<usize>,
3137}
3138
3139impl GeneratedColGraph {
3140    fn build(columns: &[Column]) -> Result<Self> {
3141        let n = columns.len();
3142
3143        let mut direct_deps = try_vec![ColumnMask::default(); n]?;
3144        let mut direct_dependents = try_vec![ColumnMask::default(); n]?;
3145        let mut in_degree: Vec<u32> = try_vec![0; n]?;
3146
3147        // walk each virtual column's expression once to extract edges
3148        for (j, col) in columns.iter().enumerate() {
3149            let GeneratedType::Virtual { ref expr, .. } = col.generated_type() else {
3150                continue;
3151            };
3152            let mut direct = BitSet::default();
3153            collect_column_dependencies_of_gencol(expr, columns, &mut direct);
3154            if direct.get(j) {
3155                bail_parse_error!(
3156                    "generated column \"{}\" cannot reference itself",
3157                    col.name.as_deref().unwrap_or("?")
3158                );
3159            }
3160            let direct_mask: ColumnMask = ColumnMask::try_from_iter(direct.iter())?;
3161            direct_deps[j].union_with(&direct_mask)?;
3162            for i in direct.iter() {
3163                direct_dependents[i].set(j)?;
3164                in_degree[j] += 1;
3165            }
3166        }
3167
3168        // Kahn's algorithm (topological sort) over direct_deps.
3169        let mut topological_sort: Vec<usize> = Vec::try_with_capacity_ext(n)?;
3170        let mut ready: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).try_collect()?;
3171        while let Some(i) = ready.pop() {
3172            topological_sort.try_push(i)?;
3173            for j in direct_dependents[i].iter() {
3174                in_degree[j] -= 1;
3175                if in_degree[j] == 0 {
3176                    ready.try_push(j)?;
3177                }
3178            }
3179        }
3180
3181        // see if there's cycles in the graph
3182        if topological_sort.len() != n {
3183            let cycle_names: Vec<&str> = (0..n)
3184                .filter(|i| in_degree[*i] > 0)
3185                .filter_map(|i| columns[i].name.as_deref())
3186                .try_collect()?;
3187            bail_parse_error!(
3188                "circular dependency in generated columns: {}",
3189                cycle_names.join(", ")
3190            );
3191        }
3192
3193        // compute transitive closures.
3194        let mut dependencies = try_vec![ColumnMask::default(); n]?;
3195        for &j in &topological_sort {
3196            dependencies[j] = direct_deps[j].try_clone()?;
3197            for i in direct_deps[j].iter() {
3198                let snapshot = dependencies[i].try_clone()?;
3199                dependencies[j].union_with(&snapshot)?;
3200            }
3201        }
3202
3203        // compute transitive closures of the transpose graph (dependents)
3204        let mut dependents = try_vec![ColumnMask::default(); n]?;
3205        for &i in topological_sort.iter().rev() {
3206            dependents[i] = direct_dependents[i].try_clone()?;
3207            for j in direct_dependents[i].iter() {
3208                let snapshot = dependents[j].try_clone()?;
3209                dependents[i].union_with(&snapshot)?;
3210            }
3211        }
3212
3213        Ok(Self {
3214            dependencies,
3215            dependents,
3216            topological_sort,
3217        })
3218    }
3219}
3220
3221#[derive(Clone, Debug)]
3222pub struct BTreeTable {
3223    pub root_page: i64,
3224    pub name: String,
3225    pub primary_key_columns: Vec<(String, SortOrder)>,
3226    columns: Vec<Column>,
3227    pub has_rowid: bool,
3228    pub is_strict: bool,
3229    pub has_autoincrement: bool,
3230    pub unique_sets: Vec<UniqueSet>,
3231    pub foreign_keys: Vec<Arc<ForeignKey>>,
3232    pub check_constraints: Vec<CheckConstraint>,
3233    /// ON CONFLICT clause for the INTEGER PRIMARY KEY constraint.
3234    /// Stored here because rowid-alias PKs have their UniqueSet removed.
3235    pub rowid_alias_conflict_clause: Option<ResolveType>,
3236    pub has_virtual_columns: bool,
3237    pub logical_to_physical_map: Vec<usize>,
3238    column_dependencies: ResetOnClone<OnceLock<GeneratedColGraph>>,
3239}
3240
3241pub struct ColumnsMut<'a> {
3242    table: &'a mut BTreeTable,
3243}
3244
3245impl std::ops::Deref for ColumnsMut<'_> {
3246    type Target = Vec<Column>;
3247    fn deref(&self) -> &Vec<Column> {
3248        &self.table.columns
3249    }
3250}
3251
3252impl std::ops::DerefMut for ColumnsMut<'_> {
3253    fn deref_mut(&mut self) -> &mut Vec<Column> {
3254        &mut self.table.columns
3255    }
3256}
3257
3258impl Drop for ColumnsMut<'_> {
3259    fn drop(&mut self) {
3260        self.table.column_dependencies.0 = OnceLock::new();
3261        self.table.has_virtual_columns =
3262            self.table.columns.iter().any(|c| c.is_virtual_generated());
3263        self.table.logical_to_physical_map = BTreeTable::build_logical_to_physical_map(
3264            &self.table.columns,
3265            &self.table.primary_key_columns,
3266            self.table.has_rowid,
3267        );
3268    }
3269}
3270
3271impl BTreeTable {
3272    #[allow(clippy::too_many_arguments)]
3273    pub fn new(
3274        root_page: i64,
3275        name: String,
3276        primary_key_columns: Vec<(String, SortOrder)>,
3277        columns: Vec<Column>,
3278        characteristics: BTreeCharacteristics,
3279        unique_sets: Vec<UniqueSet>,
3280        foreign_keys: Vec<Arc<ForeignKey>>,
3281        check_constraints: Vec<CheckConstraint>,
3282        rowid_alias_conflict_clause: Option<ResolveType>,
3283    ) -> Self {
3284        let has_virtual_columns = columns.iter().any(|c| c.is_virtual_generated());
3285        let has_rowid = characteristics.contains(BTreeCharacteristics::HAS_ROWID);
3286        let logical_to_physical_map =
3287            Self::build_logical_to_physical_map(&columns, &primary_key_columns, has_rowid);
3288        Self {
3289            root_page,
3290            name,
3291            primary_key_columns,
3292            columns,
3293            has_rowid,
3294            is_strict: characteristics.contains(BTreeCharacteristics::STRICT),
3295            has_autoincrement: characteristics.contains(BTreeCharacteristics::HAS_AUTOINCREMENT),
3296            unique_sets,
3297            foreign_keys,
3298            check_constraints,
3299            rowid_alias_conflict_clause,
3300            has_virtual_columns,
3301            logical_to_physical_map,
3302            column_dependencies: Default::default(),
3303        }
3304    }
3305
3306    pub fn columns(&self) -> &[Column] {
3307        &self.columns
3308    }
3309
3310    pub fn columns_mut(&mut self) -> ColumnsMut<'_> {
3311        ColumnsMut { table: self }
3312    }
3313
3314    /// Create a table reference for TypeCheck where custom type columns have
3315    /// their `ty_str` replaced with the base type name, and where virtual columns
3316    /// are skipped. This ensures TypeCheck validates the encoded value against the
3317    /// correct base type (e.g., BLOB) rather than accepting any STRICT type via the wildcard arm.
3318    pub fn type_check_table_ref(table: &Arc<BTreeTable>, schema: &Schema) -> Arc<BTreeTable> {
3319        let has_virtual = table.has_virtual_columns();
3320        let has_custom = table
3321            .columns
3322            .iter()
3323            .any(|c| c.is_array() || schema.get_type_def(&c.ty_str, table.is_strict).is_some());
3324        if !has_custom && !has_virtual {
3325            return Arc::clone(table);
3326        }
3327        let mut modified = (**table).clone();
3328        if has_virtual {
3329            modified.columns.retain(|c| !c.is_virtual_generated());
3330            modified.has_virtual_columns = false;
3331        }
3332        for col in &mut modified.columns {
3333            if col.is_array() {
3334                // Arrays are stored as record-format blobs.
3335                col.ty_str = "BLOB".to_string();
3336            } else if let Ok(Some(resolved)) = schema.resolve_type(&col.ty_str, table.is_strict) {
3337                col.ty_str = resolved.primitive.to_uppercase();
3338            }
3339        }
3340        Arc::new(modified)
3341    }
3342
3343    /// Create a table ref for pre-encode TypeCheck that validates user input
3344    /// against the type's declared `value` input type (or base if not declared).
3345    /// For UPDATE, `only_columns` limits which columns are checked — non-SET
3346    /// columns hold encoded values and must be skipped (set to ANY).
3347    pub fn input_type_check_table_ref(
3348        table: &Arc<BTreeTable>,
3349        schema: &Schema,
3350        only_columns: Option<&ColumnMask>,
3351    ) -> Result<Arc<BTreeTable>> {
3352        let has_virtual = table.has_virtual_columns();
3353        let has_custom = table
3354            .columns
3355            .iter()
3356            .any(|c| c.is_array() || schema.get_type_def(&c.ty_str, table.is_strict).is_some());
3357        if !has_custom && !has_virtual {
3358            return Ok(Arc::clone(table));
3359        }
3360        let mut modified = (**table).clone();
3361        let remapped_only_columns = if has_virtual {
3362            let remapped = only_columns
3363                .map(|only| {
3364                    let mut new_set = ColumnMask::default();
3365                    let mut physical = 0usize;
3366                    for (orig, col) in modified.columns.iter().enumerate() {
3367                        if col.is_virtual_generated() {
3368                            continue;
3369                        }
3370                        if only.get(orig) {
3371                            new_set.set(physical)?;
3372                        }
3373                        physical += 1;
3374                    }
3375                    Ok::<_, LimboError>(new_set)
3376                })
3377                .transpose()?;
3378            modified.columns.retain(|c| !c.is_virtual_generated());
3379            modified.has_virtual_columns = false;
3380            remapped
3381        } else {
3382            None
3383        };
3384        let effective_only = remapped_only_columns.as_ref().or(only_columns);
3385        for (i, col) in modified.columns.iter_mut().enumerate() {
3386            if let Some(only) = effective_only {
3387                if !only.get(i) {
3388                    col.ty_str = "ANY".to_string();
3389                    continue;
3390                }
3391            }
3392            if col.is_array() {
3393                // Pre-encode: user input can be text ('[1,2]') or blob (ARRAY[]),
3394                // so accept ANY here; the encoder handles conversion.
3395                col.ty_str = "ANY".to_string();
3396            } else if let Some(type_def) = schema.get_type_def(&col.ty_str, table.is_strict) {
3397                col.ty_str = type_def.value_input_type().to_uppercase();
3398            }
3399        }
3400        Ok(Arc::new(modified))
3401    }
3402
3403    /// Override column type metadata for custom type columns so that
3404    /// SQLite's name-based type/affinity rules use the BASE type
3405    /// instead of the custom type name (e.g. "doubled" contains "DOUB"
3406    /// which would incorrectly map to REAL instead of INTEGER).
3407    pub fn resolve_custom_type_affinities(&mut self, schema: &Schema) {
3408        if !self.is_strict {
3409            return;
3410        }
3411        for col in &mut self.columns {
3412            if col.is_array() {
3413                // Arrays are stored as record-format blobs regardless of element type.
3414                col.set_ty(Type::Blob);
3415                col.set_base_affinity(Affinity::Blob);
3416                continue;
3417            }
3418            if let Ok(Some(resolved)) = schema.resolve_type_unchecked(&col.ty_str) {
3419                let (base_ty, _) = type_from_name(&resolved.primitive);
3420                col.set_ty(base_ty);
3421                col.set_base_affinity(Affinity::affinity(&resolved.primitive));
3422            }
3423        }
3424    }
3425
3426    /// Propagate domain NOT NULL and CHECK constraints to table columns.
3427    /// For each column whose type resolves to a domain, this:
3428    /// - Sets the column's NOT NULL flag if any domain in the chain has NOT NULL
3429    /// - Adds domain CHECK constraints (with `value` rewritten to the column name)
3430    ///   to the table's check_constraints list
3431    pub fn propagate_domain_constraints(&mut self, schema: &Schema) -> Result<()> {
3432        if !self.is_strict {
3433            return Ok(());
3434        }
3435        // Collect new constraints and notnull flags to avoid borrowing issues
3436        let mut new_checks = vec![];
3437        let mut notnull_cols = vec![];
3438
3439        for (col_idx, col) in self.columns.iter().enumerate() {
3440            let Ok(Some(resolved)) = schema.resolve_type_unchecked(&col.ty_str) else {
3441                continue;
3442            };
3443            if !resolved.is_domain() {
3444                continue;
3445            }
3446            let col_name = col.name.as_deref().unwrap_or("").to_string();
3447            for td in &resolved.chain {
3448                if td.not_null {
3449                    notnull_cols.try_push(col_idx)?;
3450                }
3451                for (i, dc) in td.domain_checks.iter().enumerate() {
3452                    let rewritten = rewrite_value_to_column(&dc.check, &col_name);
3453                    let name = dc
3454                        .name
3455                        .clone()
3456                        .unwrap_or_else(|| format!("{}_{}", td.name, i));
3457                    new_checks.try_push(CheckConstraint {
3458                        name: Some(name),
3459                        expr: *rewritten,
3460                        column: Some(col_name.clone()),
3461                    })?;
3462                }
3463            }
3464        }
3465
3466        for col_idx in notnull_cols {
3467            self.columns[col_idx].set_notnull(true);
3468        }
3469        self.check_constraints.try_extend(new_checks)?;
3470        Ok(())
3471    }
3472
3473    pub fn get_rowid_alias_column(&self) -> Option<(usize, &Column)> {
3474        self.columns
3475            .iter()
3476            .enumerate()
3477            .find(|(_, column)| column.is_rowid_alias())
3478    }
3479
3480    pub fn has_virtual_columns(&self) -> bool {
3481        self.has_virtual_columns
3482    }
3483
3484    /// Build a `ColumnLayout` for this table's register mapping.
3485    pub fn column_layout(&self) -> Result<ColumnLayout, TryReserveError> {
3486        ColumnLayout::from_btree(self)
3487    }
3488
3489    /// Returns the column position and column for a given column name.
3490    /// Returns None if the column name is not found.
3491    /// E.g. if table is CREATE TABLE t (a, b, c)
3492    /// then get_column("b") returns (1, &Column { .. })
3493    pub fn get_column(&self, name: &str) -> Option<(usize, &Column)> {
3494        self.columns.iter().enumerate().find(|(_, column)| {
3495            column
3496                .name
3497                .as_ref()
3498                .is_some_and(|n| n.eq_ignore_ascii_case(name))
3499        })
3500    }
3501
3502    pub fn from_sql(sql: &str, root_page: i64) -> Result<BTreeTable> {
3503        let mut parser = Parser::new(sql.as_bytes());
3504        let cmd = parser.next_cmd()?;
3505        match cmd {
3506            Some(Cmd::Stmt(Stmt::CreateTable { tbl_name, body, .. })) => {
3507                create_table(tbl_name.name.as_str(), &body, root_page)
3508            }
3509            _ => unreachable!("Expected CREATE TABLE statement"),
3510        }
3511    }
3512
3513    /// Reconstruct the SQL for the table.
3514    /// FIXME: this makes us incompatible with SQLite since sqlite stores the user-provided SQL as is in
3515    /// `sqlite_schema.sql`
3516    /// For example, if a user creates a table like: `CREATE TABLE t              (x)`, we store it as
3517    /// `CREATE TABLE t (x)`, whereas sqlite stores it with the original extra whitespace.
3518    pub fn to_sql(&self) -> String {
3519        let mut sql = format!("CREATE TABLE {} (", quote_ident(&self.name));
3520        let needs_pk_inline = self.primary_key_columns.len() == 1;
3521        // Add columns
3522        for (i, column) in self.columns.iter().enumerate() {
3523            if i > 0 {
3524                sql.push_str(", ");
3525            }
3526
3527            let column_name = column.name.as_ref().expect("column name is None");
3528            sql.push_str(&quote_ident(column_name));
3529
3530            if !column.ty_str.is_empty() {
3531                sql.push(' ');
3532                sql.push_str(&column.ty_str);
3533                if column.is_array() {
3534                    sql.push_str("[]");
3535                }
3536            }
3537            if column.notnull()
3538                && (column.explicit_notnull() || !self.is_without_rowid_inline_pk(column))
3539            {
3540                sql.push_str(" NOT NULL");
3541            }
3542
3543            if column.unique() {
3544                sql.push_str(" UNIQUE");
3545            }
3546            if needs_pk_inline && column.primary_key() {
3547                sql.push_str(" PRIMARY KEY");
3548                if self.has_autoincrement && column.is_rowid_alias() {
3549                    sql.push_str(" AUTOINCREMENT");
3550                }
3551            }
3552
3553            if let Some(default) = &column.default {
3554                sql.push_str(" DEFAULT ");
3555                sql.push_str(&default.to_string());
3556            }
3557
3558            if let GeneratedType::Virtual { original_sql, .. } = &column.generated_type() {
3559                sql.push_str(" AS (");
3560                sql.push_str(original_sql);
3561                sql.push(')');
3562            }
3563
3564            // Add column-level CHECK constraints inline
3565            for check_constraint in &self.check_constraints {
3566                if check_constraint.column.as_deref() == Some(column_name) {
3567                    sql.push(' ');
3568                    if let Some(name) = &check_constraint.name {
3569                        sql.push_str("CONSTRAINT ");
3570                        sql.push_str(&Name::exact(name.clone()).as_ident());
3571                        sql.push(' ');
3572                    }
3573                    sql.push_str(&check_constraint.sql());
3574                }
3575            }
3576        }
3577
3578        let has_table_pk = !self.primary_key_columns.is_empty();
3579        // Add table-level PRIMARY KEY constraint if exists
3580        if !needs_pk_inline && has_table_pk {
3581            sql.push_str(", PRIMARY KEY (");
3582            for (i, col) in self.primary_key_columns.iter().enumerate() {
3583                if i > 0 {
3584                    sql.push_str(", ");
3585                }
3586                sql.push_str(&col.0);
3587            }
3588            sql.push(')');
3589        }
3590
3591        for fk in &self.foreign_keys {
3592            sql.push_str(", FOREIGN KEY (");
3593            for (i, col) in fk.child_columns.iter().enumerate() {
3594                if i > 0 {
3595                    sql.push_str(", ");
3596                }
3597                sql.push_str(col);
3598            }
3599            sql.push_str(") REFERENCES ");
3600            sql.push_str(&fk.parent_table);
3601            sql.push('(');
3602            for (i, col) in fk.parent_columns.iter().enumerate() {
3603                if i > 0 {
3604                    sql.push_str(", ");
3605                }
3606                sql.push_str(col);
3607            }
3608            sql.push(')');
3609
3610            // Add ON DELETE/UPDATE actions, NoAction is default so just make empty in that case
3611            if fk.on_delete != RefAct::NoAction {
3612                sql.push_str(" ON DELETE ");
3613                sql.push_str(match fk.on_delete {
3614                    RefAct::SetNull => "SET NULL",
3615                    RefAct::SetDefault => "SET DEFAULT",
3616                    RefAct::Cascade => "CASCADE",
3617                    RefAct::Restrict => "RESTRICT",
3618                    _ => "",
3619                });
3620            }
3621            if fk.on_update != RefAct::NoAction {
3622                sql.push_str(" ON UPDATE ");
3623                sql.push_str(match fk.on_update {
3624                    RefAct::SetNull => "SET NULL",
3625                    RefAct::SetDefault => "SET DEFAULT",
3626                    RefAct::Cascade => "CASCADE",
3627                    RefAct::Restrict => "RESTRICT",
3628                    _ => "",
3629                });
3630            }
3631            if fk.deferred {
3632                sql.push_str(" DEFERRABLE INITIALLY DEFERRED");
3633            }
3634        }
3635
3636        // Add table-level CHECK constraints (column-level ones were emitted inline above)
3637        for check_constraint in &self.check_constraints {
3638            if check_constraint.column.is_some() {
3639                continue;
3640            }
3641            sql.push_str(", ");
3642            if let Some(name) = &check_constraint.name {
3643                sql.push_str("CONSTRAINT ");
3644                sql.push_str(&Name::exact(name.clone()).as_ident());
3645                sql.push(' ');
3646            }
3647            sql.push_str(&check_constraint.sql());
3648        }
3649
3650        // Add table-level UNIQUE constraints
3651        for unique_set in &self.unique_sets {
3652            // Skip primary key (handled above)
3653            if unique_set.is_primary_key {
3654                continue;
3655            }
3656            // Skip single-column unique constraints that were already emitted inline
3657            if unique_set.columns.len() == 1 {
3658                let col_name = &unique_set.columns[0].0;
3659                if let Some((_, col)) = self.get_column(col_name) {
3660                    if col.unique() {
3661                        continue;
3662                    }
3663                }
3664            }
3665            sql.push_str(", UNIQUE (");
3666            for (i, (col_name, _)) in unique_set.columns.iter().enumerate() {
3667                if i > 0 {
3668                    sql.push_str(", ");
3669                }
3670                sql.push_str(&quote_ident(col_name));
3671            }
3672            sql.push(')');
3673        }
3674
3675        sql.push(')');
3676
3677        // Add STRICT keyword if this is a STRICT table
3678        if self.is_strict {
3679            sql.push_str(" STRICT");
3680        }
3681        if !self.has_rowid {
3682            if self.is_strict {
3683                sql.push_str(", WITHOUT ROWID");
3684            } else {
3685                sql.push_str(" WITHOUT ROWID");
3686            }
3687        }
3688
3689        sql
3690    }
3691
3692    fn is_without_rowid_inline_pk(&self, column: &Column) -> bool {
3693        !self.has_rowid && self.primary_key_columns.len() == 1 && column.primary_key()
3694    }
3695
3696    pub fn column_collations(&self) -> Result<Vec<CollationSeq>> {
3697        Ok(self
3698            .columns
3699            .iter()
3700            .map(|column| column.collation())
3701            .try_collect()?)
3702    }
3703
3704    #[inline]
3705    pub fn logical_to_physical_column(&self, logical: usize) -> usize {
3706        self.logical_to_physical_map[logical]
3707    }
3708
3709    pub fn build_logical_to_physical_map(
3710        columns: &[Column],
3711        primary_key_columns: &[(String, SortOrder)],
3712        has_rowid: bool,
3713    ) -> Vec<usize> {
3714        Self::try_build_logical_to_physical_map(columns, primary_key_columns, has_rowid)
3715            .expect(crate::alloc::ALLOC_ERR_MSG)
3716    }
3717
3718    pub fn try_build_logical_to_physical_map(
3719        columns: &[Column],
3720        primary_key_columns: &[(String, SortOrder)],
3721        has_rowid: bool,
3722    ) -> Result<Vec<usize>, crate::alloc::TryReserveError> {
3723        let mut map = try_vec![usize::MAX; columns.len()]?;
3724        let mut physical = 0;
3725
3726        if !has_rowid {
3727            for (pk_name, _) in primary_key_columns {
3728                let Some((pk_idx, col)) = columns.iter().enumerate().find(|(_, col)| {
3729                    col.name
3730                        .as_ref()
3731                        .is_some_and(|name| name.eq_ignore_ascii_case(pk_name))
3732                }) else {
3733                    continue;
3734                };
3735                if col.is_virtual_generated() || map[pk_idx] != usize::MAX {
3736                    continue;
3737                }
3738                map[pk_idx] = physical;
3739                physical += 1;
3740            }
3741        }
3742
3743        for (idx, col) in columns.iter().enumerate() {
3744            if col.is_virtual_generated() || map[idx] != usize::MAX {
3745                continue;
3746            }
3747            map[idx] = physical;
3748            physical += 1;
3749        }
3750
3751        for offset in &mut map {
3752            if *offset == usize::MAX {
3753                *offset = physical;
3754                physical += 1;
3755            }
3756        }
3757        Ok(map)
3758    }
3759
3760    pub fn prepare_generated_columns(&mut self) -> Result<()> {
3761        {
3762            let mut guard = self.columns_mut();
3763            for i in 0..guard.len() {
3764                if guard[i].is_virtual_generated() {
3765                    let mut expr = guard[i].generated_expr().cloned().unwrap();
3766                    resolve_gencol_expr_columns(&mut expr, &guard)?;
3767                    *guard[i].generated_expr_mut().unwrap() = expr;
3768                }
3769            }
3770        }
3771        self.column_graph()?;
3772        Ok(())
3773    }
3774
3775    pub fn shift_generated_column_indices_after_drop(
3776        &mut self,
3777        dropped_index: usize,
3778    ) -> Result<()> {
3779        if !self.has_virtual_columns {
3780            return Ok(());
3781        }
3782
3783        for column in &mut self.columns {
3784            let Some(expr) = column.generated_expr_mut() else {
3785                continue;
3786            };
3787
3788            walk_expr_mut(expr, &mut |e| match e {
3789                Expr::Column {
3790                    table,
3791                    column,
3792                    is_rowid_alias: _,
3793                    ..
3794                } if table.is_self_table() => {
3795                    if *column == dropped_index {
3796                        return Err(LimboError::InternalError(
3797                            "dropped column remained referenced by generated column".to_string(),
3798                        ));
3799                    }
3800                    if *column > dropped_index {
3801                        *column -= 1;
3802                    }
3803                    Ok(WalkControl::Continue)
3804                }
3805                _ => Ok(WalkControl::Continue),
3806            })?;
3807        }
3808
3809        Ok(())
3810    }
3811
3812    fn column_graph(&self) -> Result<&GeneratedColGraph> {
3813        if let Some(graph) = self.column_dependencies.0.get() {
3814            return Ok(graph);
3815        }
3816        let graph = GeneratedColGraph::build(&self.columns)?;
3817        // we ignore a concurrent initialization, because OnceLock::get_or_try_init is still nightly-only
3818        let _ = self.column_dependencies.0.set(graph);
3819        Ok(self
3820            .column_dependencies
3821            .0
3822            .get()
3823            .expect("column_dependencies was just initialized"))
3824    }
3825
3826    /// Returns an iterator over columns in topological (dependency) order. Processing
3827    /// columns in this order guarantees that all dependencies of generated columns are computed
3828    /// before the columns that reference them.
3829    pub(crate) fn columns_topo_sort(&self) -> Result<ColumnsTopologicalSort<'_>> {
3830        let topo = self.column_graph()?.topological_sort.try_to_vec()?;
3831        Ok(ColumnsTopologicalSort {
3832            columns: &self.columns,
3833            topological_sort: topo,
3834        })
3835    }
3836
3837    #[cfg(test)]
3838    pub(crate) fn peek_column_dependencies(&self) -> Option<&GeneratedColGraph> {
3839        self.column_dependencies.0.get()
3840    }
3841
3842    pub(crate) fn columns_affected_by_update(
3843        &self,
3844        updated_cols: impl IntoIterator<Item = usize>,
3845    ) -> Result<ColumnMask> {
3846        let graph = self.column_graph()?;
3847        let mut affected = ColumnMask::default();
3848        for i in updated_cols {
3849            affected.set(i)?;
3850            if i < graph.dependents.len() {
3851                let snapshot = graph.dependents[i].try_clone()?;
3852                affected.union_with(&snapshot)?;
3853            }
3854        }
3855        Ok(affected)
3856    }
3857
3858    pub(crate) fn dependencies_of_columns(
3859        &self,
3860        targets: impl IntoIterator<Item = usize>,
3861    ) -> Result<ColumnMask> {
3862        let graph = self.column_graph()?;
3863        let mut deps = ColumnMask::default();
3864        for j in targets {
3865            if !self.columns[j].is_virtual_generated() {
3866                deps.set(j)?;
3867                continue;
3868            }
3869            for i in graph.dependencies[j].iter() {
3870                if !self.columns[i].is_virtual_generated() {
3871                    deps.set(i)?;
3872                }
3873            }
3874        }
3875        Ok(deps)
3876    }
3877}
3878
3879/// Topologically sorted generated columns, yielding `(column_index, &Column)`.
3880pub(crate) struct ColumnsTopologicalSort<'a> {
3881    columns: &'a [Column],
3882    /// indices of `columns`
3883    topological_sort: Vec<usize>,
3884}
3885
3886impl<'a> ColumnsTopologicalSort<'a> {
3887    pub fn iter(&self) -> impl Iterator<Item = (usize, &'a Column)> + '_ {
3888        self.topological_sort
3889            .iter()
3890            .map(|&idx| (idx, &self.columns[idx]))
3891    }
3892}
3893
3894#[derive(Debug, Default, Clone, Copy)]
3895pub struct PseudoCursorType {
3896    pub column_count: usize,
3897}
3898
3899impl PseudoCursorType {
3900    pub fn new() -> Self {
3901        Self { column_count: 0 }
3902    }
3903
3904    pub fn new_with_columns(columns: impl AsRef<[Column]>) -> Self {
3905        Self {
3906            column_count: columns.as_ref().len(),
3907        }
3908    }
3909}
3910
3911/// A derived table from a FROM clause subquery.
3912#[derive(Debug, Clone)]
3913pub struct FromClauseSubquery {
3914    /// The name of the derived table; uses the alias if available.
3915    pub name: String,
3916    /// The query plan for the derived table. Can be either a simple SelectPlan
3917    /// or a compound select (UNION/INTERSECT/EXCEPT).
3918    pub plan: Box<Plan>,
3919    /// The columns of the derived table.
3920    pub columns: Vec<Column>,
3921    /// The start register for the result columns of the derived table;
3922    /// must be set before data is read from it.
3923    pub result_columns_start_reg: Option<usize>,
3924    /// The table cursor backing a materialized EphemeralTable representation of
3925    /// this subquery, if one was emitted.
3926    pub materialized_cursor_id: Option<CursorID>,
3927    /// CTE-specific materialization metadata, when this FROM-subquery is a CTE
3928    /// reference rather than an inline derived table.
3929    pub cte: Option<FromClauseSubqueryCteMetadata>,
3930}
3931
3932#[derive(Debug, Clone, Copy)]
3933pub struct FromClauseSubqueryCteMetadata {
3934    /// Identity shared by all references to the same CTE definition.
3935    pub id: usize,
3936    /// True when more than one read in the same query tree can reuse one
3937    /// materialized result for this CTE.
3938    pub shared_materialization: bool,
3939    /// True for explicit WITH ... AS MATERIALIZED.
3940    pub materialize_hint: bool,
3941}
3942
3943impl FromClauseSubquery {
3944    pub fn cte_id(&self) -> Option<usize> {
3945        self.cte.map(|cte| cte.id)
3946    }
3947
3948    pub fn materialize_hint(&self) -> bool {
3949        self.cte.is_some_and(|cte| cte.materialize_hint)
3950    }
3951
3952    pub fn shared_materialization(&self) -> bool {
3953        self.cte.is_some_and(|cte| cte.shared_materialization)
3954    }
3955
3956    pub fn set_shared_materialization(&mut self, shared: bool) {
3957        if let Some(cte) = &mut self.cte {
3958            cte.shared_materialization = shared;
3959        }
3960    }
3961
3962    /// Shared CTE references and explicit MATERIALIZED hints both force a
3963    /// table-backed materialization that can be scanned or probed later.
3964    pub fn requires_table_materialization(&self) -> bool {
3965        self.shared_materialization() || self.materialize_hint()
3966    }
3967
3968    /// Only simple single-reference SELECT subqueries can safely use their
3969    /// synthesized seek index as the storage target directly. Compound
3970    /// subqueries still need table-backed storage so their set-operation
3971    /// semantics are preserved before any later SEARCH shape is chosen.
3972    pub fn supports_direct_index_materialization(&self) -> bool {
3973        matches!(self.plan.as_ref(), Plan::Select(_)) && !self.requires_table_materialization()
3974    }
3975}
3976
3977fn collect_column_refs(expr: &Expr) -> HashSet<String> {
3978    collect_column_dependencies_of_expr(expr, &[])
3979}
3980
3981/// Extract all column name references from an expression as a set.
3982/// `columns` is used to resolve pre-resolved `Expr::Column { SELF_TABLE }` back to names.
3983//TODO all this usage of [normalize_ident] should be replaced with a proper [Identifier] domain type.
3984pub fn collect_column_dependencies_of_expr(expr: &Expr, columns: &[Column]) -> HashSet<String> {
3985    let mut refs = HashSet::default();
3986
3987    let _ = walk_expr(expr, &mut |e| match e {
3988        Expr::Id(name) | Expr::Name(name) => {
3989            refs.insert(normalize_ident(name.as_str()));
3990            Ok(WalkControl::Continue)
3991        }
3992        Expr::Qualified(_, col) | Expr::DoublyQualified(_, _, col) => {
3993            refs.insert(normalize_ident(col.as_str()));
3994            Ok(WalkControl::Continue)
3995        }
3996        Expr::Column { table, column, .. } if table.is_self_table() => {
3997            if let Some(col) = columns.get(*column) {
3998                if let Some(name) = &col.name {
3999                    refs.insert(normalize_ident(name));
4000                }
4001            }
4002            Ok(WalkControl::Continue)
4003        }
4004        Expr::Subquery(_)
4005        | Expr::Exists(_)
4006        | Expr::InTable { .. }
4007        | Expr::SubqueryResult { .. } => Ok(WalkControl::SkipChildren),
4008        _ => Ok(WalkControl::Continue),
4009    });
4010
4011    refs
4012}
4013
4014fn collect_column_dependencies_of_gencol(expr: &Expr, columns: &[Column], out: &mut BitSet) {
4015    let _ = walk_expr(expr, &mut |e| {
4016        match e {
4017            Expr::Column { table, column, .. } if table.is_self_table() => {
4018                out.set(*column)?;
4019            }
4020            Expr::Id(name) | Expr::Name(name) => {
4021                if let Some(idx) = find_column_index_by_name(columns, name.as_str()) {
4022                    out.set(idx)?;
4023                }
4024            }
4025            Expr::Qualified(_, col) | Expr::DoublyQualified(_, _, col) => {
4026                if let Some(idx) = find_column_index_by_name(columns, col.as_str()) {
4027                    out.set(idx)?;
4028                }
4029            }
4030            Expr::Subquery(_)
4031            | Expr::Exists(_)
4032            | Expr::InTable { .. }
4033            | Expr::SubqueryResult { .. } => {
4034                unreachable!("generated columns cannot contain subqueries")
4035            }
4036            _ => {}
4037        }
4038        Ok(WalkControl::Continue)
4039    });
4040}
4041
4042fn find_column_index_by_name(columns: &[Column], col_name: &str) -> Option<usize> {
4043    columns.iter().enumerate().find_map(|(i, col)| {
4044        col.name
4045            .as_ref()
4046            .filter(|name| name.eq_ignore_ascii_case(col_name))
4047            .map(|_| i)
4048    })
4049}
4050
4051/// Resolve [Expr::Id] / [Expr::Qualified] / [Expr::DoublyQualified] in a generated column
4052/// or partial-index expression to `Expr::Column { table: SELF_TABLE, column: idx }`.
4053pub fn resolve_gencol_expr_columns(gencol_expr: &mut Expr, columns: &[Column]) -> Result<()> {
4054    walk_expr_mut(gencol_expr, &mut |e| match e {
4055        Expr::Id(name) | Expr::Qualified(_, name) | Expr::DoublyQualified(_, _, name) => {
4056            let col_name = normalize_ident(name.as_str());
4057            let (idx, col) = columns
4058                .iter()
4059                .enumerate()
4060                .find(|(_, c)| {
4061                    c.name
4062                        .as_ref()
4063                        .is_some_and(|n| n.eq_ignore_ascii_case(&col_name))
4064                })
4065                .ok_or_else(|| LimboError::ParseError(format!("no such column: {col_name}")))?;
4066            *e = Expr::Column {
4067                database: None,
4068                table: TableInternalId::SELF_TABLE,
4069                column: idx,
4070                is_rowid_alias: col.is_rowid_alias(),
4071            };
4072            Ok(WalkControl::Continue)
4073        }
4074        _ => Ok(WalkControl::Continue),
4075    })?;
4076    Ok(())
4077}
4078
4079/// Re-render the SQL text of a generated-column expression using current column names. The input
4080/// AST may have been previously resolved into `Expr::Column { table: SELF_TABLE, column: idx, .. }`
4081/// nodes; we replace each such self-table reference with a fresh `Expr::Id(<col-name>)` before
4082/// stringifying so the result round-trips through the parser, even if a referenced column was
4083/// renamed since the original `original_sql` was captured.
4084pub fn render_gencol_expr_sql_with_new_names(expr: &Expr, columns: &[Column]) -> Result<String> {
4085    let mut clone = expr.clone();
4086    walk_expr_mut(&mut clone, &mut |e| -> Result<WalkControl> {
4087        if let Expr::Column { table, column, .. } = e {
4088            if table.is_self_table() {
4089                if let Some(col) = columns.get(*column) {
4090                    if let Some(name) = col.name.as_ref() {
4091                        *e = Expr::Id(Name::exact(name.clone()));
4092                    }
4093                }
4094            }
4095        }
4096        Ok(WalkControl::Continue)
4097    })?;
4098    Ok(clone.to_string())
4099}
4100
4101pub(crate) fn is_deterministic_schema_function_call(func: &Func, args: &[Box<Expr>]) -> bool {
4102    match func {
4103        Func::Scalar(
4104            ScalarFunc::Date
4105            | ScalarFunc::Time
4106            | ScalarFunc::DateTime
4107            | ScalarFunc::UnixEpoch
4108            | ScalarFunc::JulianDay
4109            | ScalarFunc::StrfTime
4110            | ScalarFunc::TimeDiff,
4111        ) => is_deterministic_datetime_call(func, args),
4112        _ => func.is_deterministic(),
4113    }
4114}
4115
4116// SQLite allows date/time functions in schema-persistent expressions only when
4117// they do not depend on the current clock or local timezone. This applies to
4118// expression indexes, partial index WHERE clauses, and generated columns.
4119fn is_deterministic_datetime_call(func: &Func, args: &[Box<Expr>]) -> bool {
4120    match func {
4121        Func::Scalar(ScalarFunc::Date)
4122        | Func::Scalar(ScalarFunc::Time)
4123        | Func::Scalar(ScalarFunc::DateTime)
4124        | Func::Scalar(ScalarFunc::UnixEpoch)
4125        | Func::Scalar(ScalarFunc::JulianDay) => {
4126            !args.is_empty()
4127                && !is_current_time_expr(args[0].as_ref())
4128                && !args[1..]
4129                    .iter()
4130                    .any(|arg| is_unsafe_datetime_modifier(arg.as_ref()))
4131        }
4132        Func::Scalar(ScalarFunc::StrfTime) => {
4133            args.len() >= 2
4134                && !is_current_time_expr(args[1].as_ref())
4135                && !args[2..]
4136                    .iter()
4137                    .any(|arg| is_unsafe_datetime_modifier(arg.as_ref()))
4138        }
4139        Func::Scalar(ScalarFunc::TimeDiff) => {
4140            !args.iter().any(|arg| is_current_time_expr(arg.as_ref()))
4141        }
4142        _ => unreachable!("non-datetime function passed to datetime index validator"),
4143    }
4144}
4145
4146fn is_current_time_expr(expr: &Expr) -> bool {
4147    matches!(
4148        expr,
4149        Expr::Literal(ast::Literal::String(value)) if string_literal_eq(value, "now")
4150    ) || matches!(
4151        expr,
4152        Expr::Literal(
4153            ast::Literal::CurrentDate | ast::Literal::CurrentTime | ast::Literal::CurrentTimestamp
4154        )
4155    )
4156}
4157
4158fn is_unsafe_datetime_modifier(expr: &Expr) -> bool {
4159    matches!(
4160        expr,
4161        Expr::Literal(ast::Literal::String(value))
4162            if string_literal_eq(value, "localtime") || string_literal_eq(value, "utc")
4163    ) || is_current_time_expr(expr)
4164}
4165
4166fn string_literal_eq(value: &str, expected: &str) -> bool {
4167    value.trim_matches('\'').eq_ignore_ascii_case(expected)
4168}
4169
4170pub(crate) fn validate_generated_expr(expr: &Expr) -> Result<()> {
4171    use ast::Expr;
4172    match expr {
4173        Expr::Qualified(_, _) => {
4174            bail_parse_error!("the \".\" operator prohibited in generated columns");
4175        }
4176        Expr::DoublyQualified(_, _, _) => {
4177            bail_parse_error!("the \".\" operator prohibited in generated columns");
4178        }
4179
4180        Expr::Variable(_) => {
4181            bail_parse_error!("bind parameters prohibited in generated columns");
4182        }
4183
4184        Expr::Subquery(_) | Expr::InSelect { .. } | Expr::Exists(_) | Expr::InTable { .. } => {
4185            bail_parse_error!("subqueries prohibited in generated columns");
4186        }
4187
4188        Expr::FunctionCall {
4189            name,
4190            args,
4191            filter_over,
4192            ..
4193        } => {
4194            if filter_over.over_clause.is_some() {
4195                bail_parse_error!("window functions prohibited in generated columns");
4196            }
4197            let arg_count = args.len();
4198            let Some(func) = Func::resolve_function(name.as_str(), arg_count)? else {
4199                return Err(LimboError::ParseError(format!(
4200                    "could not resolve function {}",
4201                    name.as_str()
4202                )));
4203            };
4204            if matches!(func, Func::Agg(_)) {
4205                bail_parse_error!("aggregate functions prohibited in generated columns");
4206            }
4207            if !is_deterministic_schema_function_call(&func, args) {
4208                bail_parse_error!("non-deterministic functions prohibited in generated columns");
4209            }
4210            for arg in args {
4211                validate_generated_expr(arg)?;
4212            }
4213        }
4214
4215        Expr::FunctionCallStar { name, filter_over } => {
4216            if filter_over.over_clause.is_some() {
4217                bail_parse_error!("window functions prohibited in generated columns");
4218            }
4219            let Some(func) = Func::resolve_function(name.as_str(), 0)? else {
4220                return Err(LimboError::ParseError(format!(
4221                    "could not resolve function {}",
4222                    name.as_str()
4223                )));
4224            };
4225
4226            if matches!(func, Func::Agg(_)) {
4227                bail_parse_error!("aggregate functions prohibited in generated columns");
4228            }
4229            if !func.is_deterministic() {
4230                bail_parse_error!("non-deterministic functions prohibited in generated columns");
4231            }
4232        }
4233
4234        Expr::Binary(lhs, _, rhs) => {
4235            validate_generated_expr(lhs)?;
4236            validate_generated_expr(rhs)?;
4237        }
4238        Expr::Unary(_, inner) => {
4239            validate_generated_expr(inner)?;
4240        }
4241        Expr::Parenthesized(exprs) => {
4242            for e in exprs {
4243                validate_generated_expr(e)?;
4244            }
4245        }
4246        Expr::Case {
4247            base,
4248            when_then_pairs,
4249            else_expr,
4250            ..
4251        } => {
4252            if let Some(b) = base {
4253                validate_generated_expr(b)?;
4254            }
4255            for (w, t) in when_then_pairs {
4256                validate_generated_expr(w)?;
4257                validate_generated_expr(t)?;
4258            }
4259            if let Some(e) = else_expr {
4260                validate_generated_expr(e)?;
4261            }
4262        }
4263        Expr::Cast { expr, .. } => {
4264            validate_generated_expr(expr)?;
4265        }
4266        Expr::InList { lhs, rhs, .. } => {
4267            validate_generated_expr(lhs)?;
4268            for e in rhs {
4269                validate_generated_expr(e)?;
4270            }
4271        }
4272        Expr::Between {
4273            lhs, start, end, ..
4274        } => {
4275            validate_generated_expr(lhs)?;
4276            validate_generated_expr(start)?;
4277            validate_generated_expr(end)?;
4278        }
4279        Expr::Like {
4280            lhs, rhs, escape, ..
4281        } => {
4282            validate_generated_expr(lhs)?;
4283            validate_generated_expr(rhs)?;
4284            if let Some(e) = escape {
4285                validate_generated_expr(e)?;
4286            }
4287        }
4288        Expr::Collate(inner, _) => {
4289            validate_generated_expr(inner)?;
4290        }
4291        Expr::IsNull(inner) | Expr::NotNull(inner) => {
4292            validate_generated_expr(inner)?;
4293        }
4294        // CURRENT_TIME/DATE/TIMESTAMP parse as literals but evaluate to a
4295        // different value on every read; SQLite rejects them like any other
4296        // non-deterministic function (an index on such a column goes stale
4297        // as soon as the value is recomputed).
4298        Expr::Literal(
4299            ast::Literal::CurrentDate | ast::Literal::CurrentTime | ast::Literal::CurrentTimestamp,
4300        ) => {
4301            bail_parse_error!("non-deterministic functions prohibited in generated columns");
4302        }
4303        _ => {}
4304    }
4305    Ok(())
4306}
4307
4308/// Peel an optional `COLLATE` wrapper off a PRIMARY KEY / UNIQUE table
4309/// constraint column, e.g. `PRIMARY KEY(a COLLATE NOCASE)`, returning the
4310/// inner expression and the resolved collation.
4311fn constraint_column_collation(expr: &Expr) -> Result<(&Expr, Option<CollationSeq>)> {
4312    match expr {
4313        Expr::Collate(inner, collation_name) => {
4314            let collation_seq = CollationSeq::new(collation_name.as_str())?;
4315            if collation_seq.is_custom() {
4316                crate::bail_parse_error!(
4317                    "custom collations are not supported in schema definitions"
4318                );
4319            }
4320            Ok((inner.as_ref(), Some(collation_seq)))
4321        }
4322        _ => Ok((expr, None)),
4323    }
4324}
4325
4326pub fn create_table(tbl_name: &str, body: &CreateTableBody, root_page: i64) -> Result<BTreeTable> {
4327    let table_name = normalize_ident(tbl_name);
4328    trace!("Creating table {}", table_name);
4329    let has_rowid;
4330    let mut has_autoincrement = false;
4331    let mut primary_key_columns = vec![];
4332    let mut foreign_keys = vec![];
4333    let mut check_constraints = vec![];
4334    let mut cols: Vec<Column> = vec![];
4335    let is_strict: bool;
4336    let mut unique_sets_columns: Vec<UniqueSet> = vec![];
4337    let mut unique_sets_constraints: Vec<UniqueSet> = vec![];
4338    match body {
4339        CreateTableBody::ColumnsAndConstraints {
4340            columns,
4341            constraints,
4342            options,
4343        } => {
4344            has_rowid = !options.contains_without_rowid();
4345            is_strict = options.contains_strict();
4346            let column_fk_count = columns
4347                .iter()
4348                .flat_map(|col| col.constraints.iter())
4349                .filter(|constraint| {
4350                    matches!(
4351                        &constraint.constraint,
4352                        ast::ColumnConstraint::ForeignKey { .. }
4353                    )
4354                })
4355                .count();
4356
4357            // we need to preserve order of unique sets definition
4358            // but also, we analyze constraints first in order to check PRIMARY KEY constraint and recognize rowid alias properly
4359            // that's why we maintain 2 unique_set sequences and merge them together in the end
4360
4361            let mut table_fk_order = column_fk_count;
4362            for c in constraints {
4363                if let ast::TableConstraint::PrimaryKey {
4364                    columns,
4365                    auto_increment,
4366                    conflict_clause,
4367                } = &c.constraint
4368                {
4369                    if !primary_key_columns.is_empty() {
4370                        crate::bail_parse_error!(
4371                            "table \"{}\" has more than one primary key",
4372                            tbl_name
4373                        );
4374                    }
4375                    if *auto_increment {
4376                        has_autoincrement = true;
4377                    }
4378
4379                    let mut pk_collations = Vec::try_with_capacity_ext(columns.len())?;
4380                    for column in columns {
4381                        let (expr, collation) = constraint_column_collation(column.expr.as_ref())?;
4382                        let col_name = match expr {
4383                            Expr::Id(id) => normalize_ident(id.as_str()),
4384                            Expr::Literal(Literal::String(value)) => {
4385                                value.trim_matches('\'').to_owned()
4386                            }
4387                            expr => {
4388                                bail_parse_error!("unsupported primary key expression: {}", expr)
4389                            }
4390                        };
4391                        primary_key_columns
4392                            .try_push((col_name, column.order.unwrap_or(SortOrder::Asc)))?;
4393                        pk_collations.try_push(collation)?;
4394                    }
4395                    unique_sets_constraints.try_push(UniqueSet {
4396                        columns: primary_key_columns.try_clone()?,
4397                        collations: pk_collations,
4398                        is_primary_key: true,
4399                        conflict_clause: *conflict_clause,
4400                    })?;
4401                } else if let ast::TableConstraint::Unique {
4402                    columns,
4403                    conflict_clause,
4404                } = &c.constraint
4405                {
4406                    let mut unique_columns = Vec::try_with_capacity_ext(columns.len())?;
4407                    let mut unique_collations = Vec::try_with_capacity_ext(columns.len())?;
4408                    for column in columns {
4409                        let (expr, collation) = constraint_column_collation(column.expr.as_ref())?;
4410                        match expr {
4411                            Expr::Id(id) => unique_columns.try_push((
4412                                id.as_str().to_string(),
4413                                column.order.unwrap_or(SortOrder::Asc),
4414                            ))?,
4415                            Expr::Literal(Literal::String(value)) => unique_columns.try_push((
4416                                value.trim_matches('\'').to_owned(),
4417                                column.order.unwrap_or(SortOrder::Asc),
4418                            ))?,
4419                            expr => {
4420                                bail_parse_error!("unsupported unique key expression: {}", expr)
4421                            }
4422                        }
4423                        unique_collations.try_push(collation)?;
4424                    }
4425                    let unique_set = UniqueSet {
4426                        columns: unique_columns,
4427                        collations: unique_collations,
4428                        is_primary_key: false,
4429                        conflict_clause: *conflict_clause,
4430                    };
4431                    unique_sets_constraints.try_push(unique_set)?;
4432                } else if let ast::TableConstraint::ForeignKey {
4433                    columns,
4434                    clause,
4435                    defer_clause,
4436                } = &c.constraint
4437                {
4438                    let child_columns: Box<[String]> = columns
4439                        .iter()
4440                        .map(|ic| normalize_ident(ic.col_name.as_str()))
4441                        .try_collect()?;
4442                    // derive parent columns: explicit or default to parent PK
4443                    let parent_table = normalize_ident(clause.tbl_name.as_str());
4444                    let parent_columns: Box<[String]> = clause
4445                        .columns
4446                        .iter()
4447                        .map(|ic| normalize_ident(ic.col_name.as_str()))
4448                        .try_collect()?;
4449
4450                    // Only check arity if parent columns were explicitly listed
4451                    if !parent_columns.is_empty() && child_columns.len() != parent_columns.len() {
4452                        crate::bail_parse_error!(
4453                            "foreign key on \"{}\" has {} child column(s) but {} parent column(s)",
4454                            tbl_name,
4455                            child_columns.len(),
4456                            parent_columns.len()
4457                        );
4458                    }
4459                    // deferrable semantics
4460                    let deferred = match defer_clause {
4461                        Some(d) => {
4462                            d.deferrable
4463                                && matches!(
4464                                    d.init_deferred,
4465                                    Some(InitDeferredPred::InitiallyDeferred)
4466                                )
4467                        }
4468                        None => false, // NOT DEFERRABLE INITIALLY IMMEDIATE by default
4469                    };
4470                    let fk = ForeignKey {
4471                        parent_table,
4472                        parent_columns,
4473                        child_columns,
4474                        on_delete: clause
4475                            .args
4476                            .iter()
4477                            .find_map(|a| {
4478                                if let ast::RefArg::OnDelete(x) = a {
4479                                    Some(*x)
4480                                } else {
4481                                    None
4482                                }
4483                            })
4484                            .unwrap_or(RefAct::NoAction),
4485                        on_update: clause
4486                            .args
4487                            .iter()
4488                            .find_map(|a| {
4489                                if let ast::RefArg::OnUpdate(x) = a {
4490                                    Some(*x)
4491                                } else {
4492                                    None
4493                                }
4494                            })
4495                            .unwrap_or(RefAct::NoAction),
4496                        deferred,
4497                        decl_order: table_fk_order,
4498                    };
4499                    foreign_keys.try_push(Arc::new(fk))?;
4500                    table_fk_order += 1;
4501                } else if let ast::TableConstraint::Check(expr) = &c.constraint {
4502                    check_constraints.try_push(CheckConstraint::new(
4503                        c.name.as_ref(),
4504                        expr,
4505                        None,
4506                    ))?;
4507                }
4508            }
4509
4510            // Due to a bug in SQLite, this check is needed to maintain backwards compatibility with rowid alias
4511            // SQLite docs: https://sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key
4512            // Issue: https://github.com/tursodatabase/turso/issues/3665
4513            let mut primary_key_desc_columns_constraint = false;
4514
4515            let mut column_fk_order = 0;
4516            for ast::ColumnDefinition {
4517                col_name,
4518                col_type,
4519                constraints,
4520            } in columns
4521            {
4522                let name = col_name.as_str().to_string();
4523                // Regular sqlite tables have an integer rowid that uniquely identifies a row.
4524                // Even if you create a table with a column e.g. 'id INT PRIMARY KEY', there will still
4525                // be a separate hidden rowid, and the 'id' column will have a separate index built for it.
4526                //
4527                // However:
4528                // A column defined as exactly INTEGER PRIMARY KEY is a rowid alias, meaning that the rowid
4529                // and the value of this column are the same.
4530                // https://www.sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key
4531                let ty_str = col_type
4532                    .as_ref()
4533                    .cloned()
4534                    .map(|ast::Type { name, .. }| name)
4535                    .unwrap_or_default();
4536
4537                let ty_params: std::vec::Vec<Box<Expr>> = match col_type {
4538                    Some(ast::Type {
4539                        size: Some(ast::TypeSize::MaxSize(ref expr)),
4540                        ..
4541                    }) => std::vec![expr.clone()],
4542                    Some(ast::Type {
4543                        size: Some(ast::TypeSize::TypeSize(ref e1, ref e2)),
4544                        ..
4545                    }) => std::vec![e1.clone(), e2.clone()],
4546                    _ => std::vec::Vec::new(),
4547                };
4548
4549                let mut typename_exactly_integer = false;
4550                let ty = match col_type {
4551                    Some(data_type) => {
4552                        let (ty, ei) = type_from_name(&data_type.name);
4553                        typename_exactly_integer = ei;
4554                        ty
4555                    }
4556                    None => Type::Null,
4557                };
4558
4559                let mut default = None;
4560                let mut generated: Option<Box<Expr>> = None;
4561                let mut primary_key = false;
4562                let mut notnull = false;
4563                let mut explicit_notnull = false;
4564                let mut notnull_conflict_clause = None;
4565                let mut order = SortOrder::Asc;
4566                let mut unique = false;
4567                let mut collation = None;
4568                for c_def in constraints {
4569                    match &c_def.constraint {
4570                        ast::ColumnConstraint::Check(expr) => {
4571                            check_constraints.try_push(CheckConstraint::new(
4572                                c_def.name.as_ref(),
4573                                expr,
4574                                Some(&name),
4575                            ))?;
4576                        }
4577                        ast::ColumnConstraint::Generated { expr, typ } => {
4578                            if typ
4579                                .as_ref()
4580                                .is_some_and(|t| matches!(t, ast::GeneratedColumnType::Stored))
4581                            {
4582                                bail_parse_error!("Stored generated columns are not supported");
4583                            }
4584                            validate_generated_expr(expr)?;
4585                            generated = Some(expr.clone());
4586                        }
4587                        ast::ColumnConstraint::PrimaryKey {
4588                            order: o,
4589                            auto_increment,
4590                            conflict_clause,
4591                            ..
4592                        } => {
4593                            if !primary_key_columns.is_empty() {
4594                                crate::bail_parse_error!(
4595                                    "table \"{}\" has more than one primary key",
4596                                    tbl_name
4597                                );
4598                            }
4599                            primary_key = true;
4600                            if *auto_increment {
4601                                has_autoincrement = true;
4602                            }
4603                            if let Some(o) = o {
4604                                order = *o;
4605                            }
4606                            unique_sets_columns.try_push(UniqueSet {
4607                                columns: try_vec![(name.clone(), order)]?,
4608                                collations: try_vec![None]?,
4609                                is_primary_key: true,
4610                                conflict_clause: *conflict_clause,
4611                            })?;
4612                        }
4613                        ast::ColumnConstraint::NotNull {
4614                            nullable,
4615                            conflict_clause,
4616                            ..
4617                        } => {
4618                            notnull = !nullable;
4619                            explicit_notnull = !nullable;
4620                            notnull_conflict_clause = *conflict_clause;
4621                        }
4622                        ast::ColumnConstraint::Default(ref expr) => {
4623                            default = Some(
4624                                translate_ident_to_string_literal(expr)
4625                                    .unwrap_or_else(|| expr.clone()),
4626                            );
4627                        }
4628                        ast::ColumnConstraint::Unique(conflict) => {
4629                            unique = true;
4630                            unique_sets_columns.try_push(UniqueSet {
4631                                columns: try_vec![(name.clone(), order)]?,
4632                                collations: try_vec![None]?,
4633                                is_primary_key: false,
4634                                conflict_clause: *conflict,
4635                            })?;
4636                        }
4637                        ast::ColumnConstraint::Collate { ref collation_name } => {
4638                            let collation_seq = CollationSeq::new(collation_name.as_str())?;
4639                            if collation_seq.is_custom() {
4640                                crate::bail_parse_error!(
4641                                    "custom collations are not supported in schema definitions"
4642                                );
4643                            }
4644                            collation = Some(collation_seq);
4645                        }
4646                        ast::ColumnConstraint::ForeignKey {
4647                            clause,
4648                            defer_clause,
4649                        } => {
4650                            if clause.columns.len() > 1 {
4651                                crate::bail_parse_error!(
4652                                    "foreign key on {} should reference only one column of table {}",
4653                                    name,
4654                                    clause.tbl_name.as_str()
4655                                );
4656                            }
4657                            let fk = ForeignKey {
4658                                parent_table: normalize_ident(clause.tbl_name.as_str()),
4659                                parent_columns: clause
4660                                    .columns
4661                                    .iter()
4662                                    .map(|c| normalize_ident(c.col_name.as_str()))
4663                                    .try_collect()?,
4664                                on_delete: clause
4665                                    .args
4666                                    .iter()
4667                                    .find_map(|arg| {
4668                                        if let ast::RefArg::OnDelete(act) = arg {
4669                                            Some(*act)
4670                                        } else {
4671                                            None
4672                                        }
4673                                    })
4674                                    .unwrap_or(RefAct::NoAction),
4675                                on_update: clause
4676                                    .args
4677                                    .iter()
4678                                    .find_map(|arg| {
4679                                        if let ast::RefArg::OnUpdate(act) = arg {
4680                                            Some(*act)
4681                                        } else {
4682                                            None
4683                                        }
4684                                    })
4685                                    .unwrap_or(RefAct::NoAction),
4686                                child_columns: Box::from([name.clone()]),
4687                                deferred: match defer_clause {
4688                                    Some(d) => {
4689                                        d.deferrable
4690                                            && matches!(
4691                                                d.init_deferred,
4692                                                Some(InitDeferredPred::InitiallyDeferred)
4693                                            )
4694                                    }
4695                                    None => false,
4696                                },
4697                                decl_order: column_fk_order,
4698                            };
4699                            foreign_keys.try_push(Arc::new(fk))?;
4700                            column_fk_order += 1;
4701                        }
4702                    }
4703                }
4704
4705                if let Some(ref gen_expr) = generated {
4706                    if primary_key {
4707                        bail_parse_error!(
4708                            "generated column \"{}\" cannot be part of the PRIMARY KEY",
4709                            name
4710                        );
4711                    }
4712                    if default.is_some() {
4713                        bail_parse_error!(
4714                            "generated column \"{}\" cannot have a DEFAULT value",
4715                            name
4716                        );
4717                    }
4718
4719                    let referenced_cols = collect_column_refs(gen_expr);
4720                    let current_col_name = normalize_ident(&name);
4721
4722                    if referenced_cols.iter().any(|c| c == &current_col_name) {
4723                        bail_parse_error!("generated column \"{}\" cannot reference itself", name);
4724                    }
4725                }
4726
4727                if primary_key {
4728                    primary_key_columns.try_push((name.clone(), order))?;
4729                    if order == SortOrder::Desc {
4730                        primary_key_desc_columns_constraint = true;
4731                    }
4732                } else if primary_key_columns
4733                    .iter()
4734                    .any(|(col_name, _)| col_name.eq_ignore_ascii_case(&name))
4735                {
4736                    if generated.is_some() {
4737                        crate::bail_parse_error!(
4738                            "generated column \"{}\" cannot be part of the PRIMARY KEY",
4739                            name
4740                        );
4741                    }
4742                    primary_key = true;
4743                }
4744
4745                let mut col = Column::new(
4746                    Some(name),
4747                    ty_str,
4748                    default,
4749                    generated,
4750                    ty,
4751                    collation,
4752                    ColDef {
4753                        primary_key,
4754                        rowid_alias: typename_exactly_integer
4755                            && primary_key
4756                            && !primary_key_desc_columns_constraint,
4757                        notnull,
4758                        explicit_notnull,
4759                        unique,
4760                        hidden: false,
4761                        notnull_conflict_clause,
4762                    },
4763                );
4764                col.ty_params = ty_params;
4765                if let Some(t) = col_type.as_ref() {
4766                    if t.is_array() {
4767                        col.set_array_dimensions(t.array_dimensions);
4768                    }
4769                }
4770                cols.try_push(col)?;
4771            }
4772        }
4773        CreateTableBody::AsSelect(_) => {
4774            crate::bail_parse_error!("CREATE TABLE AS SELECT is not supported")
4775        }
4776    };
4777
4778    // flip is_rowid_alias back to false if the table has multiple primary key columns
4779    // or if the table has no rowid
4780    if !has_rowid || primary_key_columns.len() > 1 {
4781        for col in cols.iter_mut() {
4782            col.set_rowid_alias(false);
4783        }
4784    }
4785
4786    if has_autoincrement {
4787        // only allow integers
4788        if primary_key_columns.len() != 1 {
4789            crate::bail_parse_error!("AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY");
4790        }
4791
4792        let pk_col_name = &primary_key_columns[0].0;
4793        let pk_col = cols.iter().find(|c| {
4794            c.name
4795                .as_deref()
4796                .is_some_and(|n| n.eq_ignore_ascii_case(pk_col_name))
4797        });
4798
4799        if let Some(col) = pk_col {
4800            if col.ty() != Type::Integer {
4801                crate::bail_parse_error!("AUTOINCREMENT is only allowed on an INTEGER PRIMARY KEY");
4802            }
4803        }
4804    }
4805
4806    // concat unqiue_sets collected from column definitions and constraints in correct order
4807    let mut unique_sets = unique_sets_columns
4808        .into_iter()
4809        .chain(unique_sets_constraints)
4810        .try_collect::<Vec<_>>()?;
4811    // Capture PK conflict clause before the rowid-alias UniqueSet is removed.
4812    let rowid_alias_conflict_clause = unique_sets
4813        .iter()
4814        .find(|us| us.is_primary_key)
4815        .and_then(|us| us.conflict_clause);
4816    for col in cols.iter() {
4817        if col.is_rowid_alias() {
4818            // Unique sets are used for creating automatic indexes. An index is not created for a rowid alias PRIMARY KEY.
4819            // However, an index IS created for a rowid alias UNIQUE, e.g. CREATE TABLE t(x INTEGER PRIMARY KEY, UNIQUE(x))
4820            let unique_set_w_only_rowid_alias = unique_sets.iter().position(|us| {
4821                us.is_primary_key
4822                    && us.columns.len() == 1
4823                    && us
4824                        .columns
4825                        .first()
4826                        .unwrap()
4827                        .0
4828                        .eq_ignore_ascii_case(col.name.as_ref().unwrap())
4829            });
4830            if let Some(u) = unique_set_w_only_rowid_alias {
4831                unique_sets.remove(u);
4832            }
4833        }
4834    }
4835
4836    let mut table = BTreeTable {
4837        root_page,
4838        name: table_name,
4839        has_rowid,
4840        primary_key_columns,
4841        has_autoincrement,
4842        columns: cols,
4843        is_strict,
4844        foreign_keys,
4845        unique_sets: {
4846            // If there are any unique sets that have identical column names in the same order (even if they are PRIMARY KEY and UNIQUE and have different sort orders), remove the duplicates.
4847            // Examples:
4848            // PRIMARY KEY (a, b) and UNIQUE (a desc, b) are the same
4849            // PRIMARY KEY (a, b) and UNIQUE (b, a) are not the same
4850            // Using a n^2 monkey algorithm here because n is small, CPUs are fast, life is short, and most importantly:
4851            // we want to preserve the order of the sets -- automatic index names in sqlite_schema must be in definition order.
4852            let mut i = 0;
4853            while i < unique_sets.len() {
4854                let mut j = i + 1;
4855                while j < unique_sets.len() {
4856                    let lengths_equal =
4857                        unique_sets[i].columns.len() == unique_sets[j].columns.len();
4858                    if lengths_equal
4859                        && unique_sets[i]
4860                            .columns
4861                            .iter()
4862                            .zip(unique_sets[j].columns.iter())
4863                            .all(|((a_name, _), (b_name, _))| a_name.eq_ignore_ascii_case(b_name))
4864                    {
4865                        // SQLite rejects duplicate constraints on the same columns when both
4866                        // specify ON CONFLICT with different resolve types.
4867                        if let (Some(a), Some(b)) = (
4868                            unique_sets[i].conflict_clause,
4869                            unique_sets[j].conflict_clause,
4870                        ) {
4871                            if a != b {
4872                                crate::bail_parse_error!(
4873                                    "conflicting ON CONFLICT clauses specified"
4874                                );
4875                            }
4876                        }
4877                        unique_sets.remove(j);
4878                    } else {
4879                        j += 1;
4880                    }
4881                }
4882                i += 1;
4883            }
4884            unique_sets
4885        },
4886        check_constraints,
4887        rowid_alias_conflict_clause,
4888        has_virtual_columns: false,
4889        logical_to_physical_map: vec![],
4890        column_dependencies: Default::default(),
4891    };
4892    table.prepare_generated_columns()?;
4893    if !table.has_rowid {
4894        if table.primary_key_columns.is_empty() {
4895            crate::bail_parse_error!("PRIMARY KEY missing on table {}", table.name);
4896        }
4897        for (pk_name, _) in &table.primary_key_columns {
4898            let Some((_, col)) = table.get_column(pk_name) else {
4899                crate::bail_parse_error!(
4900                    "PRIMARY KEY column {pk_name} not found in table {}",
4901                    table.name
4902                );
4903            };
4904            if !col.notnull() {
4905                let Some(idx) = table.get_column(pk_name).map(|(idx, _)| idx) else {
4906                    unreachable!("PRIMARY KEY column should exist");
4907                };
4908                table.columns[idx].set_notnull(true);
4909            }
4910        }
4911    }
4912    table.logical_to_physical_map = BTreeTable::build_logical_to_physical_map(
4913        &table.columns,
4914        &table.primary_key_columns,
4915        table.has_rowid,
4916    );
4917    Ok(table)
4918}
4919
4920/// SQLite treats bare identifiers in DEFAULT clauses as string literals.
4921/// E.g., `DEFAULT hello` becomes the string "hello", not a column reference.
4922pub fn translate_ident_to_string_literal(expr: &Expr) -> Option<Box<Expr>> {
4923    match expr {
4924        Expr::Name(name) | Expr::Id(name) => {
4925            Some(Box::new(Expr::Literal(Literal::String(name.as_literal()))))
4926        }
4927        _ => None,
4928    }
4929}
4930
4931pub fn _build_pseudo_table(columns: &[ResultColumn]) -> PseudoCursorType {
4932    let table = PseudoCursorType::new();
4933    for column in columns {
4934        match column {
4935            ResultColumn::Expr(expr, _as_name) => {
4936                todo!("unsupported expression {:?}", expr);
4937            }
4938            ResultColumn::Star => {
4939                todo!();
4940            }
4941            ResultColumn::TableStar(_) => {
4942                todo!();
4943            }
4944        }
4945    }
4946    table
4947}
4948
4949#[derive(Debug, Clone)]
4950pub struct ForeignKey {
4951    /// Columns in this table (child side). Never empty (validated at parse time).
4952    pub child_columns: Box<[String]>,
4953    /// Referenced (parent) table
4954    pub parent_table: String,
4955    /// Parent-side referenced columns. Empty means "use parent's PRIMARY KEY".
4956    pub parent_columns: Box<[String]>,
4957    pub on_delete: RefAct,
4958    pub on_update: RefAct,
4959    /// DEFERRABLE INITIALLY DEFERRED
4960    pub deferred: bool,
4961    /// Declaration order among this table's foreign key constraints.
4962    ///
4963    /// SQLite reports PRAGMA foreign_key_list rows in reverse declaration order.
4964    pub decl_order: usize,
4965}
4966#[inline]
4967fn fk_mismatch_err(child: &str, parent: &str) -> crate::LimboError {
4968    crate::LimboError::ForeignKeyConstraint(format!(
4969        "foreign key mismatch - \"{child}\" referencing \"{parent}\""
4970    ))
4971}
4972
4973impl ForeignKey {
4974    fn validate(&self) -> Result<()> {
4975        if self
4976            .parent_columns
4977            .iter()
4978            .any(|c| ROWID_STRS.iter().any(|&r| r.eq_ignore_ascii_case(c)))
4979        {
4980            return Err(crate::LimboError::ForeignKeyConstraint(format!(
4981                "foreign key mismatch referencing \"{}\"",
4982                self.parent_table
4983            )));
4984        }
4985        Ok(())
4986    }
4987}
4988
4989/// A single resolved foreign key where `parent_table == target`.
4990///
4991/// Child column names live in `fk.child_columns` — not duplicated here.
4992#[derive(Clone, Debug)]
4993pub struct ResolvedFkRef {
4994    /// Child table that owns the FK.
4995    pub child_table: Arc<BTreeTable>,
4996    /// The FK as declared on the child table.
4997    pub fk: Arc<ForeignKey>,
4998
4999    /// Resolved parent columns: either `fk.parent_columns` or, when that is
5000    /// empty, the parent table's PRIMARY KEY columns. Always non-empty.
5001    pub parent_cols: Box<[String]>,
5002    /// Column positions in the child/parent tables (pos_in_table)
5003    pub child_pos: BoxedSlice<usize>,
5004    pub parent_pos: BoxedSlice<usize>,
5005
5006    /// If the parent key is rowid or a rowid-alias (single-column only)
5007    pub parent_uses_rowid: bool,
5008    /// For non-rowid parents: the UNIQUE index that enforces the parent key.
5009    /// (None when `parent_uses_rowid == true`.)
5010    pub parent_unique_index: Option<Arc<Index>>,
5011}
5012
5013impl ResolvedFkRef {
5014    /// Returns if any referenced parent column can change when these column positions are updated.
5015    pub fn parent_key_may_change(
5016        &self,
5017        updated_parent_positions: &ColumnMask,
5018        parent_tbl: &BTreeTable,
5019    ) -> Result<bool> {
5020        if self.parent_uses_rowid {
5021            // parent rowid changes if the parent's rowid or alias is updated
5022            if let Some((idx, _)) = parent_tbl
5023                .columns
5024                .iter()
5025                .enumerate()
5026                .find(|(_, c)| c.is_rowid_alias())
5027            {
5028                return Ok(updated_parent_positions.get(idx));
5029            }
5030            // Without a rowid alias, a direct rowid update is represented separately with ROWID_SENTINEL
5031            return Ok(true);
5032        }
5033        let affected = parent_tbl.columns_affected_by_update(updated_parent_positions)?;
5034        Ok(self.parent_pos.iter().any(|p| affected.get(*p)))
5035    }
5036
5037    /// Returns if any child column of this FK is in `updated_child_positions`
5038    pub fn child_key_changed(
5039        &self,
5040        updated_child_positions: &ColumnMask,
5041        child_tbl: &BTreeTable,
5042    ) -> bool {
5043        if self
5044            .child_pos
5045            .iter()
5046            .any(|p| updated_child_positions.get(*p))
5047        {
5048            return true;
5049        }
5050        // special case: if FK uses a rowid alias on child, and rowid changed
5051        if self.fk.child_columns.len() == 1 {
5052            let (i, col) = child_tbl.get_column(&self.fk.child_columns[0]).unwrap();
5053            if col.is_rowid_alias() && updated_child_positions.get(i) {
5054                return true;
5055            }
5056        }
5057        false
5058    }
5059}
5060
5061#[derive(Debug, Clone)]
5062pub struct Column {
5063    pub name: Option<String>,
5064    pub ty_str: String,
5065    pub ty_params: std::vec::Vec<Box<Expr>>,
5066    pub default: Option<Box<Expr>>,
5067    generated_type: GeneratedType,
5068    raw: u32,
5069    explicit_notnull: bool,
5070    /// ON CONFLICT clause for NOT NULL constraint on this column.
5071    pub notnull_conflict_clause: Option<ResolveType>,
5072}
5073
5074#[derive(Default)]
5075pub struct ColDef {
5076    pub primary_key: bool,
5077    pub rowid_alias: bool,
5078    pub notnull: bool,
5079    pub explicit_notnull: bool,
5080    pub unique: bool,
5081    pub hidden: bool,
5082    pub notnull_conflict_clause: Option<ResolveType>,
5083}
5084
5085#[derive(Debug, Clone)]
5086pub enum GeneratedType {
5087    /// `resolved` holds the expression with column references resolved to
5088    /// `Expr::Column { table: SELF_TABLE }` for use at compile time.
5089    /// `original_sql` preserves the original SQL text for `to_sql()` round-tripping.
5090    Virtual {
5091        expr: Box<Expr>,
5092        original_sql: String,
5093    },
5094    // Stored { resolved: Box<Expr>, original_sql: String },
5095    NotGenerated,
5096}
5097
5098// flags
5099const F_PRIMARY_KEY: u32 = 1;
5100const F_ROWID_ALIAS: u32 = 2;
5101const F_NOTNULL: u32 = 4;
5102const F_UNIQUE: u32 = 8;
5103const F_HIDDEN: u32 = 16;
5104
5105// pack Type and Collation in the remaining bits
5106const TYPE_SHIFT: u32 = 5;
5107const TYPE_MASK: u32 = 0b111 << TYPE_SHIFT;
5108const COLL_SHIFT: u32 = TYPE_SHIFT + 3;
5109const COLL_MASK: u32 = 0b1111_1111_1111 << COLL_SHIFT;
5110
5111// Bits 20-22: base type affinity override for custom type columns.
5112// 0 = not set (use ty_str-based affinity), 1-5 = Affinity value + 1
5113const BASE_AFF_SHIFT: u32 = COLL_SHIFT + 12;
5114const BASE_AFF_MASK: u32 = 0b111 << BASE_AFF_SHIFT;
5115
5116// Bits 23-25: array dimensions (0 = scalar, 1-7 = number of [] dimensions)
5117const ARRAY_DIM_SHIFT: u32 = BASE_AFF_SHIFT + 3;
5118const ARRAY_DIM_MASK: u32 = 0b111 << ARRAY_DIM_SHIFT;
5119
5120impl Column {
5121    pub fn affinity(&self) -> Affinity {
5122        let v = ((self.raw & BASE_AFF_MASK) >> BASE_AFF_SHIFT) as u8;
5123        if v > 0 {
5124            // Custom type column: use the base type's affinity
5125            match v {
5126                1 => Affinity::Integer,
5127                2 => Affinity::Text,
5128                3 => Affinity::Blob,
5129                4 => Affinity::Real,
5130                _ => Affinity::Numeric,
5131            }
5132        } else {
5133            Affinity::affinity(&self.ty_str)
5134        }
5135    }
5136
5137    /// Set the base type affinity override for a custom type column.
5138    /// This ensures affinity rules use the custom type's BASE type
5139    /// rather than applying SQLite name-based rules to the type name.
5140    pub fn set_base_affinity(&mut self, affinity: Affinity) {
5141        let v: u32 = match affinity {
5142            Affinity::Integer => 1,
5143            Affinity::Text => 2,
5144            Affinity::Blob => 3,
5145            Affinity::Real => 4,
5146            Affinity::Numeric => 5,
5147        };
5148        self.raw = (self.raw & !BASE_AFF_MASK) | ((v << BASE_AFF_SHIFT) & BASE_AFF_MASK);
5149    }
5150    pub fn affinity_with_strict(&self, is_strict: bool) -> Affinity {
5151        if is_strict && self.ty_str.eq_ignore_ascii_case("ANY") {
5152            Affinity::Blob
5153        } else {
5154            self.affinity()
5155        }
5156    }
5157    pub fn new_default_text(
5158        name: Option<String>,
5159        ty_str: String,
5160        default: Option<Box<Expr>>,
5161    ) -> Self {
5162        Self::new(
5163            name,
5164            ty_str,
5165            default,
5166            None,
5167            Type::Text,
5168            None,
5169            ColDef::default(),
5170        )
5171    }
5172    pub fn new_default_integer(
5173        name: Option<String>,
5174        ty_str: String,
5175        default: Option<Box<Expr>>,
5176    ) -> Self {
5177        Self::new(
5178            name,
5179            ty_str,
5180            default,
5181            None,
5182            Type::Integer,
5183            None,
5184            ColDef::default(),
5185        )
5186    }
5187    #[inline]
5188    pub fn new(
5189        name: Option<String>,
5190        ty_str: String,
5191        default: Option<Box<Expr>>,
5192        generated: Option<Box<Expr>>,
5193        ty: Type,
5194        col: Option<CollationSeq>,
5195        coldef: ColDef,
5196    ) -> Self {
5197        let generated_type = match generated {
5198            Some(expr) => {
5199                let original_sql = expr.to_string();
5200                GeneratedType::Virtual { expr, original_sql }
5201            }
5202            None => GeneratedType::NotGenerated,
5203        };
5204        let mut raw = 0u32;
5205        raw |= (ty as u32) << TYPE_SHIFT;
5206        if let Some(c) = col {
5207            raw |= (u32::from(c.to_bits()) << COLL_SHIFT) & COLL_MASK;
5208        }
5209        if coldef.primary_key {
5210            raw |= F_PRIMARY_KEY
5211        }
5212        if coldef.rowid_alias {
5213            raw |= F_ROWID_ALIAS
5214        }
5215        if coldef.notnull {
5216            raw |= F_NOTNULL
5217        }
5218        if coldef.unique {
5219            raw |= F_UNIQUE
5220        }
5221        if coldef.hidden {
5222            raw |= F_HIDDEN
5223        }
5224        Self {
5225            name,
5226            ty_str,
5227            ty_params: std::vec::Vec::new(),
5228            default,
5229            generated_type,
5230            raw,
5231            explicit_notnull: coldef.explicit_notnull,
5232            notnull_conflict_clause: coldef.notnull_conflict_clause,
5233        }
5234    }
5235    #[inline]
5236    pub const fn ty(&self) -> Type {
5237        let v = ((self.raw & TYPE_MASK) >> TYPE_SHIFT) as u8;
5238        Type::from_bits(v)
5239    }
5240
5241    #[inline]
5242    pub const fn set_ty(&mut self, ty: Type) {
5243        self.raw = (self.raw & !TYPE_MASK) | (((ty as u32) << TYPE_SHIFT) & TYPE_MASK);
5244    }
5245
5246    #[inline]
5247    pub const fn collation_opt(&self) -> Option<CollationSeq> {
5248        if self.has_explicit_collation() {
5249            Some(self.collation())
5250        } else {
5251            None
5252        }
5253    }
5254
5255    #[inline]
5256    pub const fn collation(&self) -> CollationSeq {
5257        let v = ((self.raw & COLL_MASK) >> COLL_SHIFT) as u16;
5258        if v == CollationSeq::Unset.to_bits() {
5259            CollationSeq::Binary
5260        } else {
5261            CollationSeq::from_storage_bits(v)
5262        }
5263    }
5264
5265    #[inline]
5266    pub const fn has_explicit_collation(&self) -> bool {
5267        let v = ((self.raw & COLL_MASK) >> COLL_SHIFT) as u16;
5268        v != CollationSeq::Unset.to_bits()
5269    }
5270
5271    #[inline]
5272    pub const fn set_collation(&mut self, c: Option<CollationSeq>) {
5273        if let Some(c) = c {
5274            self.raw = (self.raw & !COLL_MASK) | (((c.to_bits() as u32) << COLL_SHIFT) & COLL_MASK);
5275        }
5276    }
5277
5278    #[inline]
5279    pub fn primary_key(&self) -> bool {
5280        self.raw & F_PRIMARY_KEY != 0
5281    }
5282    #[inline]
5283    pub const fn is_rowid_alias(&self) -> bool {
5284        self.raw & F_ROWID_ALIAS != 0
5285    }
5286    #[inline]
5287    pub const fn notnull(&self) -> bool {
5288        self.raw & F_NOTNULL != 0
5289    }
5290    #[inline]
5291    pub const fn explicit_notnull(&self) -> bool {
5292        self.explicit_notnull
5293    }
5294    #[inline]
5295    pub const fn unique(&self) -> bool {
5296        self.raw & F_UNIQUE != 0
5297    }
5298    #[inline]
5299    pub const fn hidden(&self) -> bool {
5300        self.raw & F_HIDDEN != 0
5301    }
5302
5303    /// Returns an error if this column is a generated column.
5304    /// `verb_phrase` should describe the operation, e.g. "INSERT into" or "UPDATE".
5305    pub fn ensure_not_generated(&self, verb_phrase: &str, col_name: &str) -> Result<()> {
5306        if !matches!(self.generated_type, GeneratedType::NotGenerated) {
5307            bail_parse_error!("cannot {} generated column \"{}\"", verb_phrase, col_name);
5308        }
5309        Ok(())
5310    }
5311
5312    #[inline]
5313    pub fn generated_type(&self) -> &GeneratedType {
5314        &self.generated_type
5315    }
5316
5317    #[inline]
5318    pub const fn is_generated(&self) -> bool {
5319        !matches!(self.generated_type, GeneratedType::NotGenerated)
5320    }
5321
5322    #[inline]
5323    pub const fn is_virtual_generated(&self) -> bool {
5324        matches!(self.generated_type, GeneratedType::Virtual { .. })
5325    }
5326
5327    #[inline]
5328    pub fn generated_expr(&self) -> Option<&Expr> {
5329        match &self.generated_type {
5330            GeneratedType::Virtual { expr, .. } => Some(expr.as_ref()),
5331            GeneratedType::NotGenerated => None,
5332        }
5333    }
5334
5335    #[inline]
5336    pub fn generated_expr_mut(&mut self) -> Option<&mut Expr> {
5337        match &mut self.generated_type {
5338            GeneratedType::Virtual { expr, .. } => Some(expr.as_mut()),
5339            GeneratedType::NotGenerated => None,
5340        }
5341    }
5342
5343    #[inline]
5344    pub fn set_generated_original_sql(&mut self, new_sql: String) {
5345        if let GeneratedType::Virtual {
5346            ref mut original_sql,
5347            ..
5348        } = self.generated_type
5349        {
5350            *original_sql = new_sql;
5351        }
5352    }
5353
5354    #[inline]
5355    pub const fn set_primary_key(&mut self, v: bool) {
5356        self.set_flag(F_PRIMARY_KEY, v);
5357    }
5358    #[inline]
5359    pub const fn set_rowid_alias(&mut self, v: bool) {
5360        self.set_flag(F_ROWID_ALIAS, v);
5361    }
5362    #[inline]
5363    pub const fn set_notnull(&mut self, v: bool) {
5364        self.set_flag(F_NOTNULL, v);
5365    }
5366    #[inline]
5367    pub const fn set_unique(&mut self, v: bool) {
5368        self.set_flag(F_UNIQUE, v);
5369    }
5370    #[inline]
5371    pub const fn set_hidden(&mut self, v: bool) {
5372        self.set_flag(F_HIDDEN, v);
5373    }
5374
5375    #[inline]
5376    pub const fn is_array(&self) -> bool {
5377        (self.raw & ARRAY_DIM_MASK) != 0
5378    }
5379
5380    /// Number of array dimensions (0 = scalar, 1 = `[]`, 2 = `[][]`, etc.)
5381    #[inline]
5382    pub const fn array_dimensions(&self) -> u32 {
5383        (self.raw & ARRAY_DIM_MASK) >> ARRAY_DIM_SHIFT
5384    }
5385
5386    #[inline]
5387    pub fn set_array_dimensions(&mut self, dims: u32) {
5388        assert!(dims <= 7, "array dimensions must be <= 7");
5389        self.raw = (self.raw & !ARRAY_DIM_MASK) | (dims << ARRAY_DIM_SHIFT);
5390    }
5391
5392    #[inline]
5393    const fn set_flag(&mut self, mask: u32, val: bool) {
5394        if val {
5395            self.raw |= mask
5396        } else {
5397            self.raw &= !mask
5398        }
5399    }
5400}
5401
5402// TODO: This might replace some of util::columns_from_create_table_body
5403impl TryFrom<&ColumnDefinition> for Column {
5404    type Error = crate::LimboError;
5405
5406    fn try_from(value: &ColumnDefinition) -> crate::Result<Self> {
5407        let name = value.col_name.as_str();
5408
5409        let mut default = None;
5410        let mut generated = None;
5411        let mut notnull = false;
5412        let mut notnull_conflict_clause = None;
5413        let mut primary_key = false;
5414        let mut unique = false;
5415        let mut collation = None;
5416
5417        for ast::NamedColumnConstraint { constraint, .. } in &value.constraints {
5418            match constraint {
5419                ast::ColumnConstraint::PrimaryKey { .. } => primary_key = true,
5420                ast::ColumnConstraint::NotNull {
5421                    conflict_clause, ..
5422                } => {
5423                    notnull = true;
5424                    notnull_conflict_clause = *conflict_clause;
5425                }
5426                ast::ColumnConstraint::Unique(..) => unique = true,
5427                ast::ColumnConstraint::Default(expr) => {
5428                    default.replace(
5429                        translate_ident_to_string_literal(expr).unwrap_or_else(|| expr.clone()),
5430                    );
5431                }
5432                ast::ColumnConstraint::Collate { collation_name } => {
5433                    let collation_seq = CollationSeq::new(collation_name.as_str())?;
5434                    if collation_seq.is_custom() {
5435                        crate::bail_parse_error!(
5436                            "custom collations are not supported in schema definitions"
5437                        );
5438                    }
5439                    collation.replace(collation_seq);
5440                }
5441                ast::ColumnConstraint::Generated { expr, .. } => {
5442                    generated = Some(expr.clone());
5443                }
5444                _ => {}
5445            };
5446        }
5447
5448        let ty = match value.col_type {
5449            Some(ref data_type) => type_from_name(&data_type.name).0,
5450            None => Type::Null,
5451        };
5452
5453        let ty_str = value
5454            .col_type
5455            .as_ref()
5456            .map(|t| t.name.to_string())
5457            .unwrap_or_default();
5458
5459        let ty_params: std::vec::Vec<Box<turso_parser::ast::Expr>> = match &value.col_type {
5460            Some(ast::Type {
5461                size: Some(ast::TypeSize::MaxSize(ref expr)),
5462                ..
5463            }) => std::vec![expr.clone()],
5464            Some(ast::Type {
5465                size: Some(ast::TypeSize::TypeSize(ref e1, ref e2)),
5466                ..
5467            }) => std::vec![e1.clone(), e2.clone()],
5468            _ => std::vec::Vec::new(),
5469        };
5470
5471        let hidden = ty_str.contains("HIDDEN");
5472
5473        let mut col = Column::new(
5474            Some(name.to_string()),
5475            ty_str,
5476            default,
5477            generated,
5478            ty,
5479            collation,
5480            ColDef {
5481                primary_key,
5482                rowid_alias: primary_key && matches!(ty, Type::Integer),
5483                notnull,
5484                explicit_notnull: notnull,
5485                unique,
5486                hidden,
5487                notnull_conflict_clause,
5488            },
5489        );
5490        col.ty_params = ty_params;
5491        if let Some(t) = value.col_type.as_ref() {
5492            if t.is_array() {
5493                col.set_array_dimensions(t.array_dimensions);
5494            }
5495        }
5496        Ok(col)
5497    }
5498}
5499
5500#[repr(u8)]
5501#[derive(Debug, Clone, Copy, PartialEq)]
5502pub enum Type {
5503    Null = 0,
5504    Text = 1,
5505    Numeric = 2,
5506    Integer = 3,
5507    Real = 4,
5508    Blob = 5,
5509}
5510
5511impl Type {
5512    #[inline]
5513    const fn from_bits(bits: u8) -> Self {
5514        match bits {
5515            0 => Type::Null,
5516            1 => Type::Text,
5517            2 => Type::Numeric,
5518            3 => Type::Integer,
5519            4 => Type::Real,
5520            5 => Type::Blob,
5521            _ => Type::Null,
5522        }
5523    }
5524}
5525
5526impl fmt::Display for Type {
5527    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5528        let s = match self {
5529            Self::Null => "",
5530            Self::Text => "TEXT",
5531            Self::Numeric => "NUMERIC",
5532            Self::Integer => "INTEGER",
5533            Self::Real => "REAL",
5534            Self::Blob => "BLOB",
5535        };
5536        write!(f, "{s}")
5537    }
5538}
5539
5540pub fn sqlite_schema_table() -> Result<BTreeTable> {
5541    let columns = try_vec![
5542        Column::new_default_text(Some("type".to_string()), "TEXT".to_string(), None),
5543        Column::new_default_text(Some("name".to_string()), "TEXT".to_string(), None),
5544        Column::new_default_text(Some("tbl_name".to_string()), "TEXT".to_string(), None),
5545        Column::new_default_integer(Some("rootpage".to_string()), "INT".to_string(), None),
5546        Column::new_default_text(Some("sql".to_string()), "TEXT".to_string(), None),
5547    ]?;
5548    let logical_to_physical_map =
5549        BTreeTable::try_build_logical_to_physical_map(&columns, &[], true)?;
5550    Ok(BTreeTable {
5551        root_page: 1,
5552        name: "sqlite_schema".to_string(),
5553        has_rowid: true,
5554        is_strict: false,
5555        has_autoincrement: false,
5556        primary_key_columns: try_vec![]?,
5557        columns,
5558        foreign_keys: try_vec![]?,
5559        check_constraints: try_vec![]?,
5560        rowid_alias_conflict_clause: None,
5561        unique_sets: try_vec![]?,
5562        has_virtual_columns: false,
5563        logical_to_physical_map,
5564        column_dependencies: Default::default(),
5565    })
5566}
5567
5568#[allow(dead_code)]
5569#[derive(Debug, Clone)]
5570pub struct Index {
5571    pub name: String,
5572    pub table_name: String,
5573    pub root_page: i64,
5574    pub columns: Vec<IndexColumn>,
5575    pub unique: bool,
5576    pub ephemeral: bool,
5577    /// Does the index have a rowid as the last column?
5578    /// This is the case for btree indexes (persistent or ephemeral) that
5579    /// have been created based on a table with a rowid.
5580    /// For example, WITHOUT ROWID tables and SELECT DISTINCT ephemeral indexes
5581    /// will not have a rowid.
5582    pub has_rowid: bool,
5583    pub where_clause: Option<Box<Expr>>,
5584    pub index_method: Option<Arc<dyn IndexMethodAttachment>>,
5585    /// ON CONFLICT clause from the constraint definition (PRIMARY KEY or UNIQUE).
5586    pub on_conflict: Option<ResolveType>,
5587}
5588
5589#[allow(dead_code)]
5590#[derive(Debug, Clone)]
5591pub struct IndexColumn {
5592    pub name: String,
5593    pub order: SortOrder,
5594    /// the position of the column in the source table.
5595    /// for example:
5596    /// CREATE TABLE t (a,b,c)
5597    /// CREATE INDEX idx ON t(b)
5598    /// b.pos_in_table == 1
5599    pub pos_in_table: usize,
5600    pub collation: Option<CollationSeq>,
5601    pub default: Option<Box<Expr>>,
5602    /// Expression for expression indexes. None for simple column indexes.
5603    pub expr: Option<Box<Expr>>,
5604}
5605
5606impl Index {
5607    pub fn from_sql(
5608        syms: &SymbolTable,
5609        sql: &str,
5610        root_page: i64,
5611        table: &BTreeTable,
5612    ) -> Result<Index> {
5613        let mut parser = Parser::new(sql.as_bytes());
5614        let cmd = parser.next_cmd()?;
5615        match cmd {
5616            Some(Cmd::Stmt(Stmt::CreateIndex {
5617                idx_name,
5618                tbl_name,
5619                columns,
5620                unique,
5621                where_clause,
5622                using,
5623                with_clause,
5624                ..
5625            })) => {
5626                let index_name = normalize_ident(idx_name.name.as_str());
5627                let index_columns = resolve_sorted_columns(table, &columns)?;
5628                if let Some(using) = using {
5629                    if where_clause.is_some() {
5630                        bail_parse_error!("custom index module do not support partial indices");
5631                    }
5632                    if unique {
5633                        bail_parse_error!("custom index module do not support UNIQUE indices");
5634                    }
5635                    let parameters = resolve_index_method_parameters(with_clause)?;
5636                    let Some(module) = syms.index_methods.get(using.as_str()) else {
5637                        bail_parse_error!("unknown module name: '{}'", using);
5638                    };
5639                    let configuration = IndexMethodConfiguration {
5640                        table_name: table.name.clone(),
5641                        index_name: index_name.clone(),
5642                        columns: index_columns.try_clone()?,
5643                        parameters,
5644                    };
5645                    let descriptor = module.attach(&configuration)?;
5646                    Ok(Index {
5647                        name: index_name,
5648                        table_name: normalize_ident(tbl_name.as_str()),
5649                        root_page,
5650                        columns: index_columns,
5651                        unique: false,
5652                        ephemeral: false,
5653                        has_rowid: table.has_rowid,
5654                        where_clause: None,
5655                        index_method: Some(descriptor),
5656                        on_conflict: None,
5657                    })
5658                } else {
5659                    Ok(Index {
5660                        name: index_name,
5661                        table_name: normalize_ident(tbl_name.as_str()),
5662                        root_page,
5663                        columns: index_columns,
5664                        unique,
5665                        ephemeral: false,
5666                        has_rowid: table.has_rowid,
5667                        where_clause,
5668                        index_method: None,
5669                        on_conflict: None,
5670                    })
5671                }
5672            }
5673            _ => todo!("Expected create index statement"),
5674        }
5675    }
5676
5677    /// Check if this is an expression index.
5678    pub fn is_expression_index(&self) -> bool {
5679        self.columns.iter().any(|c| c.expr.is_some())
5680    }
5681
5682    /// check if this is special backing_btree index created and managed by custom index_method
5683    pub fn is_backing_btree_index(&self) -> bool {
5684        self.index_method
5685            .as_ref()
5686            .is_some_and(|x| x.definition().backing_btree)
5687    }
5688
5689    pub fn automatic_from_primary_key(
5690        table: &BTreeTable,
5691        auto_index: (String, i64), // name, root_page
5692        column_count: usize,
5693        conflict_clause: Option<ResolveType>,
5694        collation_overrides: &[Option<CollationSeq>],
5695    ) -> Result<Index> {
5696        let has_primary_key_index =
5697            table.get_rowid_alias_column().is_none() && !table.primary_key_columns.is_empty();
5698        assert!(has_primary_key_index);
5699        let (index_name, root_page) = auto_index;
5700
5701        let mut primary_keys = Vec::try_with_capacity_ext(column_count)?;
5702        for (i, (col_name, order)) in table.primary_key_columns.iter().enumerate() {
5703            let Some((pos_in_table, _)) = table.get_column(col_name) else {
5704                return Err(crate::LimboError::ParseError(format!(
5705                    "Column {} not found in table {}",
5706                    col_name, table.name
5707                )));
5708            };
5709            let (_, column) = table.get_column(col_name).unwrap();
5710            primary_keys
5711                .push_within_capacity(IndexColumn {
5712                    name: normalize_ident(col_name),
5713                    order: *order,
5714                    pos_in_table,
5715                    collation: collation_overrides
5716                        .get(i)
5717                        .copied()
5718                        .flatten()
5719                        .or_else(|| column.collation_opt()),
5720                    default: column.default.clone(),
5721                    expr: None,
5722                })
5723                .expect("primary key index columns vector was preallocated");
5724        }
5725
5726        assert!(primary_keys.len() == column_count);
5727
5728        Ok(Index {
5729            name: normalize_ident(index_name.as_str()),
5730            table_name: table.name.clone(),
5731            root_page,
5732            columns: primary_keys,
5733            unique: true,
5734            ephemeral: false,
5735            has_rowid: table.has_rowid,
5736            where_clause: None,
5737            index_method: None,
5738            on_conflict: conflict_clause,
5739        })
5740    }
5741
5742    pub fn automatic_from_unique(
5743        table: &BTreeTable,
5744        auto_index: (String, i64), // name, root_page
5745        column_indices_and_sort_orders: Vec<(usize, SortOrder)>,
5746        conflict_clause: Option<ResolveType>,
5747        collation_overrides: &[Option<CollationSeq>],
5748    ) -> Result<Index> {
5749        let (index_name, root_page) = auto_index;
5750
5751        let mut unique_cols = Vec::try_with_capacity_ext(column_indices_and_sort_orders.len())?;
5752        for (i, (pos, sort_order)) in column_indices_and_sort_orders.iter().enumerate() {
5753            let Some((pos_in_table, col)) = table
5754                .columns
5755                .iter()
5756                .enumerate()
5757                .find(|(pos_in_table, _)| pos == pos_in_table)
5758            else {
5759                return Err(crate::LimboError::ParseError(format!(
5760                    "Unique constraint column not found in table {}",
5761                    table.name
5762                )));
5763            };
5764            unique_cols
5765                .push_within_capacity(IndexColumn {
5766                    name: normalize_ident(col.name.as_ref().unwrap()),
5767                    order: *sort_order,
5768                    pos_in_table,
5769                    collation: collation_overrides
5770                        .get(i)
5771                        .copied()
5772                        .flatten()
5773                        .or_else(|| col.collation_opt()),
5774                    default: col.default.clone(),
5775                    expr: None,
5776                })
5777                .expect("unique index columns vector was preallocated");
5778        }
5779
5780        Ok(Index {
5781            name: normalize_ident(index_name.as_str()),
5782            table_name: table.name.clone(),
5783            root_page,
5784            columns: unique_cols,
5785            unique: true,
5786            ephemeral: false,
5787            has_rowid: table.has_rowid,
5788            where_clause: None,
5789            index_method: None,
5790            on_conflict: conflict_clause,
5791        })
5792    }
5793
5794    /// Given a column position in the table, return the position in the index.
5795    /// Returns None if the column is not found in the index.
5796    /// For example, given:
5797    /// CREATE TABLE t (a, b, c)
5798    /// CREATE INDEX idx ON t(b)
5799    /// then column_table_pos_to_index_pos(1) returns Some(0)
5800    pub fn column_table_pos_to_index_pos(&self, table_pos: usize) -> Option<usize> {
5801        self.columns
5802            .iter()
5803            .position(|c| c.pos_in_table == table_pos)
5804    }
5805
5806    /// Given an expression, return the position in the index if it matches an expression index column.
5807    /// Expression index matching is textual (after binding), so the caller should normalize the query
5808    /// expression to resemble the stored index expression (e.g. unqualified column names).
5809    pub fn expression_to_index_pos(&self, expr: &Expr) -> Option<usize> {
5810        self.columns.iter().position(|c| {
5811            c.expr
5812                .as_ref()
5813                .is_some_and(|e| exprs_are_equivalent(e, expr))
5814        })
5815    }
5816
5817    /// Walk the where_clause Expr of a partial index and validate that it doesn't reference any other
5818    /// tables or use any disallowed constructs.
5819    pub fn validate_where_expr(&self, table: &Table, _resolver: &Resolver) -> bool {
5820        let Some(where_clause) = &self.where_clause else {
5821            return true;
5822        };
5823
5824        let tbl_norm = self.table_name.as_str();
5825        let has_col = |name: &str| {
5826            table.columns().iter().any(|c| {
5827                c.name
5828                    .as_ref()
5829                    .is_some_and(|cn| cn.eq_ignore_ascii_case(name))
5830            })
5831        };
5832        let is_tbl = |ns: &str| normalize_ident(ns) == tbl_norm;
5833        let is_deterministic_fn = |name: &str, argc: usize| {
5834            let n = normalize_ident(name);
5835            Func::resolve_function(&n, argc).is_ok_and(|f| f.is_some_and(|f| f.is_deterministic()))
5836        };
5837
5838        let mut ok = true;
5839        let _ = walk_expr(where_clause.as_ref(), &mut |e: &Expr| -> crate::Result<
5840            WalkControl,
5841        > {
5842            if !ok {
5843                return Ok(WalkControl::SkipChildren);
5844            }
5845            match e {
5846                Expr::Literal(_) | Expr::RowId { .. } => {}
5847                // Unqualified identifier: must be a column of the target table or ROWID
5848                Expr::Id(n) => {
5849                    let n = n.as_str();
5850                    if !ROWID_STRS.iter().any(|s| s.eq_ignore_ascii_case(n)) && !has_col(n) {
5851                        ok = false;
5852                    }
5853                }
5854                // Qualified: qualifier must match this index's table; column must exist
5855                Expr::Qualified(ns, col) | Expr::DoublyQualified(_, ns, col) => {
5856                    if !is_tbl(ns.as_str()) || !has_col(col.as_str()) {
5857                        ok = false;
5858                    }
5859                }
5860                Expr::FunctionCall {
5861                    name, filter_over, ..
5862                }
5863                | Expr::FunctionCallStar {
5864                    name, filter_over, ..
5865                } => {
5866                    // reject windowed
5867                    if filter_over.over_clause.is_some() {
5868                        ok = false;
5869                    } else {
5870                        let argc = match e {
5871                            Expr::FunctionCall { args, .. } => args.len(),
5872                            Expr::FunctionCallStar { .. } => 0,
5873                            _ => unreachable!(),
5874                        };
5875                        // Reject non-deterministic functions. Function arguments can reference
5876                        // columns of the indexed table (e.g., LENGTH(t0.c0)), which will be
5877                        // validated by the Expr::Id and Expr::Qualified cases during the walk.
5878                        if !is_deterministic_fn(name.as_str(), argc) {
5879                            ok = false;
5880                        }
5881                    }
5882                }
5883                // Explicitly disallowed constructs
5884                Expr::Exists(_)
5885                | Expr::InSelect { .. }
5886                | Expr::Subquery(_)
5887                | Expr::Raise { .. }
5888                | Expr::Variable(_) => {
5889                    ok = false;
5890                }
5891                _ => {}
5892            }
5893            Ok(if ok {
5894                WalkControl::Continue
5895            } else {
5896                WalkControl::SkipChildren
5897            })
5898        });
5899        ok
5900    }
5901
5902    pub fn bind_where_expr(
5903        &self,
5904        table_refs: Option<&mut TableReferences>,
5905        resolver: &Resolver,
5906    ) -> Option<ast::Expr> {
5907        let Some(where_clause) = &self.where_clause else {
5908            return None;
5909        };
5910        let mut expr = where_clause.clone();
5911        bind_and_rewrite_expr(
5912            &mut expr,
5913            table_refs,
5914            None,
5915            resolver,
5916            BindingBehavior::ResultColumnsNotAllowed,
5917        )
5918        .ok()?;
5919        Some(*expr)
5920    }
5921}
5922
5923#[cfg(test)]
5924mod tests {
5925    use super::*;
5926    use crate::alloc::vec;
5927
5928    #[test]
5929    pub fn test_has_rowid_true() -> Result<()> {
5930        let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT);"#;
5931        let table = BTreeTable::from_sql(sql, 0)?;
5932        assert!(table.has_rowid, "has_rowid should be set to true");
5933        Ok(())
5934    }
5935
5936    #[test]
5937    pub fn test_has_rowid_false() -> Result<()> {
5938        let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT) WITHOUT ROWID;"#;
5939        let table = BTreeTable::from_sql(sql, 0)?;
5940        assert!(!table.has_rowid, "has_rowid should be set to false");
5941        Ok(())
5942    }
5943
5944    #[test]
5945    pub fn test_column_default_collation_is_effective_binary() -> Result<()> {
5946        let sql = r#"CREATE TABLE t1 (a TEXT);"#;
5947        let table = BTreeTable::from_sql(sql, 0)?;
5948        let column = table.get_column("a").unwrap().1;
5949        assert_eq!(column.collation(), CollationSeq::Binary);
5950        assert_eq!(column.collation_opt(), None);
5951        Ok(())
5952    }
5953
5954    #[test]
5955    pub fn test_column_is_rowid_alias_single_text() -> Result<()> {
5956        let sql = r#"CREATE TABLE t1 (a TEXT PRIMARY KEY, b TEXT);"#;
5957        let table = BTreeTable::from_sql(sql, 0)?;
5958        let column = table.get_column("a").unwrap().1;
5959        assert!(
5960            !column.is_rowid_alias(),
5961            "column 'a´ has type different than INTEGER so can't be a rowid alias"
5962        );
5963        Ok(())
5964    }
5965
5966    #[test]
5967    pub fn test_column_is_rowid_alias_single_integer() -> Result<()> {
5968        let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT);"#;
5969        let table = BTreeTable::from_sql(sql, 0)?;
5970        let column = table.get_column("a").unwrap().1;
5971        assert!(
5972            column.is_rowid_alias(),
5973            "column 'a´ should be a rowid alias"
5974        );
5975        Ok(())
5976    }
5977
5978    #[test]
5979    pub fn test_column_is_rowid_alias_single_integer_separate_primary_key_definition() -> Result<()>
5980    {
5981        let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, PRIMARY KEY(a));"#;
5982        let table = BTreeTable::from_sql(sql, 0)?;
5983        let column = table.get_column("a").unwrap().1;
5984        assert!(
5985            column.is_rowid_alias(),
5986            "column 'a´ should be a rowid alias"
5987        );
5988        Ok(())
5989    }
5990
5991    #[test]
5992    pub fn test_column_is_rowid_alias_single_integer_separate_primary_key_definition_without_rowid(
5993    ) -> Result<()> {
5994        let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, PRIMARY KEY(a)) WITHOUT ROWID;"#;
5995        let table = BTreeTable::from_sql(sql, 0)?;
5996        let column = table.get_column("a").unwrap().1;
5997        assert!(
5998            !column.is_rowid_alias(),
5999            "column 'a´ shouldn't be a rowid alias because table has no rowid"
6000        );
6001        Ok(())
6002    }
6003
6004    #[test]
6005    pub fn test_column_is_rowid_alias_single_integer_without_rowid() -> Result<()> {
6006        let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT) WITHOUT ROWID;"#;
6007        let table = BTreeTable::from_sql(sql, 0)?;
6008        let column = table.get_column("a").unwrap().1;
6009        assert!(
6010            !column.is_rowid_alias(),
6011            "column 'a´ shouldn't be a rowid alias because table has no rowid"
6012        );
6013        Ok(())
6014    }
6015
6016    #[test]
6017    pub fn test_multiple_pk_forbidden() -> Result<()> {
6018        let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT PRIMARY KEY);"#;
6019        let table = BTreeTable::from_sql(sql, 0);
6020        let error = table.unwrap_err();
6021        assert!(
6022            matches!(error, LimboError::ParseError(e) if e.contains("table \"t1\" has more than one primary key"))
6023        );
6024        Ok(())
6025    }
6026
6027    #[test]
6028    pub fn test_column_is_rowid_alias_separate_composite_primary_key_definition() -> Result<()> {
6029        let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, PRIMARY KEY(a, b));"#;
6030        let table = BTreeTable::from_sql(sql, 0)?;
6031        let column = table.get_column("a").unwrap().1;
6032        assert!(
6033            !column.is_rowid_alias(),
6034            "column 'a´ shouldn't be a rowid alias because table has composite primary key"
6035        );
6036        Ok(())
6037    }
6038
6039    #[test]
6040    pub fn test_primary_key_inline_single() -> Result<()> {
6041        let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT, c REAL);"#;
6042        let table = BTreeTable::from_sql(sql, 0)?;
6043        let column = table.get_column("a").unwrap().1;
6044        assert!(column.primary_key(), "column 'a' should be a primary key");
6045        let column = table.get_column("b").unwrap().1;
6046        assert!(
6047            !column.primary_key(),
6048            "column 'b' shouldn't be a primary key"
6049        );
6050        let column = table.get_column("c").unwrap().1;
6051        assert!(
6052            !column.primary_key(),
6053            "column 'c' shouldn't be a primary key"
6054        );
6055        assert_eq!(
6056            vec![("a".to_string(), SortOrder::Asc)],
6057            table.primary_key_columns,
6058            "primary key column names should be ['a']"
6059        );
6060        Ok(())
6061    }
6062
6063    #[test]
6064    pub fn test_primary_key_inline_multiple_forbidden() -> Result<()> {
6065        let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT PRIMARY KEY, c REAL);"#;
6066        let table = BTreeTable::from_sql(sql, 0);
6067        let error = table.unwrap_err();
6068        assert!(
6069            matches!(error, LimboError::ParseError(e) if e.contains("table \"t1\" has more than one primary key"))
6070        );
6071        Ok(())
6072    }
6073
6074    #[test]
6075    pub fn test_conflicting_on_conflict_unique_rejected() -> Result<()> {
6076        let sql =
6077            r#"CREATE TABLE t1 (a UNIQUE ON CONFLICT FAIL, b, UNIQUE(a) ON CONFLICT IGNORE);"#;
6078        let table = BTreeTable::from_sql(sql, 0);
6079        let error = table.unwrap_err();
6080        assert!(
6081            matches!(error, LimboError::ParseError(e) if e.contains("conflicting ON CONFLICT clauses"))
6082        );
6083        Ok(())
6084    }
6085
6086    #[test]
6087    pub fn test_conflicting_on_conflict_composite_unique_rejected() -> Result<()> {
6088        let sql = r#"CREATE TABLE t1 (a, b, UNIQUE(a, b) ON CONFLICT FAIL, UNIQUE(a, b) ON CONFLICT IGNORE);"#;
6089        let table = BTreeTable::from_sql(sql, 0);
6090        let error = table.unwrap_err();
6091        assert!(
6092            matches!(error, LimboError::ParseError(e) if e.contains("conflicting ON CONFLICT clauses"))
6093        );
6094        Ok(())
6095    }
6096
6097    #[test]
6098    pub fn test_same_on_conflict_unique_allowed() -> Result<()> {
6099        let sql = r#"CREATE TABLE t1 (a UNIQUE ON CONFLICT FAIL, b, UNIQUE(a) ON CONFLICT FAIL);"#;
6100        assert!(BTreeTable::from_sql(sql, 0).is_ok());
6101        Ok(())
6102    }
6103
6104    #[test]
6105    pub fn test_one_on_conflict_unique_allowed() -> Result<()> {
6106        let sql = r#"CREATE TABLE t1 (a UNIQUE ON CONFLICT FAIL, b, UNIQUE(a));"#;
6107        assert!(BTreeTable::from_sql(sql, 0).is_ok());
6108        Ok(())
6109    }
6110
6111    #[test]
6112    pub fn test_primary_key_separate_single() -> Result<()> {
6113        let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a desc));"#;
6114        let table = BTreeTable::from_sql(sql, 0)?;
6115        let column = table.get_column("a").unwrap().1;
6116        assert!(column.primary_key(), "column 'a' should be a primary key");
6117        let column = table.get_column("b").unwrap().1;
6118        assert!(
6119            !column.primary_key(),
6120            "column 'b' shouldn't be a primary key"
6121        );
6122        let column = table.get_column("c").unwrap().1;
6123        assert!(
6124            !column.primary_key(),
6125            "column 'c' shouldn't be a primary key"
6126        );
6127        assert_eq!(
6128            vec![("a".to_string(), SortOrder::Desc)],
6129            table.primary_key_columns,
6130            "primary key column names should be ['a']"
6131        );
6132        Ok(())
6133    }
6134
6135    #[test]
6136    pub fn test_primary_key_separate_multiple() -> Result<()> {
6137        let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY(a, b desc));"#;
6138        let table = BTreeTable::from_sql(sql, 0)?;
6139        let column = table.get_column("a").unwrap().1;
6140        assert!(column.primary_key(), "column 'a' should be a primary key");
6141        let column = table.get_column("b").unwrap().1;
6142        assert!(column.primary_key(), "column 'b' shouldn be a primary key");
6143        let column = table.get_column("c").unwrap().1;
6144        assert!(
6145            !column.primary_key(),
6146            "column 'c' shouldn't be a primary key"
6147        );
6148        assert_eq!(
6149            vec![
6150                ("a".to_string(), SortOrder::Asc),
6151                ("b".to_string(), SortOrder::Desc)
6152            ],
6153            table.primary_key_columns,
6154            "primary key column names should be ['a', 'b']"
6155        );
6156        Ok(())
6157    }
6158
6159    #[test]
6160    pub fn test_primary_key_separate_single_quoted() -> Result<()> {
6161        let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY('a'));"#;
6162        let table = BTreeTable::from_sql(sql, 0)?;
6163        let column = table.get_column("a").unwrap().1;
6164        assert!(column.primary_key(), "column 'a' should be a primary key");
6165        let column = table.get_column("b").unwrap().1;
6166        assert!(
6167            !column.primary_key(),
6168            "column 'b' shouldn't be a primary key"
6169        );
6170        let column = table.get_column("c").unwrap().1;
6171        assert!(
6172            !column.primary_key(),
6173            "column 'c' shouldn't be a primary key"
6174        );
6175        assert_eq!(
6176            vec![("a".to_string(), SortOrder::Asc)],
6177            table.primary_key_columns,
6178            "primary key column names should be ['a']"
6179        );
6180        Ok(())
6181    }
6182    #[test]
6183    pub fn test_primary_key_separate_single_doubly_quoted() -> Result<()> {
6184        let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, c REAL, PRIMARY KEY("a"));"#;
6185        let table = BTreeTable::from_sql(sql, 0)?;
6186        let column = table.get_column("a").unwrap().1;
6187        assert!(column.primary_key(), "column 'a' should be a primary key");
6188        let column = table.get_column("b").unwrap().1;
6189        assert!(
6190            !column.primary_key(),
6191            "column 'b' shouldn't be a primary key"
6192        );
6193        let column = table.get_column("c").unwrap().1;
6194        assert!(
6195            !column.primary_key(),
6196            "column 'c' shouldn't be a primary key"
6197        );
6198        assert_eq!(
6199            vec![("a".to_string(), SortOrder::Asc)],
6200            table.primary_key_columns,
6201            "primary key column names should be ['a']"
6202        );
6203        Ok(())
6204    }
6205
6206    #[test]
6207    pub fn test_default_value() -> Result<()> {
6208        let sql = r#"CREATE TABLE t1 (a INTEGER DEFAULT 23);"#;
6209        let table = BTreeTable::from_sql(sql, 0)?;
6210        let column = table.get_column("a").unwrap().1;
6211        let default = column.default.clone().unwrap();
6212        assert_eq!(default.to_string(), "23");
6213        Ok(())
6214    }
6215
6216    #[test]
6217    pub fn test_col_notnull() -> Result<()> {
6218        let sql = r#"CREATE TABLE t1 (a INTEGER NOT NULL);"#;
6219        let table = BTreeTable::from_sql(sql, 0)?;
6220        let column = table.get_column("a").unwrap().1;
6221        assert!(column.notnull());
6222        Ok(())
6223    }
6224
6225    #[test]
6226    pub fn test_col_notnull_negative() -> Result<()> {
6227        let sql = r#"CREATE TABLE t1 (a INTEGER);"#;
6228        let table = BTreeTable::from_sql(sql, 0)?;
6229        let column = table.get_column("a").unwrap().1;
6230        assert!(!column.notnull());
6231        Ok(())
6232    }
6233
6234    #[test]
6235    pub fn test_col_type_string_integer() -> Result<()> {
6236        let sql = r#"CREATE TABLE t1 (a InTeGeR);"#;
6237        let table = BTreeTable::from_sql(sql, 0)?;
6238        let column = table.get_column("a").unwrap().1;
6239        assert_eq!(column.ty_str, "InTeGeR");
6240        Ok(())
6241    }
6242
6243    #[test]
6244    pub fn test_sqlite_schema() -> Result<()> {
6245        let expected = r#"CREATE TABLE sqlite_schema (type TEXT, name TEXT, tbl_name TEXT, rootpage INT, sql TEXT)"#;
6246        let actual = sqlite_schema_table()?.to_sql();
6247        assert_eq!(expected, actual);
6248        Ok(())
6249    }
6250
6251    #[test]
6252    pub fn test_special_column_names() -> Result<()> {
6253        let tests = [
6254            ("foobar", "CREATE TABLE t (foobar TEXT)"),
6255            ("_table_name3", r#"CREATE TABLE t (_table_name3 TEXT)"#),
6256            ("special name", r#"CREATE TABLE t ("special name" TEXT)"#),
6257            ("foo&bar", r#"CREATE TABLE t ("foo&bar" TEXT)"#),
6258            (" name", r#"CREATE TABLE t (" name" TEXT)"#),
6259        ];
6260
6261        for (input_column_name, expected_sql) in tests {
6262            let sql = format!(r#"CREATE TABLE t ("{input_column_name}" TEXT)"#);
6263            let actual = BTreeTable::from_sql(&sql, 0)?.to_sql();
6264            assert_eq!(expected_sql, actual);
6265        }
6266
6267        Ok(())
6268    }
6269
6270    #[test]
6271    fn test_special_table_names_are_quoted_in_to_sql() -> Result<()> {
6272        let tests = [
6273            (
6274                r#"CREATE TABLE "t t" (x TEXT)"#,
6275                r#"CREATE TABLE "t t" (x TEXT)"#,
6276            ),
6277            (
6278                r#"CREATE TABLE "123table" (x TEXT)"#,
6279                r#"CREATE TABLE "123table" (x TEXT)"#,
6280            ),
6281            (
6282                r#"CREATE TABLE "t""t" (x TEXT)"#,
6283                r#"CREATE TABLE "t""t" (x TEXT)"#,
6284            ),
6285        ];
6286
6287        for (input_sql, expected_sql) in tests {
6288            let actual = BTreeTable::from_sql(input_sql, 0)?.to_sql();
6289            assert_eq!(actual, expected_sql);
6290        }
6291
6292        Ok(())
6293    }
6294
6295    #[test]
6296    #[should_panic]
6297    fn test_automatic_index_single_column() {
6298        // Without composite primary keys, we should not have an automatic index on a primary key that is a rowid alias
6299        let sql = r#"CREATE TABLE t1 (a INTEGER PRIMARY KEY, b TEXT);"#;
6300        let table = BTreeTable::from_sql(sql, 0).unwrap();
6301        let _index = Index::automatic_from_primary_key(
6302            &table,
6303            ("sqlite_autoindex_t1_1".to_string(), 2),
6304            1,
6305            None,
6306            &[],
6307        )
6308        .unwrap();
6309    }
6310
6311    #[test]
6312    fn test_automatic_index_composite_key() -> Result<()> {
6313        let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT, PRIMARY KEY(a, b));"#;
6314        let table = BTreeTable::from_sql(sql, 0)?;
6315        let index = Index::automatic_from_primary_key(
6316            &table,
6317            ("sqlite_autoindex_t1_1".to_string(), 2),
6318            2,
6319            None,
6320            &[],
6321        )?;
6322
6323        assert_eq!(index.name, "sqlite_autoindex_t1_1");
6324        assert_eq!(index.table_name, "t1");
6325        assert_eq!(index.root_page, 2);
6326        assert!(index.unique);
6327        assert_eq!(index.columns.len(), 2);
6328        assert_eq!(index.columns[0].name, "a");
6329        assert_eq!(index.columns[1].name, "b");
6330        assert!(matches!(index.columns[0].order, SortOrder::Asc));
6331        assert!(matches!(index.columns[1].order, SortOrder::Asc));
6332        Ok(())
6333    }
6334
6335    #[test]
6336    #[should_panic]
6337    fn test_automatic_index_no_primary_key() {
6338        let sql = r#"CREATE TABLE t1 (a INTEGER, b TEXT);"#;
6339        let table = BTreeTable::from_sql(sql, 0).unwrap();
6340        Index::automatic_from_primary_key(
6341            &table,
6342            ("sqlite_autoindex_t1_1".to_string(), 2),
6343            1,
6344            None,
6345            &[],
6346        )
6347        .unwrap();
6348    }
6349
6350    #[test]
6351    fn test_automatic_index_nonexistent_column() {
6352        // Create a table with a primary key column that doesn't exist in the table
6353        let columns = vec![Column::new_default_integer(
6354            Some("a".to_string()),
6355            "INT".to_string(),
6356            None,
6357        )];
6358        let logical_to_physical_map =
6359            BTreeTable::build_logical_to_physical_map(&columns, &[], true);
6360        let table = BTreeTable {
6361            root_page: 0,
6362            name: "t1".to_string(),
6363            has_rowid: true,
6364            is_strict: false,
6365            has_autoincrement: false,
6366            primary_key_columns: vec![("nonexistent".to_string(), SortOrder::Asc)],
6367            columns,
6368            unique_sets: vec![],
6369            foreign_keys: vec![],
6370            check_constraints: vec![],
6371            rowid_alias_conflict_clause: None,
6372            has_virtual_columns: false,
6373            logical_to_physical_map,
6374            column_dependencies: Default::default(),
6375        };
6376
6377        let result = Index::automatic_from_primary_key(
6378            &table,
6379            ("sqlite_autoindex_t1_1".to_string(), 2),
6380            1,
6381            None,
6382            &[],
6383        );
6384        assert!(result.is_err());
6385    }
6386
6387    #[test]
6388    fn test_automatic_index_unique_column() -> Result<()> {
6389        let sql = r#"CREATE table t1 (x INTEGER, y INTEGER UNIQUE);"#;
6390        let table = BTreeTable::from_sql(sql, 0)?;
6391        let index = Index::automatic_from_unique(
6392            &table,
6393            ("sqlite_autoindex_t1_1".to_string(), 2),
6394            vec![(1, SortOrder::Asc)],
6395            None,
6396            &[],
6397        )?;
6398
6399        assert_eq!(index.name, "sqlite_autoindex_t1_1");
6400        assert_eq!(index.table_name, "t1");
6401        assert_eq!(index.root_page, 2);
6402        assert!(index.unique);
6403        assert_eq!(index.columns.len(), 1);
6404        assert_eq!(index.columns[0].name, "y");
6405        assert!(matches!(index.columns[0].order, SortOrder::Asc));
6406        Ok(())
6407    }
6408
6409    #[test]
6410    fn test_automatic_index_pkey_unique_column() -> Result<()> {
6411        let sql = r#"CREATE TABLE t1 (x PRIMARY KEY, y UNIQUE);"#;
6412        let table = BTreeTable::from_sql(sql, 0)?;
6413        let indices = [
6414            Index::automatic_from_primary_key(
6415                &table,
6416                ("sqlite_autoindex_t1_1".to_string(), 2),
6417                1,
6418                None,
6419                &[],
6420            )?,
6421            Index::automatic_from_unique(
6422                &table,
6423                ("sqlite_autoindex_t1_2".to_string(), 3),
6424                vec![(1, SortOrder::Asc)],
6425                None,
6426                &[],
6427            )?,
6428        ];
6429
6430        assert_eq!(indices[0].name, "sqlite_autoindex_t1_1");
6431        assert_eq!(indices[0].table_name, "t1");
6432        assert_eq!(indices[0].root_page, 2);
6433        assert!(indices[0].unique);
6434        assert_eq!(indices[0].columns.len(), 1);
6435        assert_eq!(indices[0].columns[0].name, "x");
6436        assert!(matches!(indices[0].columns[0].order, SortOrder::Asc));
6437
6438        assert_eq!(indices[1].name, "sqlite_autoindex_t1_2");
6439        assert_eq!(indices[1].table_name, "t1");
6440        assert_eq!(indices[1].root_page, 3);
6441        assert!(indices[1].unique);
6442        assert_eq!(indices[1].columns.len(), 1);
6443        assert_eq!(indices[1].columns[0].name, "y");
6444        assert!(matches!(indices[1].columns[0].order, SortOrder::Asc));
6445
6446        Ok(())
6447    }
6448
6449    #[test]
6450    fn test_automatic_index_pkey_many_unique_columns() -> Result<()> {
6451        let sql = r#"CREATE TABLE t1 (a PRIMARY KEY, b UNIQUE, c, d, UNIQUE(c, d));"#;
6452        let table = BTreeTable::from_sql(sql, 0)?;
6453        let auto_indices = [
6454            ("sqlite_autoindex_t1_1".to_string(), 2),
6455            ("sqlite_autoindex_t1_2".to_string(), 3),
6456            ("sqlite_autoindex_t1_3".to_string(), 4),
6457        ];
6458        let indices = vec![
6459            Index::automatic_from_primary_key(
6460                &table,
6461                ("sqlite_autoindex_t1_1".to_string(), 2),
6462                1,
6463                None,
6464                &[],
6465            )?,
6466            Index::automatic_from_unique(
6467                &table,
6468                ("sqlite_autoindex_t1_2".to_string(), 3),
6469                vec![(1, SortOrder::Asc)],
6470                None,
6471                &[],
6472            )?,
6473            Index::automatic_from_unique(
6474                &table,
6475                ("sqlite_autoindex_t1_3".to_string(), 4),
6476                vec![(2, SortOrder::Asc), (3, SortOrder::Asc)],
6477                None,
6478                &[],
6479            )?,
6480        ];
6481
6482        assert!(indices.len() == auto_indices.len());
6483
6484        for (pos, index) in indices.iter().enumerate() {
6485            let (index_name, root_page) = &auto_indices[pos];
6486            assert_eq!(index.name, *index_name);
6487            assert_eq!(index.table_name, "t1");
6488            assert_eq!(index.root_page, *root_page);
6489            assert!(index.unique);
6490
6491            if pos == 0 {
6492                assert_eq!(index.columns.len(), 1);
6493                assert_eq!(index.columns[0].name, "a");
6494            } else if pos == 1 {
6495                assert_eq!(index.columns.len(), 1);
6496                assert_eq!(index.columns[0].name, "b");
6497            } else if pos == 2 {
6498                assert_eq!(index.columns.len(), 2);
6499                assert_eq!(index.columns[0].name, "c");
6500                assert_eq!(index.columns[1].name, "d");
6501            }
6502
6503            assert!(matches!(index.columns[0].order, SortOrder::Asc));
6504        }
6505
6506        Ok(())
6507    }
6508
6509    #[test]
6510    fn test_automatic_index_unique_set_dedup() -> Result<()> {
6511        let sql = r#"CREATE TABLE t1 (a, b, UNIQUE(a, b), UNIQUE(a, b));"#;
6512        let table = BTreeTable::from_sql(sql, 0)?;
6513        let index = Index::automatic_from_unique(
6514            &table,
6515            ("sqlite_autoindex_t1_1".to_string(), 2),
6516            vec![(0, SortOrder::Asc), (1, SortOrder::Asc)],
6517            None,
6518            &[],
6519        )?;
6520
6521        assert_eq!(index.name, "sqlite_autoindex_t1_1");
6522        assert_eq!(index.table_name, "t1");
6523        assert_eq!(index.root_page, 2);
6524        assert!(index.unique);
6525        assert_eq!(index.columns.len(), 2);
6526        assert_eq!(index.columns[0].name, "a");
6527        assert!(matches!(index.columns[0].order, SortOrder::Asc));
6528        assert_eq!(index.columns[1].name, "b");
6529        assert!(matches!(index.columns[1].order, SortOrder::Asc));
6530
6531        Ok(())
6532    }
6533
6534    #[test]
6535    fn test_automatic_index_primary_key_is_unique() -> Result<()> {
6536        let sql = r#"CREATE TABLE t1 (a primary key unique);"#;
6537        let table = BTreeTable::from_sql(sql, 0)?;
6538        let index = Index::automatic_from_primary_key(
6539            &table,
6540            ("sqlite_autoindex_t1_1".to_string(), 2),
6541            1,
6542            None,
6543            &[],
6544        )?;
6545
6546        assert_eq!(index.name, "sqlite_autoindex_t1_1");
6547        assert_eq!(index.table_name, "t1");
6548        assert_eq!(index.root_page, 2);
6549        assert!(index.unique);
6550        assert_eq!(index.columns.len(), 1);
6551        assert_eq!(index.columns[0].name, "a");
6552        assert!(matches!(index.columns[0].order, SortOrder::Asc));
6553
6554        Ok(())
6555    }
6556
6557    #[test]
6558    fn test_automatic_index_primary_key_is_unique_and_composite() -> Result<()> {
6559        let sql = r#"CREATE TABLE t1 (a, b, PRIMARY KEY(a, b), UNIQUE(a, b));"#;
6560        let table = BTreeTable::from_sql(sql, 0)?;
6561        let index = Index::automatic_from_primary_key(
6562            &table,
6563            ("sqlite_autoindex_t1_1".to_string(), 2),
6564            2,
6565            None,
6566            &[],
6567        )?;
6568
6569        assert_eq!(index.name, "sqlite_autoindex_t1_1");
6570        assert_eq!(index.table_name, "t1");
6571        assert_eq!(index.root_page, 2);
6572        assert!(index.unique);
6573        assert_eq!(index.columns.len(), 2);
6574        assert_eq!(index.columns[0].name, "a");
6575        assert_eq!(index.columns[1].name, "b");
6576        assert!(matches!(index.columns[0].order, SortOrder::Asc));
6577
6578        Ok(())
6579    }
6580
6581    #[test]
6582    fn test_strict_table_to_sql() -> Result<()> {
6583        let sql = r#"CREATE TABLE test_strict (id INTEGER, name TEXT) STRICT"#;
6584        let table = BTreeTable::from_sql(sql, 0)?;
6585
6586        // Verify the table is marked as strict
6587        assert!(table.is_strict);
6588
6589        // Verify that to_sql() includes the STRICT keyword
6590        let reconstructed_sql = table.to_sql();
6591        assert!(
6592            reconstructed_sql.contains("STRICT"),
6593            "Reconstructed SQL should contain STRICT keyword: {reconstructed_sql}"
6594        );
6595        assert_eq!(
6596            reconstructed_sql,
6597            "CREATE TABLE test_strict (id INTEGER, name TEXT) STRICT"
6598        );
6599
6600        Ok(())
6601    }
6602
6603    #[test]
6604    fn test_non_strict_table_to_sql() -> Result<()> {
6605        let sql = r#"CREATE TABLE test_normal (id INTEGER, name TEXT)"#;
6606        let table = BTreeTable::from_sql(sql, 0)?;
6607
6608        // Verify the table is NOT marked as strict
6609        assert!(!table.is_strict);
6610
6611        // Verify that to_sql() does NOT include the STRICT keyword
6612        let reconstructed_sql = table.to_sql();
6613        assert!(
6614            !reconstructed_sql.contains("STRICT"),
6615            "Non-strict table SQL should not contain STRICT keyword: {reconstructed_sql}"
6616        );
6617        assert_eq!(
6618            reconstructed_sql,
6619            "CREATE TABLE test_normal (id INTEGER, name TEXT)"
6620        );
6621
6622        Ok(())
6623    }
6624
6625    #[test]
6626    fn test_autoincrement_preserved_in_to_sql() -> Result<()> {
6627        let sql = r#"CREATE TABLE t(id INTEGER PRIMARY KEY AUTOINCREMENT, doomed INT, v TEXT)"#;
6628        let table = BTreeTable::from_sql(sql, 0)?;
6629
6630        assert!(table.has_autoincrement);
6631        assert_eq!(
6632            table.to_sql(),
6633            "CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, doomed INT, v TEXT)"
6634        );
6635
6636        Ok(())
6637    }
6638
6639    #[test]
6640    fn test_without_rowid_preserved_in_sql() -> Result<()> {
6641        let sql = r#"CREATE TABLE t(code TEXT PRIMARY KEY, val TEXT) WITHOUT ROWID"#;
6642        let table = BTreeTable::from_sql(sql, 0)?;
6643        assert!(table.get_column("code").unwrap().1.notnull());
6644        assert_eq!(
6645            table.to_sql(),
6646            "CREATE TABLE t (code TEXT PRIMARY KEY, val TEXT) WITHOUT ROWID"
6647        );
6648        Ok(())
6649    }
6650
6651    #[test]
6652    fn test_strict_without_rowid_preserved_in_sql() -> Result<()> {
6653        let sql = r#"CREATE TABLE t(code TEXT PRIMARY KEY, val TEXT) STRICT, WITHOUT ROWID"#;
6654        let table = BTreeTable::from_sql(sql, 0)?;
6655        assert!(table.get_column("code").unwrap().1.notnull());
6656        assert_eq!(
6657            table.to_sql(),
6658            "CREATE TABLE t (code TEXT PRIMARY KEY, val TEXT) STRICT, WITHOUT ROWID"
6659        );
6660        Ok(())
6661    }
6662
6663    #[test]
6664    fn test_automatic_index_unique_and_a_pk() -> Result<()> {
6665        let sql = r#"CREATE TABLE t1 (a NUMERIC UNIQUE UNIQUE,  b TEXT PRIMARY KEY)"#;
6666        let table = BTreeTable::from_sql(sql, 0)?;
6667        let mut indexes = vec![
6668            Index::automatic_from_unique(
6669                &table,
6670                ("sqlite_autoindex_t1_1".to_string(), 2),
6671                vec![(0, SortOrder::Asc)],
6672                None,
6673                &[],
6674            )?,
6675            Index::automatic_from_primary_key(
6676                &table,
6677                ("sqlite_autoindex_t1_2".to_string(), 3),
6678                1,
6679                None,
6680                &[],
6681            )?,
6682        ];
6683
6684        assert!(indexes.len() == 2);
6685        let index = indexes.pop().unwrap();
6686        assert_eq!(index.name, "sqlite_autoindex_t1_2");
6687        assert_eq!(index.table_name, "t1");
6688        assert_eq!(index.root_page, 3);
6689        assert!(index.unique);
6690        assert_eq!(index.columns.len(), 1);
6691        assert_eq!(index.columns[0].name, "b");
6692        assert!(matches!(index.columns[0].order, SortOrder::Asc));
6693
6694        let index = indexes.pop().unwrap();
6695        assert_eq!(index.name, "sqlite_autoindex_t1_1");
6696        assert_eq!(index.table_name, "t1");
6697        assert_eq!(index.root_page, 2);
6698        assert!(index.unique);
6699        assert_eq!(index.columns.len(), 1);
6700        assert_eq!(index.columns[0].name, "a");
6701        assert!(matches!(index.columns[0].order, SortOrder::Asc));
6702
6703        Ok(())
6704    }
6705
6706    #[test]
6707    fn test_schema_loading_rejects_gencol_without_flag() {
6708        let mut schema = Schema::new();
6709        schema.generated_columns_enabled = false;
6710
6711        let result = schema.handle_schema_row(
6712            "table",
6713            "t1",
6714            "t1",
6715            2,
6716            Some("CREATE TABLE t1(a INTEGER, b AS (a*2))"),
6717            &SymbolTable::default(),
6718            &mut vec![],
6719            &mut HashMap::default(),
6720            &mut HashMap::default(),
6721            &mut HashMap::default(),
6722            &mut HashMap::default(),
6723            &|_| None,
6724        );
6725        assert!(result
6726            .unwrap_err()
6727            .to_string()
6728            .contains("generated columns"));
6729    }
6730
6731    fn indices(mask: &ColumnMask) -> Vec<usize> {
6732        let mut v: Vec<usize> = mask.iter().try_collect().unwrap();
6733        v.sort_unstable();
6734        v
6735    }
6736
6737    fn stored(bits: &ColumnMask) -> Vec<usize> {
6738        let mut v: Vec<usize> = bits.iter().try_collect().unwrap();
6739        v.sort_unstable();
6740        v
6741    }
6742
6743    #[test]
6744    fn gencol_graph_no_virtual_columns() -> Result<()> {
6745        let t = BTreeTable::from_sql("CREATE TABLE t(a, b)", 0)?;
6746        assert_eq!(indices(&t.columns_affected_by_update([0])?), vec![0]);
6747        assert_eq!(indices(&t.columns_affected_by_update([0, 1])?), vec![0, 1]);
6748        assert_eq!(stored(&t.dependencies_of_columns([0])?), vec![0]);
6749        assert_eq!(stored(&t.dependencies_of_columns([])?), Vec::<usize>::new());
6750        Ok(())
6751    }
6752
6753    #[test]
6754    fn gencol_graph_linear_chain() -> Result<()> {
6755        let t = BTreeTable::from_sql("CREATE TABLE t(a, b AS (a) VIRTUAL, c AS (b) VIRTUAL)", 0)?;
6756        // affected-by({a}) = {a, b, c}
6757        assert_eq!(indices(&t.columns_affected_by_update([0])?), vec![0, 1, 2]);
6758        // affected-by({b}) = {b, c} (b is virtual, but updating it still propagates through dependents)
6759        assert_eq!(indices(&t.columns_affected_by_update([1])?), vec![1, 2]);
6760        // deps-of({c}) = {a} (transitive stored deps of virtual c)
6761        assert_eq!(stored(&t.dependencies_of_columns([2])?), vec![0]);
6762        // deps-of({b}) = {a}
6763        assert_eq!(stored(&t.dependencies_of_columns([1])?), vec![0]);
6764        // deps-of({a}) = {a} (stored target included)
6765        assert_eq!(stored(&t.dependencies_of_columns([0])?), vec![0]);
6766        Ok(())
6767    }
6768
6769    #[test]
6770    fn gencol_graph_diamond() -> Result<()> {
6771        let t = BTreeTable::from_sql(
6772            "CREATE TABLE t(a, b AS (a) VIRTUAL, c AS (a) VIRTUAL, d AS (b + c) VIRTUAL)",
6773            0,
6774        )?;
6775        assert_eq!(
6776            indices(&t.columns_affected_by_update([0])?),
6777            vec![0, 1, 2, 3]
6778        );
6779        assert_eq!(stored(&t.dependencies_of_columns([3])?), vec![0]);
6780        assert_eq!(stored(&t.dependencies_of_columns([1])?), vec![0]);
6781        Ok(())
6782    }
6783
6784    #[test]
6785    fn gencol_graph_multiple_stored_roots() -> Result<()> {
6786        let t = BTreeTable::from_sql("CREATE TABLE t(a, b, c AS (a + b) VIRTUAL)", 0)?;
6787        assert_eq!(indices(&t.columns_affected_by_update([0])?), vec![0, 2]);
6788        assert_eq!(indices(&t.columns_affected_by_update([1])?), vec![1, 2]);
6789        assert_eq!(
6790            indices(&t.columns_affected_by_update([0, 1])?),
6791            vec![0, 1, 2]
6792        );
6793        assert_eq!(stored(&t.dependencies_of_columns([2])?), vec![0, 1]);
6794        Ok(())
6795    }
6796
6797    #[test]
6798    fn gencol_graph_empty_input() -> Result<()> {
6799        let t = BTreeTable::from_sql("CREATE TABLE t(a, b AS (a) VIRTUAL)", 0)?;
6800        assert!(t.columns_affected_by_update(std::iter::empty())?.is_empty());
6801        assert!(t.dependencies_of_columns(std::iter::empty())?.is_empty());
6802        Ok(())
6803    }
6804
6805    #[test]
6806    fn gencol_graph_disjoint_components() -> Result<()> {
6807        let t = BTreeTable::from_sql(
6808            "CREATE TABLE t(a, b AS (a) VIRTUAL, c, d AS (c) VIRTUAL)",
6809            0,
6810        )?;
6811        assert_eq!(indices(&t.columns_affected_by_update([0])?), vec![0, 1]);
6812        assert_eq!(indices(&t.columns_affected_by_update([2])?), vec![2, 3]);
6813        assert_eq!(stored(&t.dependencies_of_columns([1])?), vec![0]);
6814        assert_eq!(stored(&t.dependencies_of_columns([3])?), vec![2]);
6815        Ok(())
6816    }
6817
6818    #[test]
6819    fn gencol_graph_deep_chain() -> Result<()> {
6820        // Build 50-long chain: c0 (stored), c1 := c0, c2 := c1, ... c49 := c48.
6821        let mut sql = String::from("CREATE TABLE t(c0");
6822        for i in 1..50 {
6823            sql.push_str(&format!(", c{i} AS (c{prev}) VIRTUAL", prev = i - 1));
6824        }
6825        sql.push(')');
6826        let t = BTreeTable::from_sql(&sql, 0)?;
6827        // affected-by({c0}) = {c0..c49}
6828        let affected = t.columns_affected_by_update([0])?;
6829        assert_eq!(affected.count(), 50);
6830        // deps-of({c49}) = {c0}
6831        assert_eq!(stored(&t.dependencies_of_columns([49])?), vec![0]);
6832        Ok(())
6833    }
6834
6835    #[test]
6836    fn gencol_graph_very_deep_chain_no_stack_overflow() -> Result<()> {
6837        // Validates that the iterative Kahn's + DP don't blow the stack on
6838        // realistic worst-case generated-column depth.
6839        let mut sql = String::from("CREATE TABLE t(c0");
6840        for i in 1..500 {
6841            sql.push_str(&format!(", c{i} AS (c{prev}) VIRTUAL", prev = i - 1));
6842        }
6843        sql.push(')');
6844        let t = BTreeTable::from_sql(&sql, 0)?;
6845        assert_eq!(t.columns_affected_by_update([0])?.count(), 500);
6846        assert_eq!(stored(&t.dependencies_of_columns([499])?), vec![0]);
6847        Ok(())
6848    }
6849
6850    #[test]
6851    fn gencol_graph_rowid_sentinel_passthrough() -> Result<()> {
6852        let t = BTreeTable::from_sql("CREATE TABLE t(a, b AS (a) VIRTUAL)", 0)?;
6853        let affected = t.columns_affected_by_update([ROWID_SENTINEL])?;
6854        // ROWID_SENTINEL is preserved in the mask flag but does not propagate through the graph
6855        // (no generated column can depend on ROWID_SENTINEL directly).
6856        assert!(affected.get(ROWID_SENTINEL));
6857        assert_eq!(affected.count(), 1);
6858        Ok(())
6859    }
6860
6861    #[test]
6862    fn gencol_graph_transpose_duality() -> Result<()> {
6863        let t = BTreeTable::from_sql(
6864            "CREATE TABLE t(a, b AS (a) VIRTUAL, c AS (b) VIRTUAL, d AS (a + c) VIRTUAL)",
6865            0,
6866        )?;
6867        let graph = t.column_graph()?;
6868        // j ∈ dependencies[i] iff i ∈ dependents[j]
6869        for i in 0..graph.dependencies.len() {
6870            for j in graph.dependencies[i].iter() {
6871                assert!(
6872                    graph.dependents[j].get(i),
6873                    "transpose violated: {j} is in dependencies[{i}] but {i} is not in dependents[{j}]"
6874                );
6875            }
6876            for j in graph.dependents[i].iter() {
6877                assert!(
6878                    graph.dependencies[j].get(i),
6879                    "transpose violated: {j} is in dependents[{i}] but {i} is not in dependencies[{j}]"
6880                );
6881            }
6882        }
6883        Ok(())
6884    }
6885
6886    #[test]
6887    fn gencol_graph_idempotence() -> Result<()> {
6888        // affected_by(affected_by(xs)) == affected_by(xs).
6889        let t = BTreeTable::from_sql(
6890            "CREATE TABLE t(a, b, c AS (a) VIRTUAL, d AS (b + c) VIRTUAL)",
6891            0,
6892        )?;
6893        let once = t.columns_affected_by_update([0, 1])?;
6894        let twice = t.columns_affected_by_update(once.iter())?;
6895        assert_eq!(indices(&twice), indices(&once));
6896        Ok(())
6897    }
6898
6899    #[test]
6900    fn gencol_graph_union_monotonicity() -> Result<()> {
6901        // affected_by(A ∪ B) == affected_by(A) ∪ affected_by(B).
6902        let t = BTreeTable::from_sql(
6903            "CREATE TABLE t(a, b, c AS (a) VIRTUAL, d AS (b) VIRTUAL, e AS (c + d) VIRTUAL)",
6904            0,
6905        )?;
6906        let mut expected = t.columns_affected_by_update([0])?;
6907        let b_mask = t.columns_affected_by_update([1])?;
6908        expected.union_with(&b_mask).unwrap();
6909        let union_mask = t.columns_affected_by_update([0, 1])?;
6910        assert_eq!(indices(&union_mask), indices(&expected));
6911        Ok(())
6912    }
6913
6914    #[test]
6915    fn gencol_graph_cycle_rejected() {
6916        // Two-cycle: a := b, b := a. Must be rejected at CREATE TABLE time by Kahn's.
6917        let err = BTreeTable::from_sql(
6918            "CREATE TABLE t(stored, a AS (b) VIRTUAL, b AS (a) VIRTUAL)",
6919            0,
6920        )
6921        .expect_err("cycle must be rejected");
6922        assert!(
6923            err.to_string().contains("circular dependency")
6924                || err.to_string().contains("cannot reference itself"),
6925            "unexpected error: {err}"
6926        );
6927    }
6928
6929    #[test]
6930    fn gencol_graph_three_cycle_rejected() {
6931        // Three-cycle: a := b, b := c, c := a.
6932        let err = BTreeTable::from_sql(
6933            "CREATE TABLE t(stored, a AS (b) VIRTUAL, b AS (c) VIRTUAL, c AS (a) VIRTUAL)",
6934            0,
6935        )
6936        .expect_err("cycle must be rejected");
6937        assert!(err.to_string().contains("circular dependency"));
6938    }
6939
6940    #[test]
6941    fn gencol_graph_self_reference_rejected() {
6942        let err = BTreeTable::from_sql("CREATE TABLE t(a, b AS (b) VIRTUAL)", 0)
6943            .expect_err("self-reference must be rejected");
6944        assert!(err.to_string().contains("cannot reference itself"));
6945    }
6946
6947    #[test]
6948    #[allow(clippy::redundant_clone)]
6949    fn gencol_graph_clone_invalidates_cache() -> Result<()> {
6950        // After cloning a BTreeTable, the cache is fresh. Mutating columns on
6951        // the clone via `columns_mut()` keeps it fresh; `prepare_generated_columns`
6952        // rebuilds correctly.
6953        let original = BTreeTable::from_sql("CREATE TABLE t(a, b AS (a) VIRTUAL)", 0)?;
6954        // Force the cache to be populated on the original.
6955        let _ = original.columns_affected_by_update([0])?;
6956        assert!(original.peek_column_dependencies().is_some());
6957
6958        // Clone: ResetOnClone makes the cloned cache empty. We keep a real clone
6959        // (not a move) because the point of the test is that Clone produces a
6960        // fresh cache independently from the original.
6961        let cloned = original.clone();
6962        assert!(cloned.peek_column_dependencies().is_none());
6963        // Original's cache is still populated — clone didn't touch it.
6964        assert!(original.peek_column_dependencies().is_some());
6965
6966        // The clone still returns correct results — cache rebuilds lazily.
6967        assert_eq!(
6968            indices(&cloned.columns_affected_by_update([0])?),
6969            vec![0, 1]
6970        );
6971        assert!(cloned.peek_column_dependencies().is_some());
6972        Ok(())
6973    }
6974
6975    #[test]
6976    fn gencol_graph_columns_mut_invalidates_cache() -> Result<()> {
6977        let mut t = BTreeTable::from_sql("CREATE TABLE t(a, b AS (a) VIRTUAL)", 0)?;
6978        // Force the cache to be populated.
6979        let _ = t.columns_affected_by_update([0])?;
6980        assert!(t.peek_column_dependencies().is_some());
6981
6982        // Any access through columns_mut() wipes the cache, even if we don't mutate.
6983        let _ = t.columns_mut();
6984        assert!(t.peek_column_dependencies().is_none());
6985        Ok(())
6986    }
6987
6988    /// `install_sequence_descriptor` must surface an error when the
6989    /// persisted metadata is invalid (e.g. min > max) rather than
6990    /// silently dropping the sequence. The internal backing table is
6991    /// the only persistent record of the sequence; a silent drop would
6992    /// manifest later as a misleading "sequence does not exist" on the
6993    /// next nextval that masks real on-disk corruption.
6994    #[test]
6995    fn install_sequence_descriptor_rejects_invalid_metadata_with_corruption_error() {
6996        let mut schema = Schema::new();
6997        let bogus = SequenceMetadata {
6998            // increment of zero is universally invalid; Sequence::new
6999            // rejects it with a clear error.
7000            start: 0,
7001            increment: 0,
7002            min: 0,
7003            max: 100,
7004            cycle: false,
7005        };
7006        let result = schema.install_sequence_descriptor("broken_seq", bogus);
7007        let err = result.expect_err(
7008            "invalid persisted descriptor must surface as an error, not be silently dropped",
7009        );
7010        assert!(
7011            matches!(err, LimboError::Corrupt(_)),
7012            "expected Corrupt error for unreadable internal backing table, got: {err:?}",
7013        );
7014        assert!(
7015            !schema.sequences.contains_key("broken_seq"),
7016            "rejected descriptor must not land in the sequences map",
7017        );
7018    }
7019}