Skip to main content

quantydb_sqlite/
survey.rs

1//! Which stored value belongs to which declared column, and what a column
2//! actually holds.
3//!
4//! These two things live together because the second cannot be done without
5//! the first. Counting what is in a column means knowing which value in the
6//! record is that column, and the record does not line up with the column
7//! list. Three rules pull them apart, and each was checked against a file
8//! written by SQLite rather than recalled:
9//!
10//! A virtual generated column occupies no slot in the record, while a
11//! stored one does. In `(a, v virtual, s stored)` the record holds two
12//! values, `a` and `s`. Zipping the declared columns against the stored
13//! values puts `s`'s value into `v`, and then every column after it, and
14//! nothing about the result looks wrong.
15//!
16//! A column that is an alias for the rowid is stored as NULL, and its value
17//! is the cell's rowid. Reading it as stored gives a column of nothing.
18//!
19//! A record may end early. `alter table add column` does not rewrite the
20//! existing rows, so a table with four columns can hold rows with two
21//! values in them, and the rest take the default from the declaration. A
22//! reader that treats a short record as corruption refuses ordinary files.
23
24use crate::affinity::{Affinity, StorageClass};
25use crate::ddl::TableDef;
26use crate::error::Result;
27use crate::record::SqliteValue;
28use crate::schema::SchemaObject;
29use crate::source::Source;
30use crate::tree::Row;
31use crate::Reader;
32
33/// Where a declared column's value comes from.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35enum Slot {
36    /// Position in the record.
37    Stored(usize),
38    /// Position in the record, which holds NULL; the value is the rowid.
39    RowidAlias(usize),
40    /// Computed on demand by SQLite and absent from the file.
41    Virtual,
42}
43
44/// A column's value in one row, once the record has been lined up with the
45/// declaration.
46#[derive(Debug, Clone, PartialEq)]
47pub enum Cell<'a> {
48    /// The value as stored.
49    Value(&'a SqliteValue),
50    /// The row's rowid, standing in for a column that aliases it.
51    Rowid(i64),
52    /// The record ended before this column, so its default applies.
53    Missing,
54    /// A virtual generated column: the file holds nothing for it.
55    Virtual,
56}
57
58/// Lines a table's records up with its declared columns.
59///
60/// Built once per table and used for every row, because the layout is a
61/// property of the declaration rather than of any particular row.
62pub struct RowLayout {
63    slots: Vec<Slot>,
64}
65
66impl RowLayout {
67    pub fn new(def: &TableDef) -> RowLayout {
68        if def.without_rowid {
69            return RowLayout::keyed(def);
70        }
71        let alias = def.rowid_alias().map(|c| c.name.clone());
72        let mut slots = Vec::with_capacity(def.columns.len());
73        let mut position = 0usize;
74        for column in &def.columns {
75            if column.generated.is_virtual() {
76                slots.push(Slot::Virtual);
77                continue;
78            }
79            let is_alias = alias
80                .as_deref()
81                .is_some_and(|a| a.eq_ignore_ascii_case(&column.name));
82            slots.push(if is_alias {
83                Slot::RowidAlias(position)
84            } else {
85                Slot::Stored(position)
86            });
87            position += 1;
88        }
89        RowLayout { slots }
90    }
91
92    /// The layout of a without rowid table, where the record is permuted.
93    ///
94    /// Such a table is stored as an index b-tree keyed by its primary key,
95    /// and the entry holds the key columns first, in key order, then the
96    /// remaining columns in declared order. Nothing in the bytes says which
97    /// arrangement is in force, so this is read off the declaration and is
98    /// the whole reason the create statement has to be parsed before such a
99    /// table can be read at all.
100    ///
101    /// A key column named in the primary key but missing from the column
102    /// list cannot happen in a database sqlite accepted, and if it does the
103    /// column simply takes the next position, which keeps the mapping
104    /// total rather than panicking on a file we did not write.
105    fn keyed(def: &TableDef) -> RowLayout {
106        let mut order: Vec<usize> = Vec::with_capacity(def.columns.len());
107        for key in &def.primary_key {
108            if let Some(index) = def.column_index(&key.name) {
109                if !order.contains(&index) {
110                    order.push(index);
111                }
112            }
113        }
114        for index in 0..def.columns.len() {
115            if !order.contains(&index) {
116                order.push(index);
117            }
118        }
119
120        // walk the record order, handing out positions, then put the slots
121        // back into declared order for the caller
122        let mut slots = vec![Slot::Virtual; def.columns.len()];
123        let mut position = 0usize;
124        for index in order {
125            if def.columns[index].generated.is_virtual() {
126                continue;
127            }
128            slots[index] = Slot::Stored(position);
129            position += 1;
130        }
131        RowLayout { slots }
132    }
133
134    /// How many columns the declaration has, virtual ones included.
135    pub fn declared_columns(&self) -> usize {
136        self.slots.len()
137    }
138
139    /// How many values a complete record of this table holds.
140    pub fn stored_columns(&self) -> usize {
141        self.slots
142            .iter()
143            .filter(|s| !matches!(s, Slot::Virtual))
144            .count()
145    }
146
147    /// The value of declared column `index` in `row`.
148    ///
149    /// Takes the unified row, so the same layout serves both kinds of
150    /// table: one call site, one set of rules, and no way for the two paths
151    /// to drift apart.
152    pub fn cell<'a>(&self, row: &'a Row, index: usize) -> Cell<'a> {
153        match self.slots.get(index) {
154            None => Cell::Missing,
155            Some(Slot::Virtual) => Cell::Virtual,
156            Some(Slot::RowidAlias(at)) => match row.rowid {
157                // the slot exists and holds NULL; the value is the rowid.
158                // if a file ever stored something else there, the rowid is
159                // still the authority, because that is what sqlite indexes
160                // and what other tables reference.
161                Some(rowid) => Cell::Rowid(rowid),
162                // a row without a rowid cannot have a column aliasing one,
163                // so this is a file disagreeing with itself. report what is
164                // actually stored rather than inventing a number.
165                None => match row.values.get(*at) {
166                    Some(value) => Cell::Value(value),
167                    None => Cell::Missing,
168                },
169            },
170            Some(Slot::Stored(at)) => match row.values.get(*at) {
171                Some(value) => Cell::Value(value),
172                None => Cell::Missing,
173            },
174        }
175    }
176}
177
178/// What one column turned out to hold.
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct ColumnSurvey {
181    pub name: String,
182    pub declared_type: Option<String>,
183    pub affinity: Affinity,
184    /// Rows per logical storage class, affinity already applied.
185    pub nulls: u64,
186    pub integers: u64,
187    pub reals: u64,
188    pub texts: u64,
189    pub blobs: u64,
190    /// Rows whose record ended before this column, which take its default.
191    pub missing: u64,
192    /// The largest integer seen, by magnitude. A column that mixes integers
193    /// and reals can only become a float if every integer in it survives
194    /// the trip, and past 2^53 they stop doing that.
195    pub largest_integer: u64,
196    /// Bytes in the longest text or blob seen. A key or an index has a
197    /// length limit, so a caller deciding what to make of this column needs
198    /// to know its worst case before it starts writing rather than after.
199    pub longest_value: usize,
200    pub is_rowid_alias: bool,
201    pub is_virtual: bool,
202}
203
204impl ColumnSurvey {
205    /// The classes actually present, ignoring nulls, which are a question
206    /// about nullability rather than about type.
207    pub fn classes(&self) -> Vec<StorageClass> {
208        let mut present = Vec::new();
209        for (count, class) in [
210            (self.integers, StorageClass::Integer),
211            (self.reals, StorageClass::Real),
212            (self.texts, StorageClass::Text),
213            (self.blobs, StorageClass::Blob),
214        ] {
215            if count > 0 {
216                present.push(class);
217            }
218        }
219        present
220    }
221
222    pub fn has_nulls(&self) -> bool {
223        self.nulls > 0 || self.missing > 0
224    }
225}
226
227/// What a whole table turned out to hold.
228#[derive(Debug, Clone, PartialEq)]
229pub struct TableSurvey {
230    pub name: String,
231    pub rows: u64,
232    pub columns: Vec<ColumnSurvey>,
233    pub primary_key: Vec<String>,
234    pub without_rowid: bool,
235    /// The largest rowid in the table, which matters when the rowid has to
236    /// become a key column of its own.
237    pub largest_rowid: i64,
238}
239
240impl<S: Source> Reader<S> {
241    /// Read every row of a table and report what each column holds.
242    ///
243    /// This is the first of the import's two passes: it writes nothing and
244    /// its whole purpose is to let the second pass know the shape of what
245    /// it is about to write, and to let a caller see every problem at once
246    /// rather than the first one after ten minutes of work.
247    pub fn survey_table(&self, object: &SchemaObject) -> Result<TableSurvey> {
248        let def = object.table_def()?;
249        let layout = RowLayout::new(&def);
250
251        let mut columns: Vec<ColumnSurvey> = def
252            .columns
253            .iter()
254            .map(|column| ColumnSurvey {
255                name: column.name.clone(),
256                declared_type: column.declared_type.clone(),
257                affinity: Affinity::of(column.declared_type.as_deref()),
258                nulls: 0,
259                integers: 0,
260                reals: 0,
261                texts: 0,
262                blobs: 0,
263                missing: 0,
264                largest_integer: 0,
265                longest_value: 0,
266                is_rowid_alias: def
267                    .rowid_alias()
268                    .is_some_and(|c| c.name.eq_ignore_ascii_case(&column.name)),
269                is_virtual: column.generated.is_virtual(),
270            })
271            .collect();
272
273        let mut rows = 0u64;
274        let mut largest_rowid = 0i64;
275
276        let root = object.root_page.ok_or_else(|| {
277            crate::error::SqliteError::malformed(
278                1,
279                format!("the table {} has no root page", object.name),
280            )
281        })?;
282
283        // rows() picks the walk from the root page, so a without rowid
284        // table is surveyed the same way as any other
285        for row in self.rows(root)? {
286            let row = row?;
287            rows += 1;
288            largest_rowid = largest_rowid.max(row.rowid.unwrap_or(0));
289
290            for (index, survey) in columns.iter_mut().enumerate() {
291                match layout.cell(&row, index) {
292                    Cell::Virtual => {}
293                    Cell::Missing => survey.missing += 1,
294                    Cell::Rowid(rowid) => {
295                        survey.integers += 1;
296                        survey.largest_integer = survey.largest_integer.max(rowid.unsigned_abs());
297                    }
298                    Cell::Value(value) => {
299                        if let SqliteValue::Integer(n) = value {
300                            survey.largest_integer = survey.largest_integer.max(n.unsigned_abs());
301                        }
302                        survey.longest_value = survey.longest_value.max(match value {
303                            SqliteValue::Text(t) => t.len(),
304                            SqliteValue::Blob(b) => b.len(),
305                            _ => 0,
306                        });
307                        match survey.affinity.logical_class(value) {
308                            StorageClass::Null => survey.nulls += 1,
309                            StorageClass::Integer => survey.integers += 1,
310                            StorageClass::Real => survey.reals += 1,
311                            StorageClass::Text => survey.texts += 1,
312                            StorageClass::Blob => survey.blobs += 1,
313                        }
314                    }
315                }
316            }
317        }
318
319        Ok(TableSurvey {
320            name: def.name.clone(),
321            rows,
322            columns,
323            primary_key: def.primary_key.iter().map(|k| k.name.clone()).collect(),
324            without_rowid: def.without_rowid,
325            largest_rowid,
326        })
327    }
328}