use rudb::Database;
use rudb_common::Value;
fn database() -> Database {
let database = Database::new();
for sql in [
"CREATE TABLE t (i INTEGER)",
"INSERT INTO t VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9)",
] {
database.execute(sql).unwrap_or_else(|error| panic!("{sql} failed: {error}"));
}
database
}
fn numbers(database: &Database, sql: &str) -> Vec<i32> {
let result = database.query(sql).unwrap_or_else(|error| panic!("{sql} failed: {error}"));
(0..result.len())
.map(|row| match result.value_at(row, 0) {
Value::Integer(number) => number,
other => panic!("{sql} answered {other:?}"),
})
.collect()
}
fn refused(database: &Database, sql: &str) -> String {
match database.query(sql) {
Ok(_) => panic!("{sql} was expected to be refused"),
Err(error) => error.to_string(),
}
}
#[test]
fn a_limit_and_an_offset_can_be_worked_out_rather_than_written_down() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 1 + 1"), vec![0, 1]);
assert_eq!(numbers(&database, "SELECT i FROM t OFFSET 3 + 4"), vec![7, 8, 9]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 1 + 1 OFFSET 3 + 4"), vec![7, 8]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT abs(-2)"), vec![0, 1]);
}
#[test]
fn a_cast_is_worked_out_the_same_way() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT CAST(2 AS BIGINT)"), vec![0, 1]);
assert_eq!(numbers(&database, "SELECT i FROM t OFFSET CAST(8 AS BIGINT)"), vec![8, 9]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 2::UTINYINT"), vec![0, 1]);
}
#[test]
fn the_value_is_cast_to_a_row_count_whatever_it_was_written_as() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT '3'"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 2.5"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 2.4"), vec![0, 1]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT true"), vec![0]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT false"), Vec::<i32>::new());
}
#[test]
fn an_arm_of_a_case_that_does_not_fire_is_not_evaluated() {
let database = database();
let sql = "SELECT i FROM t LIMIT CASE WHEN 1 = 1 THEN 2 ELSE 1 // 0 END";
assert_eq!(numbers(&database, sql), vec![0, 1]);
let sql = "SELECT i FROM t LIMIT CASE WHEN 1 = 2 THEN 1 // 0 ELSE 2 END";
assert_eq!(numbers(&database, sql), vec![0, 1]);
}
#[test]
fn a_null_limit_or_offset_is_the_same_as_not_writing_one() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT NULL").len(), 10);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT CAST(NULL AS INTEGER)").len(), 10);
assert_eq!(numbers(&database, "SELECT i FROM t OFFSET NULL").len(), 10);
}
#[test]
fn a_negative_row_count_is_refused() {
let database = database();
for sql in [
"SELECT i FROM t LIMIT -1",
"SELECT i FROM t OFFSET -1",
"SELECT i FROM t LIMIT 1 - 5",
"SELECT i FROM t OFFSET 1 - 5",
] {
let message = refused(&database, sql);
assert!(message.contains("LIMIT/OFFSET cannot be negative"), "{sql}: {message}");
}
}
#[test]
fn a_value_that_is_not_a_row_count_is_refused_by_the_cast() {
let database = database();
let message = refused(&database, "SELECT i FROM t LIMIT DATE '2020-01-01'");
assert!(message.contains("Unimplemented type for cast (DATE -> BIGINT)"), "{message}");
let message = refused(&database, "SELECT i FROM t LIMIT INTERVAL 3 DAY");
assert!(message.contains("Unimplemented type for cast (INTERVAL -> BIGINT)"), "{message}");
let message = refused(&database, "SELECT i FROM t LIMIT 'abc'");
assert!(message.contains("Could not convert string 'abc' to INT64"), "{message}");
let message = refused(&database, "SELECT i FROM t LIMIT 99999999999999999999");
assert!(message.contains("out of range for the destination type INT64"), "{message}");
}
#[test]
fn a_row_count_that_raises_fails_the_query() {
let database = database();
let message = refused(&database, "SELECT i FROM t LIMIT 1 // 0");
assert!(message.contains("Division by zero"), "{message}");
}
#[test]
fn a_row_count_naming_a_column_is_refused_like_any_other_unknown_name() {
let database = database();
let message = refused(&database, "SELECT i FROM t LIMIT i");
assert!(message.contains("Referenced column \"i\""), "{message}");
}
#[test]
fn a_limit_can_be_a_share_of_the_input_rather_than_a_row_count() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 30 PERCENT"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 35 PERCENT"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 5 PERCENT"), Vec::<i32>::new());
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 0 PERCENT"), Vec::<i32>::new());
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 100 PERCENT").len(), 10);
}
#[test]
fn the_offset_of_a_share_is_applied_after_the_share_is_worked_out() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 30 PERCENT OFFSET 2"), vec![2, 3, 4]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT 50 PERCENT OFFSET 8"), vec![8, 9]);
let sql = "SELECT i FROM t LIMIT 30 PERCENT OFFSET 20";
assert_eq!(numbers(&database, sql), Vec::<i32>::new());
}
#[test]
fn a_share_written_with_a_percent_sign_is_worked_out_like_any_other_expression() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (10 + 20)%"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT '30'%"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT true%"), Vec::<i32>::new());
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT NULL%").len(), 10);
}
#[test]
fn a_share_that_is_not_between_nought_and_a_hundred_is_refused() {
let database = database();
for sql in [
"SELECT i FROM t LIMIT 101 PERCENT",
"SELECT i FROM t LIMIT 100.5 PERCENT",
"SELECT i FROM t LIMIT -10%",
"SELECT i FROM t LIMIT ('nan'::DOUBLE)%",
] {
let message = refused(&database, sql);
assert!(message.contains("Limit percent out of range"), "{sql}: {message}");
}
}
#[test]
fn a_row_count_can_be_a_subquery() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT 3)"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t OFFSET (SELECT 8)"), vec![8, 9]);
let sql = "SELECT i FROM t LIMIT (SELECT 3) OFFSET (SELECT 2)";
assert_eq!(numbers(&database, sql), vec![2, 3, 4]);
let sql = "SELECT i FROM t LIMIT (SELECT count(*) FROM t WHERE i < 4)";
assert_eq!(numbers(&database, sql), vec![0, 1, 2, 3]);
}
#[test]
fn the_column_the_count_is_read_out_of_is_not_a_column_of_the_answer() {
let database = database();
let sql = "SELECT * FROM t LIMIT (SELECT 2)";
let result = database.query(sql).unwrap_or_else(|error| panic!("{sql} failed: {error}"));
assert_eq!(result.width(), 1, "{sql} answered an extra column");
assert_eq!(numbers(&database, sql), vec![0, 1]);
assert_eq!(numbers(&database, "VALUES (7), (8), (9) LIMIT (SELECT 2)"), vec![7, 8]);
let sql = "SELECT i FROM t UNION ALL SELECT i FROM t LIMIT (SELECT 3)";
assert_eq!(numbers(&database, sql), vec![0, 1, 2]);
}
#[test]
fn a_subquery_that_answers_nothing_or_null_leaves_every_row() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT NULL)").len(), 10);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT i FROM t WHERE false)").len(), 10);
assert_eq!(numbers(&database, "SELECT i FROM t OFFSET (SELECT NULL)").len(), 10);
}
#[test]
fn a_row_count_read_while_the_query_runs_is_cast_the_way_a_written_one_is() {
let database = database();
for (sql, expected) in [
("SELECT i FROM t LIMIT (SELECT -1)", "LIMIT/OFFSET cannot be negative"),
("SELECT i FROM t OFFSET (SELECT -1)", "LIMIT/OFFSET cannot be negative"),
("SELECT i FROM t LIMIT (SELECT 'abc')", "Could not convert string 'abc' to INT64"),
("SELECT i FROM t LIMIT (SELECT DATE '2020-01-01')", "(DATE -> BIGINT)"),
] {
let message = refused(&database, sql);
assert!(message.contains(expected), "{sql}: {message}");
}
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT '3')"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT true)"), vec![0]);
}
#[test]
fn a_row_count_read_while_the_query_runs_does_not_become_a_top_n() {
let database = database();
let sql = "SELECT i FROM t ORDER BY i DESC LIMIT (SELECT 3)";
assert_eq!(numbers(&database, sql), vec![9, 8, 7]);
let plan = database.plan(sql).unwrap_or_else(|error| panic!("{sql} failed: {error}"));
assert!(plan.contains("Sort"), "{sql} planned as {plan}");
assert!(!plan.contains("TopN"), "{sql} planned as {plan}");
}
#[test]
fn a_share_and_its_offset_can_both_be_a_subquery() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT 30)%"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT 30)% OFFSET 1"), vec![1, 2, 3]);
let sql = "SELECT i FROM t LIMIT (SELECT 30)% OFFSET (SELECT 1)";
assert_eq!(numbers(&database, sql), vec![1, 2, 3]);
let sql = "SELECT i FROM t LIMIT 30 PERCENT OFFSET (SELECT 2)";
assert_eq!(numbers(&database, sql), vec![2, 3, 4]);
let sql = "SELECT i FROM t LIMIT 30 PERCENT OFFSET (SELECT count(*) FROM t WHERE i < 3)";
assert_eq!(numbers(&database, sql), vec![3, 4, 5]);
let sql = "SELECT i FROM t LIMIT 100 PERCENT OFFSET (SELECT 20)";
assert_eq!(numbers(&database, sql), Vec::<i32>::new());
}
#[test]
fn the_column_a_share_is_read_out_of_is_not_a_column_of_the_answer() {
let database = database();
for sql in [
"SELECT * FROM t LIMIT (SELECT 30)%",
"SELECT * FROM t LIMIT 30 PERCENT OFFSET (SELECT 0)",
"SELECT * FROM t LIMIT (SELECT 30)% OFFSET (SELECT 0)",
] {
let result = database.query(sql).unwrap_or_else(|error| panic!("{sql} failed: {error}"));
assert_eq!(result.width(), 1, "{sql} answered an extra column");
assert_eq!(numbers(&database, sql), vec![0, 1, 2]);
}
}
#[test]
fn a_share_read_while_the_query_runs_is_cast_the_way_a_written_one_is() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT '30')%"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT 35.5)%"), vec![0, 1, 2]);
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT true)%"), Vec::<i32>::new());
let message = refused(&database, "SELECT i FROM t LIMIT (SELECT 'abc')%");
assert!(message.contains("Could not convert string 'abc' to DOUBLE"), "{message}");
}
#[test]
fn a_share_that_answers_nothing_or_null_leaves_every_row() {
let database = database();
assert_eq!(numbers(&database, "SELECT i FROM t LIMIT (SELECT NULL)%").len(), 10);
let sql = "SELECT i FROM t LIMIT (SELECT i FROM t WHERE false)%";
assert_eq!(numbers(&database, sql).len(), 10);
}
#[test]
fn a_share_read_while_the_query_runs_is_checked_against_the_same_range() {
let database = database();
for sql in [
"SELECT i FROM t LIMIT (SELECT 101)%",
"SELECT i FROM t LIMIT (SELECT 100.5)%",
"SELECT i FROM t LIMIT (SELECT 'nan'::DOUBLE)%",
] {
let message = refused(&database, sql);
assert!(message.contains("Limit percent out of range"), "{sql}: {message}");
}
let message = refused(&database, "SELECT i FROM t LIMIT (SELECT -1)%");
assert!(message.contains("Percentage value(-1.000000) can't be negative"), "{message}");
let message = refused(&database, "SELECT i FROM t LIMIT (SELECT -0.5)%");
assert!(message.contains("Percentage value(-0.500000) can't be negative"), "{message}");
}
#[test]
fn a_share_over_no_rows_at_all_is_not_read_and_not_refused() {
let database = database();
for sql in [
"SELECT i FROM t WHERE false LIMIT (SELECT -1)%",
"SELECT i FROM t WHERE false LIMIT (SELECT 101)%",
"SELECT i FROM t WHERE false LIMIT 30 PERCENT OFFSET (SELECT -1)",
] {
assert_eq!(numbers(&database, sql), Vec::<i32>::new(), "{sql}");
}
}
#[test]
fn a_share_read_while_the_query_runs_does_not_become_a_top_n() {
let database = database();
let sql = "SELECT i FROM t ORDER BY i DESC LIMIT (SELECT 30)%";
assert_eq!(numbers(&database, sql), vec![9, 8, 7]);
let plan = database.plan(sql).unwrap_or_else(|error| panic!("{sql} failed: {error}"));
assert!(plan.contains("Sort"), "{sql} planned as {plan}");
assert!(!plan.contains("TopN"), "{sql} planned as {plan}");
}