Skip to main content

inillucent_sql/
declare.rs

1//! Reading a virtual table's declaration and a pragma's argument.
2//!
3//! Invariant: nothing here touches a database. These are four pure functions
4//! over types this crate already owns, a `Declaration` and a `PragmaArgument`,
5//! and they are here rather than in a connection crate because **both** engines
6//! need them and neither should have to depend on the other to get them.
7//!
8//! They lived in `inillucent-session`, which is the old engine's connection, and
9//! the new engine's statement path imported them from there. That was the last
10//! thing tying the new engine to the old one that was not itself an engine: two
11//! helpers that parse text. Moving them down removes the edge without changing
12//! a caller, because `inillucent-session` re-exports both under their old paths.
13
14use crate::bind::BoundExpr;
15use crate::catalog_view::ColumnInfo;
16use crate::directive::PragmaArgument;
17use crate::vtab::Declaration;
18
19/// Returns the columns a module's declaration provides.
20///
21/// A declared column carries a name, a declared type, an affinity, a collation
22/// and whether it is hidden. Everything else a `ColumnInfo` can say - NOT NULL,
23/// a default, a primary-key position, a generated expression - is something a
24/// `CREATE TABLE` says and a module's declaration does not, so it is left at
25/// the value that means "unsaid" rather than guessed at.
26///
27/// @param declaration - what the module answered when it was connected
28pub fn declared_columns(declaration: &Declaration) -> Vec<ColumnInfo> {
29    declaration
30        .columns
31        .iter()
32        .map(|column| ColumnInfo {
33            folded: column.name.to_ascii_lowercase(),
34            name: column.name.clone(),
35            declared_type: column.declared_type.clone(),
36            affinity: column.affinity,
37            collation: column.collation.clone(),
38            not_null: false,
39            not_null_conflict: None,
40            primary_key_conflict: None,
41            default_sql: None,
42            primary_key_position: None,
43            hidden: column.hidden,
44            generated: false,
45            stored: true,
46            generated_sql: None,
47        })
48        .collect()
49}
50
51/// Reads a pragma argument as text.
52///
53/// @param argument - the argument as the parser produced it
54pub fn argument_text(argument: &PragmaArgument) -> String {
55    match argument {
56        PragmaArgument::Name(name) => String::from_utf8_lossy(name).into_owned(),
57        PragmaArgument::Value(expr) => expression_text(expr),
58    }
59}
60
61/// Returns the text a bound pragma argument spells.
62///
63/// `PRAGMA cache_size = -4000` is a unary minus over a literal rather than a
64/// negative literal, because that is what the grammar has. Reading only the
65/// literal made every negative setting read as zero.
66///
67/// @param expr - the argument's expression
68fn expression_text(expr: &BoundExpr) -> String {
69    match expr {
70        BoundExpr::Text(text) => String::from_utf8_lossy(text).into_owned(),
71        BoundExpr::Integer(value) => value.to_string(),
72        BoundExpr::Real(value) => value.to_string(),
73        BoundExpr::Unary { op, operand } => match op {
74            crate::ast::UnaryOp::Negate => format!("-{}", expression_text(operand)),
75            crate::ast::UnaryOp::Identity => expression_text(operand),
76            _ => String::new(),
77        },
78        _ => String::new(),
79    }
80}
81
82/// Reads a pragma argument as the boolean SQLite accepts.
83///
84/// SQLite reads `on`, `yes` and `true` as one, a number that starts with a
85/// digit as itself, and everything else as zero, which is why `PRAGMA
86/// foreign_keys = maybe` and `PRAGMA foreign_keys = -1` turn them off. See
87/// [`sqlite_boolean`].
88///
89/// @param argument - the argument as the parser produced it
90pub fn argument_boolean(argument: &PragmaArgument) -> bool {
91    sqlite_boolean(&argument_text(argument), false)
92}
93
94/// Reads a pragma argument as an integer.
95///
96/// @param argument - the argument as the parser produced it
97pub fn argument_integer(argument: &PragmaArgument) -> i64 {
98    argument_text(argument).trim().parse().unwrap_or(0)
99}
100
101/// Reads a 32 bit integer the way SQLite's `sqlite3GetInt32` does.
102///
103/// An optional sign and decimal digits, or `0x` and up to eight hex digits.
104/// Anything after the digits is ignored, so `1.5` reads as 1. Text that is not
105/// a number, or a number that does not fit in 32 bits, is `None`. SQLite reads
106/// 4294967295 as nothing rather than as -1, which is why
107/// `PRAGMA user_version = 4294967295` reads back 0.
108///
109/// @param text - the argument's text
110pub fn sqlite_int32(text: &str) -> Option<i32> {
111    let bytes = text.as_bytes();
112    let (negative, digits) = match bytes.first() {
113        Some(b'-') => (true, bytes.get(1..).unwrap_or_default()),
114        Some(b'+') => (false, bytes.get(1..).unwrap_or_default()),
115        _ => (false, bytes),
116    };
117    if digits.first() == Some(&b'0')
118        && matches!(digits.get(1), Some(b'x' | b'X'))
119        && digits.get(2).is_some_and(u8::is_ascii_hexdigit)
120        && !negative
121    {
122        return hex_int32(digits.get(2..).unwrap_or_default());
123    }
124    let significant: Vec<u8> = digits
125        .iter()
126        .copied()
127        .skip_while(|byte| *byte == b'0')
128        .take_while(u8::is_ascii_digit)
129        .collect();
130    if !digits.first().is_some_and(u8::is_ascii_digit) || significant.len() > 10 {
131        return None;
132    }
133    let value = significant.iter().fold(0i64, |held, digit| {
134        held.saturating_mul(10)
135            .saturating_add(i64::from(digit.saturating_sub(b'0')))
136    });
137    let signed = if negative { -value } else { value };
138    i32::try_from(signed).ok()
139}
140
141/// Reads the eight hex digits `sqlite_int32` allows after `0x`.
142///
143/// @param digits - the text after `0x`
144fn hex_int32(digits: &[u8]) -> Option<i32> {
145    let trimmed: Vec<u8> = digits
146        .iter()
147        .copied()
148        .skip_while(|byte| *byte == b'0')
149        .collect();
150    let used: Vec<u8> = trimmed
151        .iter()
152        .copied()
153        .take_while(u8::is_ascii_hexdigit)
154        .collect();
155    if used.len() > 8 || trimmed.get(used.len()).is_some_and(u8::is_ascii_hexdigit) {
156        return None;
157    }
158    let value = used.iter().fold(0u32, |held, digit| {
159        let nibble = char::from(*digit).to_digit(16).unwrap_or(0);
160        held.wrapping_mul(16).wrapping_add(nibble)
161    });
162    if value & 0x8000_0000 != 0 {
163        return None;
164    }
165    i32::try_from(value).ok()
166}
167
168/// Reads an integer the way SQLite's `sqlite3Atoi` does: 0 when there is none.
169///
170/// @param text - the argument's text
171pub fn sqlite_atoi(text: &str) -> i32 {
172    sqlite_int32(text).unwrap_or(0)
173}
174
175/// Reads a 64 bit integer the way SQLite's `sqlite3DecOrHexToI64` does.
176///
177/// Returns the number the text starts with, 0 when it starts with none, and
178/// whether the whole text was one number. A pragma such as `mmap_size` takes the
179/// number whether or not anything followed it; `threads` ignores an argument
180/// for which the second part is false. A number too large for 64 bits is
181/// limited to the largest or smallest.
182///
183/// @param text - the argument's text
184pub fn sqlite_integer(text: &str) -> (i64, bool) {
185    let bytes = text.as_bytes();
186    if bytes.first() == Some(&b'0') && matches!(bytes.get(1), Some(b'x' | b'X')) {
187        let digits: Vec<u8> = bytes
188            .get(2..)
189            .unwrap_or_default()
190            .iter()
191            .copied()
192            .skip_while(|byte| *byte == b'0')
193            .collect();
194        let used: Vec<u8> = digits
195            .iter()
196            .copied()
197            .take_while(u8::is_ascii_hexdigit)
198            .collect();
199        let value = used.iter().fold(0u64, |held, digit| {
200            let nibble = char::from(*digit).to_digit(16).unwrap_or(0);
201            held.wrapping_mul(16).wrapping_add(u64::from(nibble))
202        });
203        return (value as i64, used.len() == digits.len() && used.len() <= 16);
204    }
205    let rest = text.trim_start_matches([' ', '\t', '\n', '\r', '\u{b}', '\u{c}']);
206    let (negative, unsigned) = match rest.as_bytes().first() {
207        Some(b'-') => (true, rest.get(1..).unwrap_or_default()),
208        Some(b'+') => (false, rest.get(1..).unwrap_or_default()),
209        _ => (false, rest),
210    };
211    let digits: Vec<u8> = unsigned.bytes().take_while(u8::is_ascii_digit).collect();
212    let magnitude = digits.iter().fold(0i128, |held, digit| {
213        held.saturating_mul(10)
214            .saturating_add(i128::from(digit.saturating_sub(b'0')))
215            .min(i128::from(i64::MAX) + 1)
216    });
217    let after = unsigned.get(digits.len()..).unwrap_or_default();
218    let whole = !digits.is_empty()
219        && after
220            .trim_start_matches([' ', '\t', '\n', '\r', '\u{b}', '\u{c}'])
221            .is_empty();
222    let signed = if negative { -magnitude } else { magnitude };
223    let clamped = signed.clamp(i128::from(i64::MIN), i128::from(i64::MAX));
224    (
225        i64::try_from(clamped).unwrap_or(0),
226        whole && clamped == signed,
227    )
228}
229
230/// Reads a setting the way SQLite's `getSafetyLevel` does.
231///
232/// A number is read as a number, so `PRAGMA synchronous = 5` reads back 5.
233/// `on`, `yes` and `true` are 1; `off`, `no` and `false` are 0; `full` is 2 and
234/// `extra` is 3 unless `omit_full` is set, which a plain boolean pragma sets.
235/// Any other word is `default`, and so is a negative number, because a sign is
236/// not a digit.
237///
238/// @param text - the argument's text
239/// @param omit_full - whether `full` and `extra` are refused as words
240/// @param default - what any other text means
241pub fn sqlite_safety_level(text: &str, omit_full: bool, default: u8) -> u8 {
242    if text.as_bytes().first().is_some_and(u8::is_ascii_digit) {
243        return u8::try_from(sqlite_atoi(text) & 0xff).unwrap_or(0);
244    }
245    let words: [(&str, u8); 8] = [
246        ("on", 1),
247        ("no", 0),
248        ("off", 0),
249        ("false", 0),
250        ("yes", 1),
251        ("true", 1),
252        ("extra", 3),
253        ("full", 2),
254    ];
255    for (word, value) in words {
256        if text.eq_ignore_ascii_case(word) && (!omit_full || value <= 1) {
257            return value;
258        }
259    }
260    default
261}
262
263/// Reads a boolean setting the way SQLite's `sqlite3GetBoolean` does.
264///
265/// @param text - the argument's text
266/// @param default - what text that is not a boolean means
267pub fn sqlite_boolean(text: &str, default: bool) -> bool {
268    sqlite_safety_level(text, true, u8::from(default)) != 0
269}