use crate::{
db::{DbSession, StorageReport},
error::Error,
traits::CanisterKind,
};
#[cfg(feature = "sql")]
use crate::{
db::{EntityAuthority, sql::SqlQueryResult},
error::{ErrorKind, QueryErrorKind},
};
pub fn execute_generated_storage_report<C: CanisterKind>(
session: &DbSession<C>,
) -> Result<StorageReport, Error> {
Ok(session.inner.storage_report_default()?)
}
#[cfg(feature = "sql")]
pub fn execute_generated_sql_query<C: CanisterKind>(
session: &DbSession<C>,
sql: &str,
authorities: &[EntityAuthority],
) -> Result<SqlQueryResult, Error> {
let attempt = session
.inner
.execute_generated_query_surface_sql(sql, authorities);
let entity_name = attempt.entity_name().to_string();
let explain_order_field = attempt.explain_order_field();
match attempt.into_result() {
Ok(result) => Ok(DbSession::<C>::map_sql_dispatch_result(result, entity_name)),
Err(err) => {
let facade = Error::from(err);
if let Some(order_field) = explain_order_field {
Err(explain_surface_error(sql, order_field, facade))
} else {
Err(facade)
}
}
}
}
#[cfg(feature = "sql")]
fn explain_surface_error(sql: &str, order_field: &str, err: Error) -> Error {
if !matches!(
err.kind(),
ErrorKind::Query(QueryErrorKind::UnorderedPagination)
) {
return err;
}
let target_sql = explain_target_sql(sql);
let suggestion = explain_order_hint_sql(target_sql, order_field);
let message = format!(
"Cannot EXPLAIN this SQL statement.\n\nReason:\nLIMIT or OFFSET without ORDER BY is non-deterministic.\n\nSQL:\n{target_sql}\n\nHow to fix:\nAdd ORDER BY for a stable total order, for example:\n{suggestion}",
);
Error::new(
ErrorKind::Query(QueryErrorKind::UnorderedPagination),
err.origin(),
message,
)
}
#[cfg(feature = "sql")]
fn explain_target_sql(sql: &str) -> &str {
let mut rest = sql.trim_start();
if let Some(next) = consume_keyword(rest, "EXPLAIN") {
rest = next;
if let Some(next) = consume_keyword(rest, "EXECUTION") {
rest = next;
} else if let Some(next) = consume_keyword(rest, "JSON") {
rest = next;
}
}
rest.trim_start()
}
#[cfg(feature = "sql")]
fn explain_order_hint_sql(target_sql: &str, order_field: &str) -> String {
let trimmed = target_sql.trim().trim_end_matches(';').trim_end();
let upper = trimmed.to_ascii_uppercase();
if let Some(index) = upper.find(" LIMIT ") {
return format!(
"EXPLAIN {} ORDER BY {order_field} ASC{}",
&trimmed[..index],
&trimmed[index..]
);
} else if let Some(index) = upper.find(" OFFSET ") {
return format!(
"EXPLAIN {} ORDER BY {order_field} ASC{}",
&trimmed[..index],
&trimmed[index..]
);
}
format!("EXPLAIN {trimmed} ORDER BY {order_field} ASC")
}
#[cfg(feature = "sql")]
fn consume_keyword<'a>(input: &'a str, keyword: &str) -> Option<&'a str> {
let rest = input.strip_prefix(keyword)?;
if rest
.chars()
.next()
.is_some_and(|ch| ch.is_ascii_alphanumeric() || ch == '_')
{
return None;
}
Some(rest.trim_start())
}