use super::*;
use crate::db::EntityAuthority;
fn assert_sql_surface_rejects_statement_lanes<T, F>(cases: &[(&str, &str)], mut execute: F)
where
F: FnMut(&str) -> Result<T, QueryError>,
{
for (sql, context) in cases {
assert_unsupported_sql_surface_result(execute(sql), context);
}
}
fn assert_sql_surface_rejects_statement_lanes_with_message<T, F>(
cases: &[(&str, &str)],
mut execute: F,
surface: &str,
) where
F: FnMut(&str) -> Result<T, QueryError>,
{
for (sql, expected) in cases {
let Err(err) = execute(sql) else {
panic!("{surface} should reject the non-owned lane: {sql}");
};
assert!(
err.to_string().contains(expected),
"{surface} should preserve a surface-local lane boundary message: {sql}",
);
}
}
fn assert_sql_surface_preserves_unsupported_feature_detail<T, F>(mut execute: F)
where
F: FnMut(&str) -> Result<T, QueryError>,
{
for (sql, feature) in unsupported_sql_feature_cases() {
let Err(err) = execute(sql) else {
panic!("unsupported SQL feature should fail through the SQL surface");
};
assert_sql_unsupported_feature_detail(err, feature);
}
}
fn assert_specific_sql_unsupported_feature_detail<T, F>(
sql: &str,
feature: &'static str,
mut execute: F,
) where
F: FnMut(&str) -> Result<T, QueryError>,
{
let Err(err) = execute(sql) else {
panic!("unsupported SQL feature should fail through the selected SQL surface");
};
assert_sql_unsupported_feature_detail(err, feature);
}
fn assert_sql_statement_route_case(
session: &DbSession<SessionSqlCanister>,
sql: &str,
expected_route: SqlStatementRoute,
expected_entity: &str,
flags: (bool, bool, bool, bool, bool),
context: &str,
) {
let route = session
.sql_statement_route(sql)
.unwrap_or_else(|err| panic!("{context} should parse: {err:?}"));
assert_eq!(route, expected_route, "{context} should classify the route");
assert_eq!(
route.entity(),
expected_entity,
"{context} should preserve the entity surface"
);
assert_eq!(
route.is_explain(),
flags.0,
"{context} explain flag should match"
);
assert_eq!(
route.is_describe(),
flags.1,
"{context} describe flag should match"
);
assert_eq!(
route.is_show_indexes(),
flags.2,
"{context} show-indexes flag should match",
);
assert_eq!(
route.is_show_columns(),
flags.3,
"{context} show-columns flag should match",
);
assert_eq!(
route.is_show_entities(),
flags.4,
"{context} show-entities flag should match",
);
}
#[expect(
clippy::too_many_lines,
reason = "query-surface rejection matrix is intentionally tabular"
)]
#[test]
fn sql_query_surfaces_reject_non_query_statement_lanes_matrix() {
reset_session_sql_store();
let session = sql_session();
assert_sql_surface_rejects_statement_lanes(
&[
(
"EXPLAIN SELECT * FROM SessionSqlEntity",
"query_from_sql must reject EXPLAIN statements",
),
(
"DESCRIBE SessionSqlEntity",
"query_from_sql must reject DESCRIBE statements",
),
(
"SHOW INDEXES SessionSqlEntity",
"query_from_sql must reject SHOW INDEXES statements",
),
(
"SHOW COLUMNS SessionSqlEntity",
"query_from_sql must reject SHOW COLUMNS statements",
),
(
"SHOW ENTITIES",
"query_from_sql must reject SHOW ENTITIES statements",
),
(
"INSERT INTO SessionSqlEntity (id, name, age) VALUES (1, 'Ada', 21)",
"query_from_sql must reject INSERT statements",
),
(
"INSERT INTO SessionSqlEntity (name, age) SELECT name, age FROM SessionSqlEntity ORDER BY id ASC LIMIT 1",
"query_from_sql must reject INSERT statements",
),
(
"UPDATE SessionSqlEntity SET age = 22 WHERE id = 1",
"query_from_sql must reject UPDATE statements",
),
],
|sql| session.query_from_sql::<SessionSqlEntity>(sql),
);
let message_cases = [
(
"EXPLAIN SELECT * FROM SessionSqlEntity",
"execute_sql rejects EXPLAIN",
),
("DESCRIBE SessionSqlEntity", "execute_sql rejects DESCRIBE"),
(
"SHOW INDEXES SessionSqlEntity",
"execute_sql rejects SHOW INDEXES",
),
(
"SHOW COLUMNS SessionSqlEntity",
"execute_sql rejects SHOW COLUMNS",
),
("SHOW ENTITIES", "execute_sql rejects SHOW ENTITIES"),
(
"INSERT INTO SessionSqlEntity (id, name, age) VALUES (1, 'Ada', 21)",
"execute_sql rejects INSERT",
),
(
"INSERT INTO SessionSqlEntity (name, age) SELECT name, age FROM SessionSqlEntity ORDER BY id ASC LIMIT 1",
"execute_sql rejects INSERT",
),
(
"UPDATE SessionSqlEntity SET age = 22 WHERE id = 1",
"execute_sql rejects UPDATE",
),
];
assert_sql_surface_rejects_statement_lanes_with_message(
&message_cases,
|sql| session.execute_sql::<SessionSqlEntity>(sql),
"execute_sql",
);
let grouped_cases = [
(
"EXPLAIN SELECT * FROM SessionSqlEntity",
"execute_sql_grouped rejects EXPLAIN",
),
(
"DESCRIBE SessionSqlEntity",
"execute_sql_grouped rejects DESCRIBE",
),
(
"SHOW INDEXES SessionSqlEntity",
"execute_sql_grouped rejects SHOW INDEXES",
),
(
"SHOW COLUMNS SessionSqlEntity",
"execute_sql_grouped rejects SHOW COLUMNS",
),
("SHOW ENTITIES", "execute_sql_grouped rejects SHOW ENTITIES"),
(
"INSERT INTO SessionSqlEntity (id, name, age) VALUES (1, 'Ada', 21)",
"execute_sql_grouped rejects INSERT",
),
(
"INSERT INTO SessionSqlEntity (name, age) SELECT name, age FROM SessionSqlEntity ORDER BY id ASC LIMIT 1",
"execute_sql_grouped rejects INSERT",
),
(
"UPDATE SessionSqlEntity SET age = 22 WHERE id = 1",
"execute_sql_grouped rejects UPDATE",
),
];
assert_sql_surface_rejects_statement_lanes_with_message(
&grouped_cases,
|sql| session.execute_sql_grouped::<SessionSqlEntity>(sql, None),
"execute_sql_grouped",
);
}
#[test]
fn generated_query_surface_rejects_unsupported_entity_with_supported_list() {
reset_session_sql_store();
let session = sql_session();
let attempt = session.execute_generated_query_surface_sql(
"SELECT * FROM SessionSqlWriteEntity",
&[EntityAuthority::for_type::<SessionSqlEntity>()],
);
let err = attempt
.into_result()
.expect_err("generated query surface should reject entities outside the authority table");
let err_text = err.to_string();
assert!(
err_text.contains("query endpoint does not support entity 'SessionSqlWriteEntity'"),
"generated query surface should name the unsupported entity: {err_text}",
);
assert!(
err_text.contains("supported: SessionSqlEntity"),
"generated query surface should keep the supported-entity list explicit: {err_text}",
);
}
#[test]
fn query_from_sql_projection_lowering_matrix_normalizes_to_scalar_fields() {
reset_session_sql_store();
let session = sql_session();
for (sql, expected_field_names, context) in [
(
"SELECT name, age FROM SessionSqlEntity",
vec!["name".to_string(), "age".to_string()],
"field-list SQL projection",
),
(
"SELECT alias.name \
FROM SessionSqlEntity alias \
WHERE alias.age >= 21 \
ORDER BY alias.age DESC LIMIT 1",
vec!["name".to_string()],
"single-table alias SQL projection",
),
] {
let query = session
.query_from_sql::<SessionSqlEntity>(sql)
.unwrap_or_else(|err| panic!("{context} should lower: {err}"));
let projection = query
.plan()
.unwrap_or_else(|err| panic!("{context} plan should build: {err}"))
.projection_spec();
let field_names = projection
.fields()
.map(|field| match field {
ProjectionField::Scalar {
expr: Expr::Field(field),
alias: None,
} => field.as_str().to_string(),
other @ ProjectionField::Scalar { .. } => {
panic!("{context} should lower to canonical field exprs: {other:?}")
}
})
.collect::<Vec<_>>();
assert_eq!(
field_names, expected_field_names,
"{context} should normalize to scalar field selection",
);
}
}
#[test]
fn sql_surface_computed_text_projection_rejection_matrix_preserves_lane_messages() {
reset_session_sql_store();
let session = sql_session();
let query_err = session
.query_from_sql::<SessionSqlEntity>("SELECT TRIM(name) FROM SessionSqlEntity")
.expect_err(
"query_from_sql should stay on the structural lowered-query lane and reject computed text projection forms",
);
assert!(
query_err
.to_string()
.contains("query_from_sql does not accept computed text projection"),
"query_from_sql should reject computed text projection with an actionable boundary message",
);
let execute_err = session
.execute_sql::<SessionSqlEntity>("SELECT TRIM(name) FROM SessionSqlEntity")
.expect_err("execute_sql should keep computed text projection on the dispatch-owned lane");
assert!(
execute_err
.to_string()
.contains("execute_sql rejects computed text projection"),
"execute_sql should reject computed text projection with an actionable boundary message",
);
}
#[test]
fn sql_statement_route_matrix_classifies_supported_surfaces() {
reset_session_sql_store();
let session = sql_session();
for (sql, expected_route, entity, flags, context) in [
(
"SELECT * FROM SessionSqlEntity ORDER BY age ASC LIMIT 1",
SqlStatementRoute::Query {
entity: "SessionSqlEntity".to_string(),
},
"SessionSqlEntity",
(false, false, false, false, false),
"select SQL statement",
),
(
"INSERT INTO SessionSqlEntity (id, name, age) VALUES (1, 'Ada', 21)",
SqlStatementRoute::Insert {
entity: "SessionSqlEntity".to_string(),
},
"SessionSqlEntity",
(false, false, false, false, false),
"insert SQL statement",
),
(
"UPDATE SessionSqlEntity SET age = 22 WHERE id = 1",
SqlStatementRoute::Update {
entity: "SessionSqlEntity".to_string(),
},
"SessionSqlEntity",
(false, false, false, false, false),
"update SQL statement",
),
(
"DESCRIBE public.SessionSqlEntity",
SqlStatementRoute::Describe {
entity: "public.SessionSqlEntity".to_string(),
},
"public.SessionSqlEntity",
(false, true, false, false, false),
"describe SQL statement",
),
(
"SHOW INDEXES public.SessionSqlEntity",
SqlStatementRoute::ShowIndexes {
entity: "public.SessionSqlEntity".to_string(),
},
"public.SessionSqlEntity",
(false, false, true, false, false),
"show indexes SQL statement",
),
(
"SHOW COLUMNS public.SessionSqlEntity",
SqlStatementRoute::ShowColumns {
entity: "public.SessionSqlEntity".to_string(),
},
"public.SessionSqlEntity",
(false, false, false, true, false),
"show columns SQL statement",
),
(
"SHOW ENTITIES",
SqlStatementRoute::ShowEntities,
"",
(false, false, false, false, true),
"show entities SQL statement",
),
(
"EXPLAIN JSON DELETE FROM SessionSqlEntity WHERE age > 20 LIMIT 1",
SqlStatementRoute::Explain {
entity: "SessionSqlEntity".to_string(),
},
"SessionSqlEntity",
(true, false, false, false, false),
"explain SQL statement",
),
] {
assert_sql_statement_route_case(&session, sql, expected_route, entity, flags, context);
}
}
#[test]
fn execute_sql_rejects_quoted_identifiers_in_reduced_parser() {
reset_session_sql_store();
let session = sql_session();
let err = session
.execute_sql::<SessionSqlEntity>("SELECT \"name\" FROM SessionSqlEntity")
.expect_err("quoted identifiers should be rejected by reduced SQL parser");
assert!(
matches!(
err,
QueryError::Execute(crate::db::query::intent::QueryExecutionError::Unsupported(
_
))
),
"quoted identifiers should fail closed through unsupported SQL boundary",
);
}
#[test]
fn sql_metadata_surfaces_match_typed_payloads() {
reset_session_sql_store();
let session = sql_session();
let describe_from_sql =
dispatch_describe_sql::<SessionSqlEntity>(&session, "DESCRIBE SessionSqlEntity")
.expect("describe_sql should succeed");
let show_indexes_from_sql =
dispatch_show_indexes_sql::<SessionSqlEntity>(&session, "SHOW INDEXES SessionSqlEntity")
.expect("show_indexes_sql should succeed");
let show_columns_from_sql =
dispatch_show_columns_sql::<SessionSqlEntity>(&session, "SHOW COLUMNS SessionSqlEntity")
.expect("show_columns_sql should succeed");
let show_entities_from_sql = dispatch_show_entities_sql(&session, "SHOW ENTITIES")
.expect("show_entities_sql should succeed");
assert_eq!(
describe_from_sql,
session.describe_entity::<SessionSqlEntity>(),
"describe_sql should project through canonical describe_entity payload",
);
assert_eq!(
show_indexes_from_sql,
session.show_indexes::<SessionSqlEntity>(),
"show_indexes_sql should project through canonical show_indexes payload",
);
assert_eq!(
show_columns_from_sql,
session.show_columns::<SessionSqlEntity>(),
"show_columns_sql should project through canonical show_columns payload",
);
assert_eq!(
show_entities_from_sql,
session.show_entities(),
"show_entities_sql should project through canonical show_entities payload",
);
}
#[expect(
clippy::too_many_lines,
reason = "metadata and explain rejection matrix is intentionally tabular"
)]
#[test]
fn sql_metadata_and_explain_surfaces_reject_non_owned_statement_lanes_matrix() {
reset_session_sql_store();
let session = sql_session();
assert_sql_surface_rejects_statement_lanes(
&[
(
"SELECT * FROM SessionSqlEntity",
"describe_sql should reject SELECT statements",
),
(
"EXPLAIN SELECT * FROM SessionSqlEntity",
"describe_sql should reject EXPLAIN statements",
),
(
"SHOW INDEXES SessionSqlEntity",
"describe_sql should reject SHOW INDEXES statements",
),
(
"SHOW COLUMNS SessionSqlEntity",
"describe_sql should reject SHOW COLUMNS statements",
),
(
"SHOW ENTITIES",
"describe_sql should reject SHOW ENTITIES statements",
),
],
|sql| dispatch_describe_sql::<SessionSqlEntity>(&session, sql),
);
assert_sql_surface_rejects_statement_lanes(
&[
(
"SELECT * FROM SessionSqlEntity",
"show_indexes_sql should reject SELECT statements",
),
(
"EXPLAIN SELECT * FROM SessionSqlEntity",
"show_indexes_sql should reject EXPLAIN statements",
),
(
"DESCRIBE SessionSqlEntity",
"show_indexes_sql should reject DESCRIBE statements",
),
(
"SHOW COLUMNS SessionSqlEntity",
"show_indexes_sql should reject SHOW COLUMNS statements",
),
(
"SHOW ENTITIES",
"show_indexes_sql should reject SHOW ENTITIES statements",
),
],
|sql| dispatch_show_indexes_sql::<SessionSqlEntity>(&session, sql),
);
assert_sql_surface_rejects_statement_lanes(
&[
(
"SELECT * FROM SessionSqlEntity",
"show_columns_sql should reject SELECT statements",
),
(
"EXPLAIN SELECT * FROM SessionSqlEntity",
"show_columns_sql should reject EXPLAIN statements",
),
(
"DESCRIBE SessionSqlEntity",
"show_columns_sql should reject DESCRIBE statements",
),
(
"SHOW INDEXES SessionSqlEntity",
"show_columns_sql should reject SHOW INDEXES statements",
),
(
"SHOW ENTITIES",
"show_columns_sql should reject SHOW ENTITIES statements",
),
],
|sql| dispatch_show_columns_sql::<SessionSqlEntity>(&session, sql),
);
assert_sql_surface_rejects_statement_lanes(
&[
(
"SELECT * FROM SessionSqlEntity",
"show_entities_sql should reject SELECT statements",
),
(
"EXPLAIN SELECT * FROM SessionSqlEntity",
"show_entities_sql should reject EXPLAIN statements",
),
(
"DESCRIBE SessionSqlEntity",
"show_entities_sql should reject DESCRIBE statements",
),
(
"SHOW INDEXES SessionSqlEntity",
"show_entities_sql should reject SHOW INDEXES statements",
),
(
"SHOW COLUMNS SessionSqlEntity",
"show_entities_sql should reject SHOW COLUMNS statements",
),
],
|sql| dispatch_show_entities_sql(&session, sql),
);
assert_sql_surface_rejects_statement_lanes(
&[
(
"DESCRIBE SessionSqlEntity",
"explain_sql should reject DESCRIBE statements",
),
(
"SHOW INDEXES SessionSqlEntity",
"explain_sql should reject SHOW INDEXES statements",
),
(
"SHOW COLUMNS SessionSqlEntity",
"explain_sql should reject SHOW COLUMNS statements",
),
(
"SHOW ENTITIES",
"explain_sql should reject SHOW ENTITIES statements",
),
],
|sql| dispatch_explain_sql::<SessionSqlEntity>(&session, sql),
);
}
#[test]
fn sql_surfaces_preserve_unsupported_feature_detail_labels() {
reset_session_sql_store();
let session = sql_session();
assert_sql_surface_preserves_unsupported_feature_detail(|sql| session.sql_statement_route(sql));
assert_sql_surface_preserves_unsupported_feature_detail(|sql| {
session.query_from_sql::<SessionSqlEntity>(sql)
});
assert_sql_surface_preserves_unsupported_feature_detail(|sql| {
session.execute_sql::<SessionSqlEntity>(sql)
});
assert_sql_surface_preserves_unsupported_feature_detail(|sql| {
dispatch_projection_rows::<SessionSqlEntity>(&session, sql)
});
assert_sql_surface_preserves_unsupported_feature_detail(|sql| {
session.execute_sql_grouped::<SessionSqlEntity>(sql, None)
});
assert_sql_surface_preserves_unsupported_feature_detail(|sql| {
session.execute_sql_aggregate::<SessionSqlEntity>(sql)
});
assert_sql_surface_preserves_unsupported_feature_detail(|sql| {
let explain_sql = format!("EXPLAIN {sql}");
dispatch_explain_sql::<SessionSqlEntity>(&session, explain_sql.as_str())
});
assert_specific_sql_unsupported_feature_detail(
"DELETE FROM SessionSqlEntity WHERE STARTS_WITH(TRIM(name), 'Al') ORDER BY age ASC LIMIT 1",
"STARTS_WITH first argument forms beyond plain or LOWER/UPPER field wrappers",
|sql| session.query_from_sql::<SessionSqlEntity>(sql),
);
let sql = "INSERT INTO SessionSqlEntity (name, age) VALUES ('Ada', 21) RETURNING id";
assert_specific_sql_unsupported_feature_detail(sql, "RETURNING", |sql| {
session.query_from_sql::<SessionSqlEntity>(sql)
});
assert_specific_sql_unsupported_feature_detail(sql, "RETURNING", |sql| {
session.execute_sql::<SessionSqlEntity>(sql)
});
assert_specific_sql_unsupported_feature_detail(sql, "RETURNING", |sql| {
dispatch_projection_rows::<SessionSqlEntity>(&session, sql)
});
assert_specific_sql_unsupported_feature_detail(sql, "RETURNING", |sql| {
session.execute_sql_grouped::<SessionSqlEntity>(sql, None)
});
assert_specific_sql_unsupported_feature_detail(sql, "RETURNING", |sql| {
session.execute_sql_aggregate::<SessionSqlEntity>(sql)
});
}