use rudb_common::{Error, Result};
use rudb_parse::ast::Statement as Parsed;
use rudb_parse::{parse, parse_ast};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RowOrder {
Declared,
Unspecified,
Unknown,
}
#[must_use]
pub fn row_order(sql: &str) -> RowOrder {
let Ok(ast) = parse_ast(sql) else {
return RowOrder::Unknown;
};
let declared = ast.statements.iter().any(|statement| match statement {
Parsed::Query(at) => {
let query = ast.query(*at);
query.order_by_all || !query.order_by.is_empty()
}
_ => false,
});
if declared { RowOrder::Declared } else { RowOrder::Unspecified }
}
pub fn accepts(sql: &str) -> Result<()> {
parse(sql).map(|_| ())
}
#[must_use]
pub fn parses(sql: &str) -> bool {
accepts(sql).is_ok()
}
pub fn split(text: &str) -> Result<Vec<String>> {
let Ok(found) = crate::statements(text) else {
let trimmed = text.trim();
return Ok(if trimmed.is_empty() { Vec::new() } else { vec![trimmed.to_string()] });
};
Ok(found.into_iter().map(|statement| statement.sql().to_string()).collect())
}
#[must_use]
pub fn line_and_column(text: &str, offset: usize) -> (usize, usize) {
let upto = &text[..offset.min(text.len())];
let line = upto.bytes().filter(|&b| b == b'\n').count() + 1;
let column = upto.rsplit('\n').next().unwrap_or("").chars().count() + 1;
(line, column)
}
#[must_use]
pub fn where_it_happened(sql: &str, error: &Error) -> Option<(usize, usize)> {
error.span().map(|span| line_and_column(sql, span.start as usize))
}
#[cfg(test)]
mod tests {
use rudb_common::Error;
use super::{RowOrder, accepts, line_and_column, parses, row_order, split, where_it_happened};
#[test]
fn a_top_level_order_by_is_a_declared_order() {
assert_eq!(row_order("SELECT x FROM t ORDER BY x"), RowOrder::Declared);
assert_eq!(row_order("SELECT * FROM t ORDER BY ALL"), RowOrder::Declared);
}
#[test]
fn a_query_with_no_order_by_promises_nothing_about_the_order() {
assert_eq!(row_order("SELECT x FROM t"), RowOrder::Unspecified);
assert_eq!(row_order("SELECT 1"), RowOrder::Unspecified);
}
#[test]
fn an_order_by_inside_a_subquery_does_not_survive_into_the_outer_result() {
assert_eq!(
row_order("SELECT x FROM (SELECT x FROM t ORDER BY x) AS inner_query"),
RowOrder::Unspecified
);
}
#[test]
fn a_statement_that_returns_no_rows_has_no_order_to_preserve() {
assert_eq!(row_order("CREATE TABLE t (x INTEGER)"), RowOrder::Unspecified);
assert_eq!(row_order("INSERT INTO t VALUES (1)"), RowOrder::Unspecified);
}
#[test]
fn text_that_does_not_parse_here_says_it_does_not_know() {
assert_eq!(row_order("SELECT FROM WHERE"), RowOrder::Unknown);
assert_eq!(row_order("this is not sql at all"), RowOrder::Unknown);
}
#[test]
fn the_grammar_accepts_more_than_the_ast_builds() {
assert!(parses("SELECT x FROM t"));
assert!(parses("MERGE INTO t USING s ON t.x = s.x WHEN MATCHED THEN DELETE"));
assert_eq!(
row_order("MERGE INTO t USING s ON t.x = s.x WHEN MATCHED THEN DELETE"),
RowOrder::Unknown
);
}
#[test]
fn something_that_is_not_sql_is_rejected_with_a_parser_error() {
let error = accepts("SELECT FROM WHERE").expect_err("that is not valid SQL");
assert_eq!(error.code().duckdb_name(), "Parser Error");
assert!(!parses("SELECT FROM WHERE"));
}
#[test]
fn a_file_splits_into_its_statements() {
assert_eq!(split("SELECT 1; SELECT 2;").unwrap(), vec!["SELECT 1", "SELECT 2"]);
assert_eq!(split("SELECT ';'").unwrap(), vec!["SELECT ';'"]);
assert!(split("-- nothing but a comment\n").unwrap().is_empty());
assert!(split(" ").unwrap().is_empty());
}
#[test]
fn text_that_does_not_tokenize_comes_back_whole_rather_than_being_refused() {
assert_eq!(split("SELECT 'unclosed").unwrap(), vec!["SELECT 'unclosed"]);
}
#[test]
fn a_byte_offset_becomes_the_line_and_column_a_person_reads() {
let text = "SELECT 1\nFROM t\nWHERE x";
assert_eq!(line_and_column(text, 0), (1, 1));
assert_eq!(line_and_column(text, 7), (1, 8));
assert_eq!(line_and_column(text, 9), (2, 1));
assert_eq!(line_and_column(text, 16), (3, 1));
assert_eq!(line_and_column(text, 9_999), (3, 8));
}
#[test]
fn a_column_counts_characters_rather_than_bytes() {
assert_eq!(line_and_column("\u{1f600}x", 4), (1, 2));
}
#[test]
fn an_error_with_no_span_says_nothing_about_where() {
assert_eq!(where_it_happened("SELECT 1", &Error::internal("no span on this one")), None);
}
}