fix: ADR-0035 4i(c) — don't pre-flag a self-referencing FK parent
A `CREATE TABLE` whose foreign key references the table being created (`create table T (id int primary key, parent_id int references T(id))`) parses and executes correctly, but the pre-submit schema-existence diagnostic flagged the not-yet-created table as "no such table" — the FK parent slot is `IdentSource::Tables`, and the target isn't in the schema yet. schema_existence_diagnostics now collects the CREATE TABLE target(s) (`IdentSource::NewName`, role `table_name`) and exempts a `Tables` reference matching one (case-insensitively) from the unknown-table flag. A FK to a genuinely-unknown *other* table is still flagged. Tests: self-ref FK not flagged; FK to an unknown other table still flagged. Full suite 1915 passing / 0 failing / 1 ignored; clippy clean.
This commit is contained in:
@@ -150,14 +150,12 @@ pub(crate) static CHECK_NODES: &[Node] = &[
|
|||||||
// endpoints; the `( <col> )` is optional (the bare `REFERENCES
|
// endpoints; the `( <col> )` is optional (the bare `REFERENCES
|
||||||
// <parent>` form resolves to the parent's PK at execution).
|
// <parent>` form resolves to the parent's PK at execution).
|
||||||
|
|
||||||
// NOTE (4i): `IdentSource::Tables` existence-checks the parent — good
|
// `IdentSource::Tables` existence-checks the parent, so a typo'd parent
|
||||||
// for the common case (a typo'd parent shows a pre-submit hint), but a
|
// shows a pre-submit hint. A self-referencing FK (`references <self>`
|
||||||
// self-referencing FK (`references <self>` while creating `<self>`)
|
// while creating `<self>`) is NOT flagged: the schema-existence
|
||||||
// false-flags the not-yet-created table as unknown. Parse + execution
|
// diagnostic exempts a `Tables` reference matching the `CREATE TABLE`
|
||||||
// are correct (the self-ref is validated against the in-statement
|
// target — the table being created in the same statement (ADR-0035 §4i c,
|
||||||
// columns); only the live typing indicator is briefly wrong. ADR-0035
|
// `schema_existence_diagnostics`'s `created_tables`).
|
||||||
// §13 4i: teach the schema-existence diagnostic about the CREATE TABLE
|
|
||||||
// target so the self-ref indicator stops lying.
|
|
||||||
const FK_PARENT_TABLE: Node = Node::Ident {
|
const FK_PARENT_TABLE: Node = Node::Ident {
|
||||||
source: IdentSource::Tables,
|
source: IdentSource::Tables,
|
||||||
role: "fk_parent_table",
|
role: "fk_parent_table",
|
||||||
|
|||||||
+51
-1
@@ -652,6 +652,12 @@ fn schema_existence_diagnostics(
|
|||||||
// (won't false-flag valid refs).
|
// (won't false-flag valid refs).
|
||||||
let mut bindings: Vec<PassBinding> = Vec::new();
|
let mut bindings: Vec<PassBinding> = Vec::new();
|
||||||
let mut cte_names: Vec<String> = Vec::new();
|
let mut cte_names: Vec<String> = Vec::new();
|
||||||
|
// Tables being *created* in this statement (a `CREATE TABLE` target —
|
||||||
|
// `IdentSource::NewName`, role `table_name`). A FK that references the
|
||||||
|
// table being created (a self-reference) names it via
|
||||||
|
// `IdentSource::Tables` before it exists in the schema, so it would
|
||||||
|
// otherwise be flagged "no such table" pre-submit (ADR-0035 §4i c).
|
||||||
|
let mut created_tables: Vec<String> = Vec::new();
|
||||||
{
|
{
|
||||||
let mut pending_alias_index: Option<usize> = None;
|
let mut pending_alias_index: Option<usize> = None;
|
||||||
for item in &path.items {
|
for item in &path.items {
|
||||||
@@ -685,6 +691,12 @@ fn schema_existence_diagnostics(
|
|||||||
}
|
}
|
||||||
pending_alias_index = None;
|
pending_alias_index = None;
|
||||||
}
|
}
|
||||||
|
IdentSource::NewName if role == "table_name" => {
|
||||||
|
// The `CREATE TABLE` target — record it so a
|
||||||
|
// self-referencing FK parent isn't flagged unknown.
|
||||||
|
created_tables.push(item.text.clone());
|
||||||
|
pending_alias_index = None;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
pending_alias_index = None;
|
pending_alias_index = None;
|
||||||
}
|
}
|
||||||
@@ -871,10 +883,16 @@ fn schema_existence_diagnostics(
|
|||||||
}
|
}
|
||||||
} else if !schema_has_table(schema, &item.text)
|
} else if !schema_has_table(schema, &item.text)
|
||||||
&& !cte_names_contains(&cte_names, &item.text)
|
&& !cte_names_contains(&cte_names, &item.text)
|
||||||
|
&& !created_tables
|
||||||
|
.iter()
|
||||||
|
.any(|t| t.eq_ignore_ascii_case(&item.text))
|
||||||
{
|
{
|
||||||
// Unknown table — the pre-pass skipped
|
// Unknown table — the pre-pass skipped
|
||||||
// pushing this as a binding, so it's not in
|
// pushing this as a binding, so it's not in
|
||||||
// the resolution scope. Flag it here.
|
// the resolution scope. A self-referencing FK
|
||||||
|
// parent (the table being created in this same
|
||||||
|
// statement) is exempt (ADR-0035 §4i c). Flag
|
||||||
|
// it here.
|
||||||
diagnostics.push(Diagnostic {
|
diagnostics.push(Diagnostic {
|
||||||
severity: Severity::Error,
|
severity: Severity::Error,
|
||||||
span: item.span,
|
span: item.span,
|
||||||
@@ -5396,6 +5414,38 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn self_referencing_fk_in_create_table_is_not_flagged_unknown() {
|
||||||
|
// ADR-0035 §4i (c): a CREATE TABLE whose FK references the table
|
||||||
|
// being created (a self-reference) must not pre-flag the
|
||||||
|
// not-yet-created table as unknown — the FK parent equals the
|
||||||
|
// CREATE target. Empty schema: `T` does not exist yet.
|
||||||
|
let schema = SchemaCache::default();
|
||||||
|
let diags = diag_keys(
|
||||||
|
"create table T (id int primary key, parent_id int references T(id))",
|
||||||
|
&schema,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!diags.iter().any(|d| d.contains("no such table")),
|
||||||
|
"self-ref FK parent must not be flagged unknown; got {diags:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_table_fk_to_genuinely_unknown_table_still_flags() {
|
||||||
|
// The exemption is only for the self-reference: a FK to some
|
||||||
|
// *other* non-existent table is still flagged pre-submit.
|
||||||
|
let schema = SchemaCache::default();
|
||||||
|
let diags = diag_keys(
|
||||||
|
"create table T (id int primary key, ghost_id int references Ghost(id))",
|
||||||
|
&schema,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
diags.iter().any(|d| d.contains("no such table")),
|
||||||
|
"FK to a genuinely-unknown table must still flag; got {diags:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn alias_resolves_qualifier() {
|
fn alias_resolves_qualifier() {
|
||||||
let schema = two_table_schema();
|
let schema = two_table_schema();
|
||||||
|
|||||||
Reference in New Issue
Block a user