use super::{
execute_prepared_query, execute_prepared_query_page, execute_prepared_query_row,
map_rusqlite_err, next_sqlite_token, prepare_bound_statement, skip_sqlite_empty_prefix,
transaction_control_head, CachedReadTransactionControl, PageRequest, SqlRow, SqlStatement,
StorageCapability, StorageError,
};
fn statement_is_cancellable_read(stmt: &rusqlite::Statement<'_>, sql: &str) -> bool {
stmt.readonly() && transaction_control_head(sql).is_none()
}
const READER_STRUCTURAL_PRAGMAS: [&str; 8] = [
"table_info",
"table_xinfo",
"table_list",
"index_list",
"index_info",
"index_xinfo",
"foreign_key_list",
"integrity_check",
];
const READER_SETTING_PRAGMAS: [&str; 10] = [
"database_list",
"collation_list",
"function_list",
"compile_options",
"page_count",
"freelist_count",
"user_version",
"schema_version",
"journal_mode",
"page_size",
];
fn skip_balanced_parens(rest: &[u8]) -> Option<&[u8]> {
debug_assert_eq!(rest.first(), Some(&b'('));
let mut depth: u32 = 0;
let mut idx = 0;
loop {
match *rest.get(idx)? {
b'(' => {
depth += 1;
idx += 1;
}
b')' => {
depth -= 1;
idx += 1;
if depth == 0 {
return Some(&rest[idx..]);
}
}
quote @ (b'\'' | b'"' | b'`') => {
idx += 1;
loop {
match *rest.get(idx)? {
byte if byte == quote => {
idx += 1;
if rest.get(idx) == Some("e) {
idx += 1; } else {
break;
}
}
_ => idx += 1,
}
}
}
b'[' => {
idx += 1;
while *rest.get(idx)? != b']' {
idx += 1;
}
idx += 1;
}
b'-' if rest.get(idx + 1) == Some(&b'-') => {
idx += 2;
while idx < rest.len() && rest[idx] != b'\n' {
idx += 1;
}
}
b'/' if rest.get(idx + 1) == Some(&b'*') => {
idx += 2;
while idx + 1 < rest.len() && !(rest[idx] == b'*' && rest[idx + 1] == b'/') {
idx += 1;
}
idx = (idx + 2).min(rest.len());
}
_ => idx += 1,
}
}
}
fn skip_sqlite_identifier(rest: &[u8]) -> Option<&[u8]> {
match *rest.first()? {
quote @ (b'"' | b'`') => {
let mut idx = 1;
loop {
match *rest.get(idx)? {
byte if byte == quote => {
idx += 1;
if rest.get(idx) == Some("e) {
idx += 1; } else {
break;
}
}
_ => idx += 1,
}
}
Some(&rest[idx..])
}
b'[' => {
let mut idx = 1;
while *rest.get(idx)? != b']' {
idx += 1;
}
Some(&rest[idx + 1..])
}
_ => next_sqlite_token(rest).map(|(_, next)| next),
}
}
fn skip_common_table_expressions(tail: &[u8]) -> Option<&[u8]> {
let mut rest = skip_sqlite_empty_prefix(tail);
if let Some((word, next)) = next_sqlite_token(rest) {
if word.eq_ignore_ascii_case(b"RECURSIVE") {
rest = skip_sqlite_empty_prefix(next);
}
}
loop {
let next = skip_sqlite_identifier(rest)?;
rest = skip_sqlite_empty_prefix(next);
if rest.first() == Some(&b'(') {
rest = skip_sqlite_empty_prefix(skip_balanced_parens(rest)?);
}
let (as_keyword, next) = next_sqlite_token(rest)?;
if !as_keyword.eq_ignore_ascii_case(b"AS") {
return None;
}
rest = skip_sqlite_empty_prefix(next);
if let Some((word, next)) = next_sqlite_token(rest) {
if word.eq_ignore_ascii_case(b"MATERIALIZED") {
rest = skip_sqlite_empty_prefix(next);
} else if word.eq_ignore_ascii_case(b"NOT") {
let (materialized, next) = next_sqlite_token(skip_sqlite_empty_prefix(next))?;
if !materialized.eq_ignore_ascii_case(b"MATERIALIZED") {
return None;
}
rest = skip_sqlite_empty_prefix(next);
}
}
if rest.first() != Some(&b'(') {
return None;
}
rest = skip_sqlite_empty_prefix(skip_balanced_parens(rest)?);
if rest.first() == Some(&b',') {
rest = skip_sqlite_empty_prefix(&rest[1..]);
continue;
}
return Some(rest);
}
}
pub(crate) fn reader_capability_admits(sql: &str) -> Result<(), String> {
let rest = skip_sqlite_empty_prefix(sql.as_bytes());
let Some((head, tail)) = next_sqlite_token(rest) else {
return Ok(());
};
if head.eq_ignore_ascii_case(b"SELECT") || head.eq_ignore_ascii_case(b"VALUES") {
return Ok(());
}
if head.eq_ignore_ascii_case(b"WITH") {
let Some(after_ctes) = skip_common_table_expressions(tail) else {
return Err(
"WITH statement's common-table-expression list could not be parsed; refusing \
to admit it through the reader capability"
.into(),
);
};
return match next_sqlite_token(after_ctes) {
Some((main_head, _))
if main_head.eq_ignore_ascii_case(b"SELECT")
|| main_head.eq_ignore_ascii_case(b"VALUES") =>
{
Ok(())
}
other => Err(format!(
"WITH ... {:?} is not admitted through the reader capability; only a \
read-only SELECT/VALUES body after the CTE list may run against a pooled \
reader connection",
other.map_or_else(
|| "<none>".to_string(),
|(main_head, _)| String::from_utf8_lossy(main_head).into_owned()
)
)),
};
}
if head.eq_ignore_ascii_case(b"EXPLAIN") {
let mut rest = skip_sqlite_empty_prefix(tail);
if let Some((query, next)) = next_sqlite_token(rest) {
if query.eq_ignore_ascii_case(b"QUERY") {
let after_query = skip_sqlite_empty_prefix(next);
match next_sqlite_token(after_query) {
Some((plan, next2)) if plan.eq_ignore_ascii_case(b"PLAN") => {
rest = skip_sqlite_empty_prefix(next2);
}
_ => {
return Err(
"EXPLAIN QUERY must be followed by PLAN through the reader capability"
.into(),
);
}
}
}
}
return reader_capability_admits(&String::from_utf8_lossy(rest));
}
if head.eq_ignore_ascii_case(b"PRAGMA") {
return reader_capability_admits_pragma(tail);
}
Err(format!(
"statement head {:?} is not admitted through the reader capability; only \
SELECT/WITH/VALUES/EXPLAIN and an allow-listed set of read-only PRAGMA forms \
may run against a pooled reader connection",
String::from_utf8_lossy(head)
))
}
fn reader_capability_admits_pragma(tail: &[u8]) -> Result<(), String> {
let rest = skip_sqlite_empty_prefix(tail);
let Some((mut name, mut after_name)) = next_sqlite_token(rest) else {
return Err("PRAGMA with no name is not admitted through the reader capability".into());
};
if after_name.first() == Some(&b'.') {
let (qualified_name, qualified_after) =
next_sqlite_token(&after_name[1..]).ok_or_else(|| {
"PRAGMA schema-qualifier with no pragma name is not admitted through the \
reader capability"
.to_string()
})?;
name = qualified_name;
after_name = qualified_after;
}
let after = skip_sqlite_empty_prefix(after_name);
let is_structural = READER_STRUCTURAL_PRAGMAS
.iter()
.any(|allowed| name.eq_ignore_ascii_case(allowed.as_bytes()));
let is_setting = READER_SETTING_PRAGMAS
.iter()
.any(|allowed| name.eq_ignore_ascii_case(allowed.as_bytes()));
if !is_structural && !is_setting {
return Err(format!(
"PRAGMA {:?} is not admitted through the reader capability",
String::from_utf8_lossy(name)
));
}
if after.first() == Some(&b'=') {
return Err(format!(
"PRAGMA {:?} may not be assigned through the reader capability",
String::from_utf8_lossy(name)
));
}
if after.first() == Some(&b'(') && !is_structural {
return Err(format!(
"PRAGMA {:?} may not carry an argument through the reader capability",
String::from_utf8_lossy(name)
));
}
Ok(())
}
pub(super) fn admit_reader_capability_sql(
statement: &SqlStatement,
transaction_control: Option<CachedReadTransactionControl>,
operation: &'static str,
) -> khive_storage::types::StorageResult<()> {
if matches!(
transaction_control,
Some(CachedReadTransactionControl::BeginDeferred)
| Some(CachedReadTransactionControl::Finish(_))
) {
return Ok(());
}
reader_capability_admits(&statement.sql).map_err(|message| StorageError::InvalidInput {
capability: StorageCapability::Sql,
operation: operation.into(),
message,
})
}
pub(super) fn execute_query_interruptibly(
scope: &crate::read_cancellation::InterruptibleReadScope,
conn: &rusqlite::Connection,
statement: &SqlStatement,
operation: &'static str,
rollback_interrupted_transaction: bool,
interruptible: bool,
) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
let stmt = prepare_bound_statement(conn, statement)
.map_err(|error| map_rusqlite_err(error, operation))?;
if interruptible && statement_is_cancellable_read(&stmt, &statement.sql) {
scope.run_with_interrupted_cleanup(
conn,
move || {
execute_prepared_query(stmt).map_err(|error| map_rusqlite_err(error, operation))
},
|| {
rollback_interrupted_read_transaction(
conn,
operation,
rollback_interrupted_transaction,
)
},
)
} else {
scope.mark_write_committed()?;
execute_prepared_query(stmt).map_err(|error| map_rusqlite_err(error, operation))
}
}
pub(super) fn execute_query_row_interruptibly(
scope: &crate::read_cancellation::InterruptibleReadScope,
conn: &rusqlite::Connection,
statement: &SqlStatement,
operation: &'static str,
rollback_interrupted_transaction: bool,
interruptible: bool,
) -> khive_storage::types::StorageResult<Option<SqlRow>> {
let stmt = prepare_bound_statement(conn, statement)
.map_err(|error| map_rusqlite_err(error, operation))?;
if interruptible && statement_is_cancellable_read(&stmt, &statement.sql) {
scope.run_with_interrupted_cleanup(
conn,
move || {
execute_prepared_query_row(stmt).map_err(|error| map_rusqlite_err(error, operation))
},
|| {
rollback_interrupted_read_transaction(
conn,
operation,
rollback_interrupted_transaction,
)
},
)
} else {
scope.mark_write_committed()?;
execute_prepared_query_row(stmt).map_err(|error| map_rusqlite_err(error, operation))
}
}
pub(super) fn execute_query_page_interruptibly(
scope: &crate::read_cancellation::InterruptibleReadScope,
conn: &rusqlite::Connection,
statement: &SqlStatement,
page: &PageRequest,
operation: &'static str,
rollback_interrupted_transaction: bool,
interruptible: bool,
) -> khive_storage::types::StorageResult<Vec<SqlRow>> {
let stmt = prepare_bound_statement(conn, statement)
.map_err(|error| map_rusqlite_err(error, operation))?;
if interruptible && statement_is_cancellable_read(&stmt, &statement.sql) {
scope.run_with_interrupted_cleanup(
conn,
move || {
execute_prepared_query_page(stmt, page)
.map_err(|error| map_rusqlite_err(error, operation))
},
|| {
rollback_interrupted_read_transaction(
conn,
operation,
rollback_interrupted_transaction,
)
},
)
} else {
scope.mark_write_committed()?;
execute_prepared_query_page(stmt, page).map_err(|error| map_rusqlite_err(error, operation))
}
}
fn rollback_interrupted_read_transaction(
conn: &rusqlite::Connection,
operation: &'static str,
enabled: bool,
) -> khive_storage::types::StorageResult<()> {
if !enabled || conn.is_autocommit() {
return Ok(());
}
conn.execute_batch("ROLLBACK")
.map_err(|error| map_rusqlite_err(error, operation))?;
if conn.is_autocommit() {
Ok(())
} else {
Err(StorageError::Transaction {
operation: operation.into(),
message: "interrupted read transaction rollback did not restore autocommit".into(),
})
}
}