test: consolidate 25 integration crates into one it binary
Each top-level tests/*.rs was its own crate → its own binary, each statically linking the bundled engine + every dep. 26 of them, so an edit to the lib relinked all 26. Moved the 25 standalone files into tests/it/ under one tests/it/main.rs (the pattern typing_surface already uses); cargo auto-detects it as the `it` target. End state: 2 integration-test binaries instead of 26. Result: target/debug/deps 1.5 GB → 629 MB (-58%). Build time barely moved (clean 22.9s→22.4s, lib-edit relink 13.3s→12.4s) — wall-clock is dominated by compiling, not linking, so this is a disk win, not a speed win (see docs/plans/20260602-test-consolidation.md). Tests unchanged at 2151/0/1; clippy clean; no fixups needed. typing_surface_matrix stays its own already-consolidated binary. Tradeoff: the 25 files now share one crate (a compile error fails the whole `it` binary; module-scoped namespaces, no clashes) — negligible for a solo project.
This commit is contained in:
@@ -0,0 +1,694 @@
|
||||
//! Integration tests for `runtime::enrich_dsl_failure`
|
||||
//! (ADR-0019 §6).
|
||||
//!
|
||||
//! Each test:
|
||||
//! 1. Bootstraps a real `Database` (in-memory).
|
||||
//! 2. Constructs the schema/data needed to trigger one
|
||||
//! class of engine error.
|
||||
//! 3. Provokes the failure through the public Database API,
|
||||
//! capturing the resulting `DbError`.
|
||||
//! 4. Calls `enrich_dsl_failure` and asserts the
|
||||
//! `FailureContext` carries the schema-resolved facts a
|
||||
//! learner would expect to see in the rendered error.
|
||||
//!
|
||||
//! Pinpoint diagnostic-table presence is verified for the
|
||||
//! UNIQUE INSERT case (the most pedagogically valuable
|
||||
//! pinpoint today).
|
||||
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
use rdbms_playground::db::{Database, DbError, SqliteErrorKind};
|
||||
use rdbms_playground::dsl::{
|
||||
action::ReferentialAction, ColumnSpec, Command, RowFilter, Type, Value,
|
||||
};
|
||||
use rdbms_playground::dsl::parser::parse_command;
|
||||
use rdbms_playground::runtime::enrich_dsl_failure;
|
||||
|
||||
fn rt() -> Runtime {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("tokio rt")
|
||||
}
|
||||
|
||||
fn db() -> Database {
|
||||
Database::open(":memory:").expect("open in-memory db")
|
||||
}
|
||||
|
||||
// ---- UNIQUE -----------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn enrich_unique_insert_resolves_table_column_value_and_pinpoint() {
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
// Create a table with a serial PK; insert a row; insert
|
||||
// again with the same PK value to trigger UNIQUE.
|
||||
db.create_table(
|
||||
"Customers".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id".to_string(), Type::Int),
|
||||
ColumnSpec::new("name".to_string(), Type::Text),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert(
|
||||
"Customers".to_string(),
|
||||
None,
|
||||
vec![Value::Number("5".to_string()), Value::Text("Alice".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Second insert with the same PK — UNIQUE violation.
|
||||
let cmd = Command::Insert {
|
||||
table: "Customers".to_string(),
|
||||
columns: Some(vec!["id".to_string(), "name".to_string()]),
|
||||
values: vec![
|
||||
Value::Number("5".to_string()),
|
||||
Value::Text("Bob".to_string()),
|
||||
],
|
||||
};
|
||||
let err = db
|
||||
.insert(
|
||||
"Customers".to_string(),
|
||||
Some(vec!["id".to_string(), "name".to_string()]),
|
||||
vec![
|
||||
Value::Number("5".to_string()),
|
||||
Value::Text("Bob".to_string()),
|
||||
],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
DbError::Sqlite { kind: SqliteErrorKind::UniqueViolation, .. }
|
||||
));
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.table.as_deref(), Some("Customers"));
|
||||
assert_eq!(facts.column.as_deref(), Some("id"));
|
||||
assert_eq!(facts.value.as_deref(), Some("5"));
|
||||
// Pinpoint: existing row with id=5 should be present.
|
||||
let table = facts.diagnostic_table.expect("UNIQUE pinpoint expected");
|
||||
assert_eq!(table.headers, vec!["id".to_string(), "name".to_string()]);
|
||||
assert_eq!(table.rows.len(), 1);
|
||||
assert_eq!(table.rows[0][0], "5");
|
||||
assert_eq!(table.rows[0][1], "Alice");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrich_unique_insert_natural_order_short_form_resolves_value_via_schema() {
|
||||
// `insert into T (1)` — natural-order short form, the
|
||||
// helper falls back to schema-driven lookup.
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
db.create_table(
|
||||
"thing".to_string(),
|
||||
vec![ColumnSpec::new("id".to_string(), Type::Int)],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert(
|
||||
"thing".to_string(),
|
||||
None,
|
||||
vec![Value::Number("1".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let cmd = Command::Insert {
|
||||
table: "thing".to_string(),
|
||||
columns: None,
|
||||
values: vec![Value::Number("1".to_string())],
|
||||
};
|
||||
let err = db
|
||||
.insert(
|
||||
"thing".to_string(),
|
||||
None,
|
||||
vec![Value::Number("1".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.value.as_deref(), Some("1"));
|
||||
assert!(facts.diagnostic_table.is_some());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrich_unique_sql_insert_natural_order_resolves_value_via_schema() {
|
||||
// ADR-0036 Phase 1 follow-up: a no-column-list (natural-order) SQL
|
||||
// INSERT also names the offending value in a constraint error. The
|
||||
// schema maps each VALUES position to its column, in declaration
|
||||
// order — ALL columns (advanced-mode Form B auto-fills nothing, so
|
||||
// the user supplies a value for every column).
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
db.create_table(
|
||||
"Customers".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id".to_string(), Type::Int),
|
||||
ColumnSpec::new("name".to_string(), Type::Text),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert(
|
||||
"Customers".to_string(),
|
||||
None,
|
||||
vec![Value::Number("5".to_string()), Value::Text("Alice".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Natural-order SQL insert (no column list) collides on id=5.
|
||||
let input = "insert into Customers values (5, 'Bob')";
|
||||
let cmd = parse_command(input).expect("parses as advanced-mode SQL insert");
|
||||
let Command::SqlInsert {
|
||||
sql,
|
||||
target_table,
|
||||
listed_columns,
|
||||
row_source,
|
||||
returning,
|
||||
literal_rows,
|
||||
} = cmd.clone()
|
||||
else {
|
||||
panic!("expected Command::SqlInsert, got {cmd:?}");
|
||||
};
|
||||
assert!(listed_columns.is_empty(), "natural-order form has no column list");
|
||||
let err = db
|
||||
.run_sql_insert_with_literals(
|
||||
sql,
|
||||
None,
|
||||
target_table,
|
||||
listed_columns,
|
||||
row_source,
|
||||
returning,
|
||||
literal_rows,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
DbError::Sqlite { kind: SqliteErrorKind::UniqueViolation, .. }
|
||||
));
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.column.as_deref(), Some("id"));
|
||||
assert_eq!(
|
||||
facts.value.as_deref(),
|
||||
Some("5"),
|
||||
"the offending value is named even without an explicit column list",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrich_unique_update_resolves_value_from_assignments() {
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
db.create_table(
|
||||
"Customers".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id".to_string(), Type::Int),
|
||||
ColumnSpec::new("name".to_string(), Type::Text),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert(
|
||||
"Customers".to_string(),
|
||||
None,
|
||||
vec![Value::Number("1".to_string()), Value::Text("Alice".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert(
|
||||
"Customers".to_string(),
|
||||
None,
|
||||
vec![Value::Number("2".to_string()), Value::Text("Bob".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to update Bob's id to 1 — collides with Alice.
|
||||
let cmd = Command::Update {
|
||||
table: "Customers".to_string(),
|
||||
assignments: vec![("id".to_string(), Value::Number("1".to_string()))],
|
||||
filter: RowFilter::eq("name", Value::Text("Bob".to_string())),
|
||||
};
|
||||
let err = db
|
||||
.update(
|
||||
"Customers".to_string(),
|
||||
vec![("id".to_string(), Value::Number("1".to_string()))],
|
||||
RowFilter::eq("name", Value::Text("Bob".to_string())),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.column.as_deref(), Some("id"));
|
||||
assert_eq!(facts.value.as_deref(), Some("1"));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrich_unique_sql_update_resolves_value_from_set_literals() {
|
||||
// ADR-0036 Phase 2: an advanced-mode SQL `UPDATE` now retains its
|
||||
// `SET` literals, so a UNIQUE violation names the offending value —
|
||||
// closing the error-value gap for advanced mode, mirroring the DSL
|
||||
// `Update` case above. The value flows from the parse-captured
|
||||
// `set_literals` through `user_value_for_column`.
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
db.create_table(
|
||||
"Customers".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id".to_string(), Type::Int),
|
||||
ColumnSpec::new("name".to_string(), Type::Text),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert(
|
||||
"Customers".to_string(),
|
||||
None,
|
||||
vec![Value::Number("1".to_string()), Value::Text("Alice".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert(
|
||||
"Customers".to_string(),
|
||||
None,
|
||||
vec![Value::Number("2".to_string()), Value::Text("Bob".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Advanced-mode SQL: set Bob's id to 1 — collides with Alice.
|
||||
let input = "update Customers set id = 1 where name = 'Bob'";
|
||||
let cmd = parse_command(input).expect("parses as advanced-mode SQL update");
|
||||
let Command::SqlUpdate {
|
||||
sql,
|
||||
target_table,
|
||||
returning,
|
||||
set_literals,
|
||||
} = cmd.clone()
|
||||
else {
|
||||
panic!("expected Command::SqlUpdate, got {cmd:?}");
|
||||
};
|
||||
// The literal `1` is a valid int, so Phase-2 validation passes and
|
||||
// the engine-level UNIQUE violation is what surfaces.
|
||||
let err = db
|
||||
.run_sql_update_with_literals(sql, None, target_table, returning, set_literals)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
DbError::Sqlite { kind: SqliteErrorKind::UniqueViolation, .. }
|
||||
));
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.column.as_deref(), Some("id"));
|
||||
assert_eq!(
|
||||
facts.value.as_deref(),
|
||||
Some("1"),
|
||||
"the offending SET value is named (from set_literals)"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- NOT NULL ---------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn enrich_not_null_resolves_table_and_column() {
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
// Create a table with a NOT NULL column. The current
|
||||
// schema_to_ddl emits NOT NULL on PK columns; make
|
||||
// a non-PK column NOT NULL via a multi-column PK
|
||||
// setup, then the second column is NOT NULL because
|
||||
// it's part of the PK.
|
||||
// (We're testing the enrichment, not the constraint
|
||||
// emission — even a PK NOT NULL works.)
|
||||
db.create_table(
|
||||
"T".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("a".to_string(), Type::Int),
|
||||
ColumnSpec::new("b".to_string(), Type::Text),
|
||||
],
|
||||
vec!["a".to_string(), "b".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Try to insert with NULL for the second PK column.
|
||||
let cmd = Command::Insert {
|
||||
table: "T".to_string(),
|
||||
columns: Some(vec!["a".to_string(), "b".to_string()]),
|
||||
values: vec![Value::Number("1".to_string()), Value::Null],
|
||||
};
|
||||
let err = db
|
||||
.insert(
|
||||
"T".to_string(),
|
||||
Some(vec!["a".to_string(), "b".to_string()]),
|
||||
vec![Value::Number("1".to_string()), Value::Null],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.table.as_deref(), Some("T"));
|
||||
assert_eq!(facts.column.as_deref(), Some("b"));
|
||||
// Per design: no value field for NOT NULL (the value is null).
|
||||
assert!(facts.value.is_none());
|
||||
// No pinpoint for NOT NULL.
|
||||
assert!(facts.diagnostic_table.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
// ---- FOREIGN KEY (child-side, INSERT) ---------------------------
|
||||
|
||||
#[test]
|
||||
fn enrich_fk_insert_resolves_parent_table_column_and_value() {
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
db.create_table(
|
||||
"Customers".to_string(),
|
||||
vec![ColumnSpec::new("id".to_string(), Type::Int)],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.create_table(
|
||||
"Orders".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id".to_string(), Type::Int),
|
||||
ColumnSpec::new("CustId".to_string(), Type::Int),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_relationship(
|
||||
None,
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Insert into Orders with a CustId that has no parent.
|
||||
let cmd = Command::Insert {
|
||||
table: "Orders".to_string(),
|
||||
columns: Some(vec!["id".to_string(), "CustId".to_string()]),
|
||||
values: vec![
|
||||
Value::Number("1".to_string()),
|
||||
Value::Number("999".to_string()),
|
||||
],
|
||||
};
|
||||
let err = db
|
||||
.insert(
|
||||
"Orders".to_string(),
|
||||
Some(vec!["id".to_string(), "CustId".to_string()]),
|
||||
vec![
|
||||
Value::Number("1".to_string()),
|
||||
Value::Number("999".to_string()),
|
||||
],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.table.as_deref(), Some("Orders"));
|
||||
assert_eq!(facts.column.as_deref(), Some("CustId"));
|
||||
assert_eq!(facts.parent_table.as_deref(), Some("Customers"));
|
||||
assert_eq!(facts.parent_column.as_deref(), Some("id"));
|
||||
assert_eq!(facts.value.as_deref(), Some("999"));
|
||||
// FK pinpoint not implemented in v1.
|
||||
assert!(facts.diagnostic_table.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enrich_fk_insert_natural_order_multi_value_resolves_via_schema() {
|
||||
// Regression: `insert into Orders values (4, 11.99)` —
|
||||
// natural-order multi-value INSERT, no explicit columns,
|
||||
// and the schema has a serial PK that gets auto-skipped.
|
||||
// Enrichment must still resolve parent_table /
|
||||
// parent_column / value via the schema-aware lookup.
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
db.create_table(
|
||||
"Customers".to_string(),
|
||||
vec![ColumnSpec::new("id".to_string(), Type::Int)],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.create_table(
|
||||
"Orders".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id".to_string(), Type::Serial),
|
||||
ColumnSpec::new("CustId".to_string(), Type::Int),
|
||||
ColumnSpec::new("Total".to_string(), Type::Real),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_relationship(
|
||||
None,
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Natural-order: serial PK auto-fills, so positional
|
||||
// values map to (CustId, Total). CustId=4 has no
|
||||
// matching parent → FK violation.
|
||||
let cmd = Command::Insert {
|
||||
table: "Orders".to_string(),
|
||||
columns: None,
|
||||
values: vec![
|
||||
Value::Number("4".to_string()),
|
||||
Value::Number("11.99".to_string()),
|
||||
],
|
||||
};
|
||||
let err = db
|
||||
.insert(
|
||||
"Orders".to_string(),
|
||||
None,
|
||||
vec![
|
||||
Value::Number("4".to_string()),
|
||||
Value::Number("11.99".to_string()),
|
||||
],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.parent_table.as_deref(), Some("Customers"));
|
||||
assert_eq!(facts.parent_column.as_deref(), Some("id"));
|
||||
assert_eq!(
|
||||
facts.value.as_deref(),
|
||||
Some("4"),
|
||||
"natural-order with serial PK skip should map values[0] to CustId"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- FOREIGN KEY (parent-side, DELETE) --------------------------
|
||||
|
||||
#[test]
|
||||
fn enrich_fk_delete_resolves_child_table() {
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
db.create_table(
|
||||
"Customers".to_string(),
|
||||
vec![ColumnSpec::new("id".to_string(), Type::Int)],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.create_table(
|
||||
"Orders".to_string(),
|
||||
vec![
|
||||
ColumnSpec::new("id".to_string(), Type::Int),
|
||||
ColumnSpec::new("CustId".to_string(), Type::Int),
|
||||
],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.add_relationship(
|
||||
None,
|
||||
"Customers".to_string(),
|
||||
"id".to_string(),
|
||||
"Orders".to_string(),
|
||||
"CustId".to_string(),
|
||||
ReferentialAction::NoAction,
|
||||
ReferentialAction::NoAction,
|
||||
false,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert(
|
||||
"Customers".to_string(),
|
||||
None,
|
||||
vec![Value::Number("1".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
db.insert(
|
||||
"Orders".to_string(),
|
||||
None,
|
||||
vec![Value::Number("1".to_string()), Value::Number("1".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Delete the parent that has children — engine refuses.
|
||||
let cmd = Command::Delete {
|
||||
table: "Customers".to_string(),
|
||||
filter: RowFilter::eq("id", Value::Number("1".to_string())),
|
||||
};
|
||||
let err = db
|
||||
.delete(
|
||||
"Customers".to_string(),
|
||||
RowFilter::eq("id", Value::Number("1".to_string())),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.table.as_deref(), Some("Customers"));
|
||||
assert_eq!(facts.child_table.as_deref(), Some("Orders"));
|
||||
});
|
||||
}
|
||||
|
||||
// ---- CHECK (ADR-0029 §10) ---------------------------------------
|
||||
|
||||
#[test]
|
||||
fn enrich_check_insert_resolves_table_column_value_and_rule() {
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
// `Scores(id serial pk)` plus a non-PK `score` column
|
||||
// carrying `CHECK (score >= 0)`.
|
||||
db.create_table(
|
||||
"Scores".to_string(),
|
||||
vec![ColumnSpec::new("id".to_string(), Type::Serial)],
|
||||
vec!["id".to_string()],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let score_spec = match parse_command(
|
||||
"create table __probe with pk score(int) check (score >= 0)",
|
||||
)
|
||||
.expect("probe create parses")
|
||||
{
|
||||
Command::CreateTable { columns, .. } => {
|
||||
columns.into_iter().next().expect("one column")
|
||||
}
|
||||
other => panic!("expected CreateTable, got {other:?}"),
|
||||
};
|
||||
db.add_column("Scores".to_string(), score_spec, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// An insert that violates the CHECK.
|
||||
let cmd = Command::Insert {
|
||||
table: "Scores".to_string(),
|
||||
columns: Some(vec!["score".to_string()]),
|
||||
values: vec![Value::Number("-5".to_string())],
|
||||
};
|
||||
let err = db
|
||||
.insert(
|
||||
"Scores".to_string(),
|
||||
Some(vec!["score".to_string()]),
|
||||
vec![Value::Number("-5".to_string())],
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert_eq!(facts.table.as_deref(), Some("Scores"));
|
||||
assert_eq!(facts.column.as_deref(), Some("score"));
|
||||
assert_eq!(facts.value.as_deref(), Some("-5"));
|
||||
let rule = facts.check_rule.expect("the CHECK rule is resolved");
|
||||
assert!(
|
||||
rule.contains("score"),
|
||||
"the resolved rule names the column: {rule}",
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ---- non-engine error → empty enrichment ------------------------
|
||||
|
||||
#[test]
|
||||
fn enrich_unsupported_returns_default_facts() {
|
||||
let db = db();
|
||||
rt().block_on(async {
|
||||
let err = DbError::Unsupported("nope".to_string());
|
||||
let cmd = Command::DropTable { name: "X".to_string() };
|
||||
let facts = enrich_dsl_failure(&db, &cmd, &err).await;
|
||||
assert!(facts.table.is_none());
|
||||
assert!(facts.column.is_none());
|
||||
assert!(facts.value.is_none());
|
||||
assert!(facts.parent_table.is_none());
|
||||
assert!(facts.child_table.is_none());
|
||||
assert!(facts.diagnostic_table.is_none());
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user