use nodedb_sql::types::{CollectionInfo, CtePlan, DocumentIndexLookupPlan, EngineType, SqlPlan};
use nodedb_sql::{SqlCatalog, SqlCatalogError, plan_sql};
use nodedb_types::DatabaseId;
fn text_column(name: &str, is_primary_key: bool) -> nodedb_sql::types::ColumnInfo {
nodedb_sql::types::ColumnInfo {
name: name.into(),
data_type: nodedb_sql::types::SqlDataType::String,
nullable: !is_primary_key,
is_primary_key,
default: None,
raw_type: None,
int_width: None,
float_width: None,
}
}
struct Catalog;
impl SqlCatalog for Catalog {
fn get_collection(
&self,
_: DatabaseId,
name: &str,
) -> std::result::Result<Option<CollectionInfo>, SqlCatalogError> {
let info = match name {
"articles" | "authors" => Some(CollectionInfo {
name: name.into(),
engine: EngineType::DocumentStrict,
columns: vec![text_column("id", true), text_column("name", false)],
primary_key: Some("id".into()),
has_auto_tier: false,
indexes: Vec::new(),
bitemporal: false,
primary: nodedb_types::PrimaryEngine::Document,
vector_primary: None,
partition_strategy: nodedb_types::PartitionStrategy::CollectionHomed,
open_schema: CollectionInfo::open_schema_for(EngineType::DocumentStrict),
}),
_ => None,
};
Ok(info)
}
fn lookup_array(&self, _name: &str) -> Option<nodedb_sql::types::ArrayCatalogView> {
None
}
fn array_exists(&self, _name: &str) -> bool {
false
}
}
fn bound_of(plan: &SqlPlan) -> String {
match plan {
SqlPlan::Scan { limit, offset, .. }
| SqlPlan::DocumentIndexLookup(DocumentIndexLookupPlan { limit, offset, .. })
| SqlPlan::Subquery { limit, offset, .. } => format!("limit={limit:?} offset={offset}"),
SqlPlan::Cte(CtePlan { outer, .. }) => format!("cte outer -> {}", bound_of(outer)),
other => format!("{other:?}"),
}
}
fn expect_rejected(sql: &str) {
match plan_sql(sql, &Catalog) {
Err(_) => {}
Ok(plans) => {
let bounds: Vec<String> = plans.iter().map(bound_of).collect();
panic!("must reject an out-of-domain row bound, planned instead: {sql} -> {bounds:?}");
}
}
}
fn plan_one(sql: &str) -> SqlPlan {
let mut plans = plan_sql(sql, &Catalog).expect("planning must succeed");
assert_eq!(plans.len(), 1, "expected exactly one plan for: {sql}");
plans.pop().expect("one plan")
}
#[test]
fn negative_limit_is_rejected() {
expect_rejected("SELECT * FROM articles LIMIT -1");
}
#[test]
fn negative_limit_as_unknown_param_text_is_rejected() {
expect_rejected("SELECT * FROM articles LIMIT '-1'");
}
#[test]
fn non_numeric_limit_is_rejected() {
expect_rejected("SELECT * FROM articles LIMIT 'abc'");
}
#[test]
fn fractional_limit_is_rejected() {
expect_rejected("SELECT * FROM articles LIMIT 1.5");
}
#[test]
fn overflowing_limit_is_rejected() {
expect_rejected("SELECT * FROM articles LIMIT 99999999999999999999999999");
}
#[test]
fn negative_offset_is_rejected() {
expect_rejected("SELECT * FROM articles OFFSET -2");
}
#[test]
fn negative_offset_as_unknown_param_text_is_rejected() {
expect_rejected("SELECT * FROM articles OFFSET '-2'");
}
#[test]
fn non_numeric_offset_is_rejected() {
expect_rejected("SELECT * FROM articles OFFSET 'abc'");
}
#[test]
fn overflowing_offset_is_rejected() {
expect_rejected("SELECT * FROM articles OFFSET 99999999999999999999999999");
}
#[test]
fn negative_limit_in_derived_table_is_rejected() {
expect_rejected("SELECT * FROM (SELECT * FROM articles LIMIT -1) s");
}
#[test]
fn negative_limit_in_cte_body_is_rejected() {
expect_rejected("WITH c AS (SELECT * FROM articles LIMIT -1) SELECT * FROM c");
}
#[test]
fn negative_limit_in_lateral_subquery_is_rejected() {
expect_rejected(
"SELECT a.id, e.id FROM authors a \
JOIN LATERAL (SELECT id FROM articles WHERE articles.id = a.id LIMIT -1) e ON true",
);
}
#[test]
fn nonzero_offset_in_lateral_subquery_with_limit_is_rejected() {
expect_rejected(
"SELECT a.id, e.id FROM authors a \
JOIN LATERAL (SELECT id FROM articles WHERE articles.id = a.id \
ORDER BY id LIMIT 3 OFFSET 2) e ON true",
);
}
#[test]
fn nonzero_offset_in_lateral_subquery_without_limit_is_rejected() {
expect_rejected(
"SELECT a.id, e.id FROM authors a \
JOIN LATERAL (SELECT id FROM articles WHERE articles.id = a.id OFFSET 2) e ON true",
);
}
#[test]
fn negative_offset_in_lateral_subquery_is_rejected() {
expect_rejected(
"SELECT a.id, e.id FROM authors a \
JOIN LATERAL (SELECT id FROM articles WHERE articles.id = a.id \
ORDER BY id LIMIT 3 OFFSET -2) e ON true",
);
}
#[test]
fn zero_offset_in_lateral_subquery_plans_cleanly() {
let _ = plan_one(
"SELECT a.id, e.id FROM authors a \
JOIN LATERAL (SELECT id FROM articles WHERE articles.id = a.id \
ORDER BY id LIMIT 3 OFFSET 0) e ON true",
);
}
#[test]
fn lateral_subquery_with_limit_and_no_offset_plans_cleanly() {
let _ = plan_one(
"SELECT a.id, e.id FROM authors a \
JOIN LATERAL (SELECT id FROM articles WHERE articles.id = a.id \
ORDER BY id LIMIT 3) e ON true",
);
}
#[test]
fn zero_limit_is_honored() {
match plan_one("SELECT * FROM articles LIMIT 0") {
SqlPlan::Scan { limit, .. } => assert_eq!(limit, Some(0)),
other => panic!("expected a Scan, got: {other:?}"),
}
}
#[test]
fn positive_limit_and_offset_are_honored() {
match plan_one("SELECT * FROM articles LIMIT 5 OFFSET 2") {
SqlPlan::Scan { limit, offset, .. } => {
assert_eq!(limit, Some(5));
assert_eq!(offset, 2);
}
other => panic!("expected a Scan, got: {other:?}"),
}
}
#[test]
fn absent_limit_stays_unbounded() {
match plan_one("SELECT * FROM articles") {
SqlPlan::Scan { limit, offset, .. } => {
assert_eq!(limit, None);
assert_eq!(offset, 0);
}
other => panic!("expected a Scan, got: {other:?}"),
}
}
#[test]
fn null_limit_stays_unbounded() {
match plan_one("SELECT * FROM articles LIMIT NULL") {
SqlPlan::Scan { limit, .. } => assert_eq!(limit, None),
other => panic!("expected a Scan, got: {other:?}"),
}
}
#[test]
fn null_offset_stays_unbounded() {
match plan_one("SELECT * FROM articles OFFSET NULL") {
SqlPlan::Scan { offset, .. } => assert_eq!(offset, 0),
other => panic!("expected a Scan, got: {other:?}"),
}
}
#[test]
fn null_limit_with_offset_is_honored() {
match plan_one("SELECT * FROM articles LIMIT NULL OFFSET 2") {
SqlPlan::Scan { limit, offset, .. } => {
assert_eq!(limit, None);
assert_eq!(offset, 2);
}
other => panic!("expected a Scan, got: {other:?}"),
}
}
#[test]
fn prepared_limit_placeholder_null_plans_cleanly() {
let _ = plan_one("SELECT id FROM articles ORDER BY id LIMIT NULL");
}
#[test]
fn fetch_first_n_rows_only_is_honored() {
match plan_one("SELECT * FROM articles FETCH FIRST 2 ROWS ONLY") {
SqlPlan::Scan { limit, .. } => assert_eq!(limit, Some(2)),
other => panic!("expected a Scan, got: {other:?}"),
}
}
#[test]
fn fetch_first_row_only_with_no_count_is_one_row() {
match plan_one("SELECT * FROM articles FETCH FIRST ROW ONLY") {
SqlPlan::Scan { limit, .. } => assert_eq!(limit, Some(1)),
other => panic!("expected a Scan, got: {other:?}"),
}
}
#[test]
fn offset_and_fetch_first_are_both_honored() {
match plan_one("SELECT * FROM articles OFFSET 2 FETCH FIRST 2 ROWS ONLY") {
SqlPlan::Scan { limit, offset, .. } => {
assert_eq!(limit, Some(2));
assert_eq!(offset, 2);
}
other => panic!("expected a Scan, got: {other:?}"),
}
}
#[test]
fn non_numeric_fetch_first_is_rejected() {
expect_rejected("SELECT * FROM articles FETCH FIRST 'abc' ROWS ONLY");
}
#[test]
fn fetch_first_with_ties_is_rejected() {
expect_rejected("SELECT * FROM articles ORDER BY id FETCH FIRST 2 ROWS WITH TIES");
}
#[test]
fn limit_combined_with_fetch_first_is_rejected() {
expect_rejected("SELECT * FROM articles LIMIT 2 FETCH FIRST 2 ROWS ONLY");
}
#[test]
fn update_limit_is_rejected() {
expect_rejected("UPDATE articles SET name = 'x' WHERE id = '1' LIMIT 1");
}
#[test]
fn delete_limit_is_rejected() {
expect_rejected("DELETE FROM articles WHERE id = '1' LIMIT 1");
}
#[test]
fn update_without_limit_plans() {
let _ = plan_one("UPDATE articles SET name = 'x' WHERE id = '1'");
}
#[test]
fn delete_without_limit_plans() {
let _ = plan_one("DELETE FROM articles WHERE id = '1'");
}