feat: add query editor above result rows

This commit is contained in:
Valdemar Lorenzen
2026-10-19 14:04:36 +02:00
parent b15460d5f8
commit 49a3dbddcf
5 changed files with 460 additions and 66 deletions
+104
View File
@@ -46,6 +46,11 @@ pub struct Page {
pub has_next: bool,
}
pub struct QueryResult {
pub headers: Vec<String>,
pub page: Page,
}
impl Database {
pub fn connect(connection: &str) -> Result<Self> {
Self::connect_postgres(connection.parse()?)
@@ -157,6 +162,59 @@ impl Database {
.collect())
}
pub fn query(&mut self, sql: &str) -> Result<QueryResult> {
let sql = sql.trim();
let sql = sql.strip_suffix(';').unwrap_or(sql).trim_end();
let client = match &mut self.backend {
Backend::Postgres(client) => client,
Backend::Sqlite(connection) => return sqlite::query(connection, sql),
};
let mut transaction = client.build_transaction().read_only(true).start()?;
transaction.batch_execute("SET LOCAL statement_timeout = '5s'")?;
let statement = transaction.prepare(&format!(
"SELECT * FROM (\n{sql}\n) AS query_result LIMIT 0"
))?;
let headers = statement
.columns()
.iter()
.map(|column| display_text(column.name()))
.collect::<Vec<_>>();
let aliases = (0..headers.len())
.map(|index| format!("c{index}"))
.collect::<Vec<_>>();
let projection = aliases
.iter()
.map(|alias| format!("query_result.{alias}::text"))
.collect::<Vec<_>>()
.join(", ");
let alias_list = if aliases.is_empty() {
String::new()
} else {
format!("({})", aliases.join(", "))
};
let result = transaction.query(
&format!(
"SELECT {projection} FROM (\n{sql}\n) AS query_result{alias_list} LIMIT {}",
PAGE_SIZE + 1
),
&[],
)?;
let has_next = result.len() > PAGE_SIZE;
let rows = result
.into_iter()
.take(PAGE_SIZE)
.map(|row| {
(0..headers.len())
.map(|index| row.get::<_, Option<&str>>(index).map(display_text))
.collect()
})
.collect();
Ok(QueryResult {
headers,
page: Page { rows, has_next },
})
}
pub fn page(&mut self, relation: &Relation, columns: &[Column], page: usize) -> Result<Page> {
let client = match &mut self.backend {
Backend::Postgres(client) => client,
@@ -319,9 +377,55 @@ mod tests {
let page = db.page(empty, &columns, 0)?;
assert!(page.rows.is_empty());
assert!(!page.has_next);
let query = db.query(&format!(
" \nWITH data AS (SELECT * FROM {schema}.\"odd\"\"table\")
SELECT * FROM data ORDER BY id; \n"
))?;
assert_eq!(query.headers[1], "odd\"column");
assert_eq!(query.page.rows, first.rows);
assert!(query.page.has_next);
let duplicate = db.query("SELECT 1 AS same, NULL::text AS same")?;
assert_eq!(duplicate.headers, ["same", "same"]);
assert_eq!(duplicate.page.rows, [vec![Some("1".into()), None]]);
assert!(!duplicate.page.has_next);
let empty = db.query("SELECT 1 AS empty WHERE false")?;
assert_eq!(empty.headers, ["empty"]);
assert!(empty.page.rows.is_empty());
assert!(!empty.page.has_next);
let controls = db.query("SELECT E'\\033[2J' AS \"line\nheader\" -- trailing comment")?;
assert_eq!(controls.headers, ["line\\nheader"]);
assert_eq!(controls.page.rows[0][0].as_deref(), Some("\\u{1b}[2J"));
assert!(db.query("SELECT 1; SELECT 2").is_err());
assert!(
db.query(&format!("DELETE FROM {schema}.empty_table RETURNING id"))
.is_err()
);
assert!(
db.query(&format!(
"WITH deleted AS (DELETE FROM {schema}.empty_table RETURNING id)
SELECT * FROM deleted"
))
.is_err()
);
db.query("SELECT set_config('default_transaction_read_only', 'off', false)")?;
let settings = db.query("SELECT current_setting('default_transaction_read_only')")?;
assert_eq!(settings.page.rows[0][0].as_deref(), Some("on"));
if let Backend::Postgres(client) = &mut db.backend {
client.batch_execute(
"SET default_transaction_read_only = off; SET statement_timeout = 0",
)?;
}
let settings = db.query(
"SELECT current_setting('transaction_read_only'), current_setting('statement_timeout')",
)?;
assert_eq!(
settings.page.rows[0],
[Some("on".into()), Some("5s".into())]
);
let Backend::Postgres(client) = &mut db.backend else {
unreachable!()
};
client.batch_execute("SET default_transaction_read_only = on")?;
let error = client
.batch_execute(&format!("DELETE FROM {schema}.empty_table"))
.unwrap_err();