feat: add read-only SQLite inspection

This commit is contained in:
Valdemar Lorenzen
2026-10-19 12:06:25 +02:00
parent c73fa18bf3
commit bbc21f96cf
6 changed files with 460 additions and 43 deletions
+61 -16
View File
@@ -1,4 +1,4 @@
use std::time::Duration;
use std::{path::Path, time::Duration};
use native_tls::TlsConnector;
use postgres::{Client, Config};
@@ -6,14 +6,25 @@ use postgres_native_tls::MakeTlsConnector;
use crate::Result;
mod sqlite;
pub const PAGE_SIZE: usize = 100;
pub struct Database {
client: Client,
backend: Backend,
}
#[allow(
clippy::large_enum_variant,
reason = "ponytail: one connection per app; box only if storing backends in bulk"
)]
enum Backend {
Postgres(Client),
Sqlite(rusqlite::Connection),
}
pub struct Relation {
pub oid: u32,
pub id: i64,
pub schema: String,
pub name: String,
pub label: String,
@@ -47,11 +58,30 @@ impl Database {
client.batch_execute(
"SET default_transaction_read_only = on; SET statement_timeout = '5s';",
)?;
Ok(Self { client })
Ok(Self {
backend: Backend::Postgres(client),
})
}
pub fn open_sqlite(path: &Path) -> Result<Self> {
Ok(Self {
backend: Backend::Sqlite(sqlite::open(path)?),
})
}
pub fn name(&self) -> &'static str {
match self.backend {
Backend::Postgres(_) => "PostgreSQL",
Backend::Sqlite(_) => "SQLite",
}
}
pub fn relations(&mut self) -> Result<Vec<Relation>> {
let rows = self.client.query(
let client = match &mut self.backend {
Backend::Postgres(client) => client,
Backend::Sqlite(connection) => return sqlite::relations(connection),
};
let rows = client.query(
"SELECT c.oid, n.nspname, c.relname, c.relkind::text
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
@@ -70,7 +100,7 @@ impl Database {
let name: String = row.get(2);
let kind: &str = row.get(3);
Relation {
oid: row.get(0),
id: i64::from(row.get::<_, u32>(0)),
label: display_text(&format!("{schema}.{name}")),
schema,
name,
@@ -89,7 +119,12 @@ impl Database {
}
pub fn columns(&mut self, relation: &Relation) -> Result<Vec<Column>> {
let rows = self.client.query(
let client = match &mut self.backend {
Backend::Postgres(client) => client,
Backend::Sqlite(connection) => return sqlite::columns(connection, relation),
};
let oid = u32::try_from(relation.id)?;
let rows = client.query(
"SELECT a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod),
NOT a.attnotnull,
EXISTS (SELECT FROM pg_catalog.pg_index i
@@ -101,7 +136,7 @@ impl Database {
ON d.adrelid = a.attrelid AND d.adnum = a.attnum
WHERE a.attrelid = $1 AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum",
&[&relation.oid],
&[&oid],
)?;
Ok(rows
.into_iter()
@@ -120,6 +155,12 @@ impl Database {
}
pub fn page(&mut self, relation: &Relation, columns: &[Column], page: usize) -> Result<Page> {
let client = match &mut self.backend {
Backend::Postgres(client) => client,
Backend::Sqlite(connection) => {
return sqlite::page(connection, relation, columns, page);
}
};
let projection = columns
.iter()
.map(|column| format!("t.{}::text", quote_identifier(&column.name)))
@@ -145,12 +186,8 @@ impl Database {
quote_identifier(&relation.schema),
quote_identifier(&relation.name),
);
let offset = i64::try_from(page)?
.checked_mul(PAGE_SIZE as i64)
.ok_or("page offset overflow")?;
let result = self
.client
.query(&sql, &[&((PAGE_SIZE + 1) as i64), &offset])?;
let offset = page_offset(page)?;
let result = client.query(&sql, &[&((PAGE_SIZE + 1) as i64), &offset])?;
let has_next = result.len() > PAGE_SIZE;
let rows = result
.into_iter()
@@ -165,6 +202,12 @@ impl Database {
}
}
fn page_offset(page: usize) -> Result<i64> {
i64::try_from(page)?
.checked_mul(PAGE_SIZE as i64)
.ok_or_else(|| "page offset overflow".into())
}
fn quote_identifier(value: &str) -> String {
format!("\"{}\"", value.replace('"', "\"\""))
}
@@ -273,8 +316,10 @@ mod tests {
let page = db.page(empty, &columns, 0)?;
assert!(page.rows.is_empty());
assert!(!page.has_next);
let error = db
.client
let Backend::Postgres(client) = &mut db.backend else {
unreachable!()
};
let error = client
.batch_execute(&format!("DELETE FROM {schema}.empty_table"))
.unwrap_err();
assert_eq!(