From c2eb8cb9823beedd43eff0d51b3428ef98373208 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Tue, 26 May 2026 12:20:30 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20ADR-0035=204i(c)=20=E2=80=94=20don't=20p?= =?UTF-8?q?re-flag=20a=20self-referencing=20FK=20parent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/dsl/grammar/sql_create_table.rs | 14 ++++---- src/dsl/walker/mod.rs | 52 ++++++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/dsl/grammar/sql_create_table.rs b/src/dsl/grammar/sql_create_table.rs index 1d0be1c..633f901 100644 --- a/src/dsl/grammar/sql_create_table.rs +++ b/src/dsl/grammar/sql_create_table.rs @@ -150,14 +150,12 @@ pub(crate) static CHECK_NODES: &[Node] = &[ // endpoints; the `( )` is optional (the bare `REFERENCES // ` form resolves to the parent's PK at execution). -// NOTE (4i): `IdentSource::Tables` existence-checks the parent — good -// for the common case (a typo'd parent shows a pre-submit hint), but a -// self-referencing FK (`references ` while creating ``) -// false-flags the not-yet-created table as unknown. Parse + execution -// are correct (the self-ref is validated against the in-statement -// columns); only the live typing indicator is briefly wrong. ADR-0035 -// §13 4i: teach the schema-existence diagnostic about the CREATE TABLE -// target so the self-ref indicator stops lying. +// `IdentSource::Tables` existence-checks the parent, so a typo'd parent +// shows a pre-submit hint. A self-referencing FK (`references ` +// while creating ``) is NOT flagged: the schema-existence +// diagnostic exempts a `Tables` reference matching the `CREATE TABLE` +// target — the table being created in the same statement (ADR-0035 §4i c, +// `schema_existence_diagnostics`'s `created_tables`). const FK_PARENT_TABLE: Node = Node::Ident { source: IdentSource::Tables, role: "fk_parent_table", diff --git a/src/dsl/walker/mod.rs b/src/dsl/walker/mod.rs index 1873fe1..ad789f2 100644 --- a/src/dsl/walker/mod.rs +++ b/src/dsl/walker/mod.rs @@ -652,6 +652,12 @@ fn schema_existence_diagnostics( // (won't false-flag valid refs). let mut bindings: Vec = Vec::new(); let mut cte_names: Vec = 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 = Vec::new(); { let mut pending_alias_index: Option = None; for item in &path.items { @@ -685,6 +691,12 @@ fn schema_existence_diagnostics( } 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; } @@ -871,10 +883,16 @@ fn schema_existence_diagnostics( } } else if !schema_has_table(schema, &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 // 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 { severity: Severity::Error, 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] fn alias_resolves_qualifier() { let schema = two_table_schema();