Skip to main content

quantydb_sqlite/
schema.rs

1//! The schema table.
2//!
3//! Every SQLite database keeps its own schema in an ordinary table rooted at
4//! page 1, with five columns: the kind of object, its name, the name of the
5//! table it belongs to, the page its b-tree starts on, and the statement
6//! that created it. Reading it is how anything else in the file is found.
7//!
8//! Three things about it are not obvious and each one has already produced
9//! a wrong assumption in this crate's tests:
10//!
11//! An index SQLite created on its own, for a primary key or a unique
12//! constraint, has no statement of its own and stores NULL there. Anything
13//! that expects text in that column falls over on the first database with a
14//! composite primary key in it.
15//!
16//! Views and triggers have no b-tree, so their root page is 0. That is not
17//! a page number that happens to be unusable, it is the absence of one, and
18//! it is worth a different type rather than a magic value.
19//!
20//! Objects whose name begins with `sqlite_` belong to SQLite itself.
21//! `sqlite_sequence` holds autoincrement counters, `sqlite_stat1` holds
22//! planner statistics, and `sqlite_autoindex_*` are the indexes above.
23//! Importing them would mean importing another engine's bookkeeping, so
24//! they are flagged here and skipped by the importer.
25
26use crate::error::{Result, SqliteError};
27use crate::record::SqliteValue;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum ObjectKind {
31    Table,
32    Index,
33    View,
34    Trigger,
35}
36
37impl ObjectKind {
38    fn parse(text: &str) -> Result<ObjectKind> {
39        Ok(match text {
40            "table" => ObjectKind::Table,
41            "index" => ObjectKind::Index,
42            "view" => ObjectKind::View,
43            "trigger" => ObjectKind::Trigger,
44            other => {
45                return Err(SqliteError::malformed(
46                    1,
47                    format!("the schema holds an object of unknown kind {other:?}"),
48                ))
49            }
50        })
51    }
52
53    pub fn as_str(self) -> &'static str {
54        match self {
55            ObjectKind::Table => "table",
56            ObjectKind::Index => "index",
57            ObjectKind::View => "view",
58            ObjectKind::Trigger => "trigger",
59        }
60    }
61}
62
63/// One row of the schema table.
64#[derive(Debug, Clone, PartialEq)]
65pub struct SchemaObject {
66    pub kind: ObjectKind,
67    pub name: String,
68    /// The table this belongs to. For a table it is its own name; for an
69    /// index, view or trigger it is what the object is defined over.
70    pub table_name: String,
71    /// Where this object's b-tree starts. None for views and triggers,
72    /// which have no b-tree at all.
73    pub root_page: Option<u32>,
74    /// The statement that created this object. None for indexes SQLite
75    /// created for a primary key or a unique constraint.
76    pub sql: Option<String>,
77}
78
79impl SchemaObject {
80    /// Whether this belongs to SQLite's own bookkeeping rather than to the
81    /// user's data.
82    pub fn is_internal(&self) -> bool {
83        let name = self.name.as_bytes();
84        name.len() >= 7 && name[..7].eq_ignore_ascii_case(b"sqlite_")
85    }
86}
87
88/// Everything the database says about itself.
89#[derive(Debug, Clone, PartialEq)]
90pub struct Schema {
91    objects: Vec<SchemaObject>,
92}
93
94impl Schema {
95    /// Build the schema from the rows of the schema table.
96    ///
97    /// `page_count` is used to reject root pages that point outside the
98    /// file, which is worth catching here rather than halfway through an
99    /// import.
100    pub(crate) fn from_rows(
101        rows: impl IntoIterator<Item = Vec<SqliteValue>>,
102        page_count: u32,
103    ) -> Result<Schema> {
104        let mut objects = Vec::new();
105        for values in rows {
106            if values.len() != 5 {
107                return Err(SqliteError::malformed(
108                    1,
109                    format!(
110                        "a schema row has {} columns, the schema table has five",
111                        values.len()
112                    ),
113                ));
114            }
115            let kind = match &values[0] {
116                SqliteValue::Text(t) => ObjectKind::parse(t)?,
117                other => {
118                    return Err(SqliteError::malformed(
119                        1,
120                        format!("a schema row's kind is {}, not text", other.type_name()),
121                    ))
122                }
123            };
124            let name = text_column(&values[1], "name")?;
125            let table_name = text_column(&values[2], "tbl_name")?;
126
127            let root_page = match &values[3] {
128                // views and triggers have no b-tree; both spellings of that
129                // appear in the wild
130                SqliteValue::Null => None,
131                SqliteValue::Integer(0) => None,
132                SqliteValue::Integer(n) if *n > 0 && *n <= page_count as i64 => Some(*n as u32),
133                other => {
134                    return Err(SqliteError::malformed(
135                        1,
136                        format!(
137                            "{name} has root page {other:?}, outside the file's 1..={page_count}"
138                        ),
139                    ))
140                }
141            };
142            if root_page.is_none() && matches!(kind, ObjectKind::Table | ObjectKind::Index) {
143                return Err(SqliteError::malformed(
144                    1,
145                    format!("the {} {name} has no root page", kind.as_str()),
146                ));
147            }
148
149            let sql = match &values[4] {
150                SqliteValue::Null => None,
151                SqliteValue::Text(t) => Some(t.clone()),
152                other => {
153                    return Err(SqliteError::malformed(
154                        1,
155                        format!("{name} has a {} statement, not text", other.type_name()),
156                    ))
157                }
158            };
159
160            objects.push(SchemaObject {
161                kind,
162                name,
163                table_name,
164                root_page,
165                sql,
166            });
167        }
168        Ok(Schema { objects })
169    }
170
171    pub fn objects(&self) -> &[SchemaObject] {
172        &self.objects
173    }
174
175    /// Every table, SQLite's own bookkeeping included. Callers that mean
176    /// the user's data want `user_tables`.
177    pub fn tables(&self) -> impl Iterator<Item = &SchemaObject> {
178        self.objects.iter().filter(|o| o.kind == ObjectKind::Table)
179    }
180
181    /// Every table that holds the user's own data.
182    pub fn user_tables(&self) -> impl Iterator<Item = &SchemaObject> {
183        self.tables().filter(|o| !o.is_internal())
184    }
185
186    /// Every index in the schema, the ones SQLite made for itself included.
187    /// Those have no statement of their own, which is how they are told
188    /// apart.
189    pub fn indexes(&self) -> impl Iterator<Item = &SchemaObject> {
190        self.objects.iter().filter(|o| o.kind == ObjectKind::Index)
191    }
192
193    /// Look an object up by name. SQLite compares identifiers without
194    /// regard to ASCII case, so `track` finds `Track`.
195    pub fn object(&self, name: &str) -> Option<&SchemaObject> {
196        self.objects
197            .iter()
198            .find(|o| o.name.eq_ignore_ascii_case(name))
199    }
200
201    /// The indexes defined over `table`, in the order the schema lists them.
202    pub fn indexes_for(&self, table: &str) -> impl Iterator<Item = &SchemaObject> {
203        let table = table.to_string();
204        self.objects.iter().filter(move |o| {
205            o.kind == ObjectKind::Index && o.table_name.eq_ignore_ascii_case(&table)
206        })
207    }
208}
209
210fn text_column(value: &SqliteValue, column: &str) -> Result<String> {
211    match value {
212        SqliteValue::Text(t) => Ok(t.clone()),
213        other => Err(SqliteError::malformed(
214            1,
215            format!("a schema row's {column} is {}, not text", other.type_name()),
216        )),
217    }
218}
219
220impl SchemaObject {
221    /// Parse this object's create statement into the shape of the table it
222    /// makes.
223    ///
224    /// Fails for anything that is not a table, and for a table whose
225    /// statement SQLite kept but this parser cannot read.
226    pub fn table_def(&self) -> Result<crate::ddl::TableDef> {
227        if self.kind != ObjectKind::Table {
228            return Err(SqliteError::unsupported(format!(
229                "{} is a {}, not a table",
230                self.name,
231                self.kind.as_str()
232            )));
233        }
234        match &self.sql {
235            Some(sql) => crate::ddl::parse_create_table(sql),
236            None => Err(SqliteError::malformed(
237                1,
238                format!("the table {} has no create statement", self.name),
239            )),
240        }
241    }
242}