feat: add read-only SQLite inspection
This commit is contained in:
@@ -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!(
|
||||
|
||||
Reference in New Issue
Block a user