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:
claude@clouddev1
2026-05-26 12:20:30 +00:00
parent f85261032d
commit c2eb8cb982
2 changed files with 57 additions and 9 deletions
+51 -1
View File
@@ -652,6 +652,12 @@ fn schema_existence_diagnostics(
// (won't false-flag valid refs).
let mut bindings: Vec<PassBinding> = 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;
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();