Skip to main content

document_svg/document/
sqlite.rs

1//! Bounded read-only SQLite table preview.
2//!
3//! This adapter is intentionally a data preview rather than a SQL client. It
4//! opens the database read-only, enumerates ordinary user tables, and renders
5//! a bounded sample of each table as SVG tables. Triggers, views, virtual
6//! tables, blobs, extensions, and arbitrary SQL are never executed.
7
8use std::path::Path;
9use std::time::{Duration, Instant};
10
11use rusqlite::limits::Limit;
12use rusqlite::{Connection, OpenFlags, types::ValueRef};
13
14use crate::convert::{ConvertOptions, PageConsumer};
15use crate::document::html::{HtmlBlock, render_blocks_to_pages_with_warnings};
16use crate::error::{Error, Result};
17use crate::table::{TableAlign, TableData};
18
19const MAX_SQLITE_INPUT_BYTES: u64 = 256 * 1024 * 1024;
20const MAX_SQLITE_TABLES: usize = 100;
21const MAX_SQLITE_ROWS_PER_TABLE: usize = 2_000;
22const MAX_SQLITE_COLUMNS: usize = 128;
23const MAX_SQLITE_CELL_CHARS: usize = 512;
24const MAX_SQLITE_TEXT_BYTES: usize = 64 * 1024 * 1024;
25const MAX_SQLITE_QUERY_SECONDS: u64 = 5;
26
27pub(crate) fn looks_like_prefix(bytes: &[u8]) -> bool {
28    bytes.starts_with(b"SQLite format 3\0")
29}
30
31pub(crate) fn convert(
32    path: &Path,
33    options: &ConvertOptions,
34    sink: &mut dyn PageConsumer,
35) -> Result<Vec<String>> {
36    let metadata = std::fs::metadata(path)?;
37    let limit = options.max_input_bytes.min(MAX_SQLITE_INPUT_BYTES);
38    if metadata.len() > limit {
39        return Err(Error::LimitExceeded(format!(
40            "SQLite input exceeds maximum bytes ({limit})"
41        )));
42    }
43    check_sidecar(path, "-wal", limit)?;
44    check_sidecar(path, "-shm", limit.min(16 * 1024 * 1024))?;
45    let flags = OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX;
46    let connection = Connection::open_with_flags(path, flags).map_err(|error| {
47        Error::InvalidInput(format!("cannot open SQLite database read-only: {error}"))
48    })?;
49    connection
50        .busy_timeout(Duration::from_secs(1))
51        .map_err(sql_error)?;
52    let started = Instant::now();
53    connection
54        .progress_handler(
55            1000,
56            Some(move || started.elapsed() >= Duration::from_secs(MAX_SQLITE_QUERY_SECONDS)),
57        )
58        .map_err(sql_error)?;
59    connection
60        .set_limit(Limit::SQLITE_LIMIT_LENGTH, 8 * 1024 * 1024)
61        .map_err(sql_error)?;
62    connection
63        .set_limit(Limit::SQLITE_LIMIT_SQL_LENGTH, 1024 * 1024)
64        .map_err(sql_error)?;
65    connection
66        .set_limit(Limit::SQLITE_LIMIT_COLUMN, MAX_SQLITE_COLUMNS as i32)
67        .map_err(sql_error)?;
68    let mut warnings = Vec::new();
69    let mut blocks = Vec::new();
70    let tables = list_tables(&connection)?;
71    if tables.is_empty() {
72        return Err(Error::InvalidInput(
73            "SQLite database contains no ordinary user tables".into(),
74        ));
75    }
76    if tables.len() > MAX_SQLITE_TABLES {
77        warnings.push(format!(
78            "SQLite contains more than {MAX_SQLITE_TABLES} user tables; remaining tables were omitted"
79        ));
80    }
81    let mut text_bytes = 0usize;
82    for table_name in tables.into_iter().take(MAX_SQLITE_TABLES) {
83        let (table, table_warnings) = read_table(&connection, &table_name, &mut text_bytes)?;
84        warnings.extend(table_warnings);
85        blocks.push(HtmlBlock::Heading {
86            level: 2,
87            text: table_name,
88        });
89        blocks.push(HtmlBlock::Table(table));
90    }
91    if text_bytes > MAX_SQLITE_TEXT_BYTES {
92        return Err(Error::LimitExceeded(format!(
93            "SQLite rendered text exceeds {MAX_SQLITE_TEXT_BYTES} bytes"
94        )));
95    }
96    render_blocks_to_pages_with_warnings(&blocks, sink, options, &warnings)?;
97    Ok(dedup_warnings(warnings))
98}
99
100fn list_tables(connection: &Connection) -> Result<Vec<String>> {
101    let mut statement = connection
102        .prepare("SELECT name FROM sqlite_schema WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name LIMIT ?1")
103        .map_err(sql_error)?;
104    let names = statement
105        .query_map([MAX_SQLITE_TABLES.saturating_add(1) as i64], |row| {
106            row.get::<_, String>(0)
107        })
108        .map_err(sql_error)?
109        .collect::<std::result::Result<Vec<_>, _>>()
110        .map_err(sql_error)?;
111    Ok(names)
112}
113
114fn read_table(
115    connection: &Connection,
116    table_name: &str,
117    text_bytes: &mut usize,
118) -> Result<(TableData, Vec<String>)> {
119    let quoted = quote_identifier(table_name);
120    let sql = format!(
121        "SELECT * FROM {quoted} LIMIT {}",
122        MAX_SQLITE_ROWS_PER_TABLE + 1
123    );
124    let mut statement = connection.prepare(&sql).map_err(sql_error)?;
125    let column_count = statement.column_count().min(MAX_SQLITE_COLUMNS);
126    let headers = statement
127        .column_names()
128        .into_iter()
129        .take(column_count)
130        .map(str::to_owned)
131        .collect::<Vec<_>>();
132    let mut rows = Vec::new();
133    let mut warnings = Vec::new();
134    let mut query = statement.query([]).map_err(sql_error)?;
135    while let Some(row) = query.next().map_err(sql_error)? {
136        if rows.len() >= MAX_SQLITE_ROWS_PER_TABLE {
137            warnings.push(format!(
138                "SQLite table '{table_name}' exceeded {MAX_SQLITE_ROWS_PER_TABLE} sampled rows; remaining rows were omitted"
139            ));
140            break;
141        }
142        let mut values = Vec::with_capacity(column_count);
143        for index in 0..column_count {
144            let value = match row.get_ref(index).map_err(sql_error)? {
145                ValueRef::Null => String::new(),
146                ValueRef::Integer(value) => value.to_string(),
147                ValueRef::Real(value) => value.to_string(),
148                ValueRef::Text(value) => String::from_utf8_lossy(value).into_owned(),
149                ValueRef::Blob(value) => format!("<BLOB {} bytes>", value.len()),
150            };
151            let value = truncate_cell(&value);
152            *text_bytes = text_bytes.saturating_add(value.len());
153            values.push(value);
154        }
155        rows.push(values);
156    }
157    Ok((
158        TableData {
159            headers,
160            rows,
161            alignments: vec![TableAlign::Left; column_count],
162            raw_source: String::new(),
163        },
164        warnings,
165    ))
166}
167
168fn quote_identifier(value: &str) -> String {
169    format!("\"{}\"", value.replace('"', "\"\""))
170}
171
172fn truncate_cell(value: &str) -> String {
173    value.chars().take(MAX_SQLITE_CELL_CHARS).collect()
174}
175
176fn sql_error(error: rusqlite::Error) -> Error {
177    Error::InvalidInput(format!("SQLite preview query failed: {error}"))
178}
179
180fn check_sidecar(path: &Path, suffix: &str, limit: u64) -> Result<()> {
181    let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
182        return Ok(());
183    };
184    let sidecar = path.with_file_name(format!("{file_name}{suffix}"));
185    let Ok(metadata) = std::fs::metadata(&sidecar) else {
186        return Ok(());
187    };
188    if !metadata.is_file() {
189        return Err(Error::InvalidInput(format!(
190            "SQLite sidecar '{suffix}' is not a regular file"
191        )));
192    }
193    if metadata.len() > limit {
194        return Err(Error::LimitExceeded(format!(
195            "SQLite sidecar '{suffix}' exceeds maximum bytes ({limit})"
196        )));
197    }
198    Ok(())
199}
200
201fn dedup_warnings(mut warnings: Vec<String>) -> Vec<String> {
202    warnings.sort();
203    warnings.dedup();
204    warnings
205}