inillucent_sqlite_reader/
lib.rs1#![forbid(unsafe_code)]
42#![deny(missing_docs)]
43#![deny(clippy::indexing_slicing)]
44#![deny(clippy::unwrap_used)]
45#![deny(clippy::expect_used)]
46#![deny(clippy::panic)]
47#![cfg_attr(
48 test,
49 allow(
50 clippy::expect_used,
51 clippy::indexing_slicing,
52 clippy::panic,
53 clippy::unwrap_used
54 )
55)]
56
57use std::path::PathBuf;
58use std::sync::Arc;
59
60use inillucent_base::error::{corrupt, misuse};
61use inillucent_base::ids::PageId;
62use inillucent_base::limits::Limits;
63use inillucent_base::DbResult;
64use inillucent_storage::cursor::BTreeCursor;
65use inillucent_storage::pager::Pager;
66use inillucent_transaction::recovery::{open_database, DatabaseOptions};
67use inillucent_tree::datum::{Datum, OwnedDatum};
68use inillucent_value::record::{FieldSpan, KeyInfo, RecordRef};
69use inillucent_value::Value;
70use inillucent_vfs::path::DbPath;
71use inillucent_vfs::{OsVfs, Vfs};
72
73#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct SchemaObject {
76 pub kind: String,
78 pub name: String,
80 pub table: String,
82 pub root: u32,
84 pub sql: String,
86}
87
88impl SchemaObject {
89 pub fn column_names(&self) -> DbResult<Vec<String>> {
104 let open = self
105 .sql
106 .find('(')
107 .ok_or_else(|| corrupt(format!("{} has no column list", self.name)))?;
108 let close = self
109 .sql
110 .rfind(')')
111 .ok_or_else(|| corrupt(format!("{} has no column list", self.name)))?;
112 if close <= open {
113 return Err(corrupt(format!(
114 "{}'s column list is inside out",
115 self.name
116 )));
117 }
118 let body = self.sql.get(open.saturating_add(1)..close).unwrap_or("");
119 let mut names = Vec::new();
120 let mut depth = 0i32;
121 let mut part = String::new();
122 for character in body.chars() {
123 match character {
124 '(' => {
125 depth = depth.saturating_add(1);
126 part.push(character);
127 }
128 ')' => {
129 depth = depth.saturating_sub(1);
130 part.push(character);
131 }
132 ',' if depth == 0 => {
133 push_column_name(&part, &mut names);
134 part.clear();
135 }
136 _ => part.push(character),
137 }
138 }
139 push_column_name(&part, &mut names);
140 if names.is_empty() {
141 return Err(corrupt(format!("{} declares no columns", self.name)));
142 }
143 Ok(names)
144 }
145}
146
147const TABLE_CONSTRAINTS: [&str; 6] = [
149 "primary",
150 "unique",
151 "check",
152 "foreign",
153 "constraint",
154 "exclude",
155];
156
157fn push_column_name(part: &str, names: &mut Vec<String>) {
162 let trimmed = part.trim();
163 let Some(first) = trimmed.split_whitespace().next() else {
164 return;
165 };
166 if TABLE_CONSTRAINTS
167 .iter()
168 .any(|keyword| first.eq_ignore_ascii_case(keyword))
169 {
170 return;
171 }
172 let cleaned = first.trim_matches(|c| c == '"' || c == '`' || c == '[' || c == ']');
173 if cleaned.is_empty() {
174 return;
175 }
176 names.push(cleaned.to_string());
177}
178
179pub struct SqliteFile {
181 pager: Pager,
182 limits: Limits,
183}
184
185impl SqliteFile {
186 pub fn open(path: PathBuf) -> DbResult<SqliteFile> {
190 let vfs: Arc<dyn Vfs> = Arc::new(OsVfs::new());
191 let options = DatabaseOptions {
192 writable: false,
193 ..DatabaseOptions::default()
194 };
195 let mut pager = open_database(vfs, &DbPath::new(path), options)?;
196 pager.begin_read()?;
197 Ok(SqliteFile {
198 pager,
199 limits: Limits::default(),
200 })
201 }
202
203 pub fn page_size(&self) -> u32 {
205 self.pager.page_size().bytes()
206 }
207
208 pub fn page_count(&self) -> u32 {
210 self.pager.page_count()
211 }
212
213 pub fn catalog(
224 &mut self,
225 name: &[u8],
226 ) -> DbResult<inillucent_catalog::snapshot::DatabaseCatalog> {
227 inillucent_catalog::load::load_database_catalog(&mut self.pager, name, 0)
228 }
229
230 pub fn schema(&mut self) -> DbResult<Vec<SchemaObject>> {
242 let root = PageId::from_persisted(1)?;
243 let mut cursor = BTreeCursor::table(root);
244 let mut payload: Vec<u8> = Vec::with_capacity(512);
245 let mut fields: Vec<FieldSpan> = Vec::with_capacity(8);
246 let mut out = Vec::new();
247 let encoding = self.pager.text_encoding();
248 let mut more = cursor.first(&mut self.pager)?;
249 while more {
250 cursor.payload_into(&mut self.pager, &self.limits, &mut payload)?;
251 let header_len = RecordRef::parse_into(&payload, &self.limits, &mut fields)?;
252 let record = RecordRef::with_fields(&payload, &fields, header_len, encoding);
253 out.push(SchemaObject {
254 kind: text_at(&record, 0)?,
255 name: text_at(&record, 1)?,
256 table: text_at(&record, 2)?,
257 root: u32::try_from(integer_at(&record, 3)?)
258 .map_err(|_| corrupt("a root page that is not a page number"))?,
259 sql: text_at(&record, 4)?,
260 });
261 more = cursor.next(&mut self.pager)?;
262 }
263 Ok(out)
264 }
265
266 pub fn object(&mut self, kind: &str, name: &str) -> DbResult<SchemaObject> {
271 self.schema()?
272 .into_iter()
273 .find(|object| object.kind == kind && object.name == name)
274 .ok_or_else(|| misuse(format!("no {kind} named {name} in this file")))
275 }
276
277 pub fn read_table(&mut self, root: u32, columns: usize) -> DbResult<Vec<Vec<OwnedDatum>>> {
287 let root = PageId::from_persisted(root)?;
288 let mut cursor = BTreeCursor::table(root);
289 let mut payload: Vec<u8> = Vec::with_capacity(512);
290 let mut fields: Vec<FieldSpan> = Vec::with_capacity(16);
291 let mut out = Vec::new();
292 let encoding = self.pager.text_encoding();
293 let mut more = cursor.first(&mut self.pager)?;
294 while more {
295 let rowid = cursor.rowid()?;
296 cursor.payload_into(&mut self.pager, &self.limits, &mut payload)?;
297 let header_len = RecordRef::parse_into(&payload, &self.limits, &mut fields)?;
298 let record = RecordRef::with_fields(&payload, &fields, header_len, encoding);
299 let mut row = Vec::with_capacity(columns.saturating_add(1));
300 row.push(OwnedDatum::Int(rowid));
301 for index in 0..columns {
302 row.push(owned_from_record(&record, index)?);
303 }
304 out.push(row);
305 more = cursor.next(&mut self.pager)?;
306 }
307 Ok(out)
308 }
309
310 pub fn read_index(&mut self, root: u32, columns: usize) -> DbResult<Vec<Vec<OwnedDatum>>> {
319 let root = PageId::from_persisted(root)?;
320 let mut cursor = BTreeCursor::index(root, KeyInfo::binary(columns));
323 let mut payload: Vec<u8> = Vec::with_capacity(512);
324 let mut fields: Vec<FieldSpan> = Vec::with_capacity(16);
325 let mut out = Vec::new();
326 let encoding = self.pager.text_encoding();
327 let mut more = cursor.first(&mut self.pager)?;
328 while more {
329 cursor.payload_into(&mut self.pager, &self.limits, &mut payload)?;
330 let header_len = RecordRef::parse_into(&payload, &self.limits, &mut fields)?;
331 let record = RecordRef::with_fields(&payload, &fields, header_len, encoding);
332 let mut row = Vec::with_capacity(columns);
333 for index in 0..columns {
334 row.push(owned_from_record(&record, index)?);
335 }
336 out.push(row);
337 more = cursor.next(&mut self.pager)?;
338 }
339 Ok(out)
340 }
341}
342
343fn owned_from_record(record: &RecordRef<'_>, index: usize) -> DbResult<OwnedDatum> {
348 Ok(match record.value(index)? {
349 Value::Null => OwnedDatum::Null,
350 Value::Integer(number) => OwnedDatum::Int(number),
351 Value::Real(number) => OwnedDatum::Real(number),
352 Value::Text(text) => OwnedDatum::Text(text.utf8_bytes().into_owned()),
353 Value::Blob(blob) => OwnedDatum::Blob(blob.raw().to_vec()),
354 })
355}
356
357fn text_at(record: &RecordRef<'_>, index: usize) -> DbResult<String> {
362 match record.value(index)? {
363 Value::Text(text) => Ok(String::from_utf8_lossy(&text.utf8_bytes()).into_owned()),
364 Value::Null => Ok(String::new()),
365 other => Err(corrupt(format!(
366 "expected text in schema field {index}, found {:?}",
367 other.storage_class()
368 ))),
369 }
370}
371
372fn integer_at(record: &RecordRef<'_>, index: usize) -> DbResult<i64> {
377 match record.value(index)? {
378 Value::Integer(number) => Ok(number),
379 Value::Null => Ok(0),
380 other => Err(corrupt(format!(
381 "expected an integer in schema field {index}, found {:?}",
382 other.storage_class()
383 ))),
384 }
385}
386
387pub fn borrow(row: &[OwnedDatum]) -> Vec<Datum<'_>> {
391 row.iter().map(OwnedDatum::borrow).collect()
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 fn object(sql: &str) -> SchemaObject {
399 SchemaObject {
400 kind: "table".to_string(),
401 name: "t".to_string(),
402 table: "t".to_string(),
403 root: 2,
404 sql: sql.to_string(),
405 }
406 }
407
408 #[test]
410 fn the_fixture_schema_parses() {
411 let names = object(
412 "CREATE TABLE main_table(id INTEGER PRIMARY KEY, key INTEGER NOT NULL, \
413 category INTEGER NOT NULL, label TEXT NOT NULL, payload BLOB)",
414 )
415 .column_names()
416 .unwrap();
417 assert_eq!(names, ["id", "key", "category", "label", "payload"]);
418 }
419
420 #[test]
422 fn table_constraints_are_not_columns() {
423 let names = object(
424 "CREATE TABLE t(a INTEGER, b TEXT, PRIMARY KEY (a, b), \
425 FOREIGN KEY (b) REFERENCES u(x), CHECK (a > 0))",
426 )
427 .column_names()
428 .unwrap();
429 assert_eq!(names, ["a", "b"]);
430 }
431
432 #[test]
434 fn parenthesised_types_stay_in_one_column() {
435 let names = object("CREATE TABLE t(a VARCHAR(20), b DECIMAL(10, 2), c INT)")
436 .column_names()
437 .unwrap();
438 assert_eq!(names, ["a", "b", "c"]);
439 }
440
441 #[test]
443 fn quoted_identifiers_are_unquoted() {
444 let names = object("CREATE TABLE t(\"a b\" INTEGER, `c` TEXT, [d] BLOB)")
445 .column_names()
446 .unwrap();
447 assert_eq!(names, ["a", "c", "d"]);
448 }
449
450 #[test]
452 fn a_missing_column_list_is_refused() {
453 assert!(object("CREATE TABLE t").column_names().is_err());
454 assert!(object("CREATE TABLE t)(").column_names().is_err());
455 assert!(object("CREATE TABLE t()").column_names().is_err());
456 }
457}