feat: add query editor above result rows
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user