Skip to content

Commit fe2c2c8

Browse files
committed
Fix sqlite fk dup issue
1 parent 1aa4be1 commit fe2c2c8

2 files changed

Lines changed: 69 additions & 7 deletions

File tree

crates/vespertide-query/src/snapshots/vespertide_query__builder__tests__add_column_with_fk_no_duplicate_fk_in_temp_table@dup_fk_sqlite.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ CREATE UNIQUE INDEX "uq_companion__invite_code" ON "companion" ("invite_code");
1111
CREATE INDEX "ix_companion__user_id" ON "companion" ("user_id")
1212

1313
-- Action 1: AddConstraint { table: "companion", constraint: ForeignKey { name: None, columns: ["project_id"], ref_table: "project", ref_columns: ["id"], on_delete: Some(Cascade), on_update: None } }
14-
CREATE TABLE "companion_temp" ( "id" integer, "user_id" bigint, "project_id" bigint NOT NULL, FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE, FOREIGN KEY ("project_id") REFERENCES "project" ("id"), FOREIGN KEY ("project_id") REFERENCES "project" ("id") ON DELETE CASCADE );
14+
CREATE TABLE "companion_temp" ( "id" integer, "user_id" bigint, "project_id" bigint NOT NULL, FOREIGN KEY ("user_id") REFERENCES "user" ("id") ON DELETE CASCADE, FOREIGN KEY ("project_id") REFERENCES "project" ("id") ON DELETE CASCADE );
1515
INSERT INTO "companion_temp" ("id", "user_id", "project_id") SELECT "id", "user_id", "project_id" FROM "companion";
1616
DROP TABLE "companion";
1717
ALTER TABLE "companion_temp" RENAME TO "companion";

crates/vespertide-query/src/sql/add_constraint.rs

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,71 @@ use vespertide_core::{TableConstraint, TableDef};
55
use super::helpers::{
66
build_sqlite_temp_table_create, recreate_indexes_after_rebuild, to_sea_fk_action,
77
};
8+
9+
/// Build new_constraints by cloning `table_def.constraints` and adding the
10+
/// given `constraint`. If an equivalent constraint already exists (e.g.
11+
/// promoted by AddColumn normalization), it is **replaced** so that the
12+
/// explicit AddConstraint version (which may carry ON DELETE / ON UPDATE)
13+
/// wins, without producing duplicates.
14+
fn merge_constraint(
15+
existing: &[TableConstraint],
16+
constraint: &TableConstraint,
17+
) -> Vec<TableConstraint> {
18+
let mut out: Vec<TableConstraint> = Vec::with_capacity(existing.len() + 1);
19+
let mut replaced = false;
20+
21+
for c in existing {
22+
if constraints_overlap(c, constraint) {
23+
// Replace the existing (possibly weaker) constraint with the new one.
24+
if !replaced {
25+
out.push(constraint.clone());
26+
replaced = true;
27+
}
28+
// else: skip additional duplicates
29+
} else {
30+
out.push(c.clone());
31+
}
32+
}
33+
34+
if !replaced {
35+
out.push(constraint.clone());
36+
}
37+
out
38+
}
39+
40+
/// Two constraints "overlap" when they are the same variant and target the
41+
/// same columns, even if their details (name, on_delete, …) differ.
42+
fn constraints_overlap(a: &TableConstraint, b: &TableConstraint) -> bool {
43+
match (a, b) {
44+
(
45+
TableConstraint::ForeignKey {
46+
columns: a_cols, ..
47+
},
48+
TableConstraint::ForeignKey {
49+
columns: b_cols, ..
50+
},
51+
) => a_cols == b_cols,
52+
(
53+
TableConstraint::PrimaryKey {
54+
columns: a_cols, ..
55+
},
56+
TableConstraint::PrimaryKey {
57+
columns: b_cols, ..
58+
},
59+
) => a_cols == b_cols,
60+
(
61+
TableConstraint::Check {
62+
name: a_name,
63+
expr: a_expr,
64+
},
65+
TableConstraint::Check {
66+
name: b_name,
67+
expr: b_expr,
68+
},
69+
) => a_name == b_name && a_expr == b_expr,
70+
_ => false,
71+
}
72+
}
873
use super::rename_table::build_rename_table;
974
use super::types::{BuiltQuery, DatabaseBackend, RawSql};
1075
use crate::error::QueryError;
@@ -24,8 +89,7 @@ pub fn build_add_constraint(
2489
let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to add constraints.", table)))?;
2590

2691
// Create new constraints with the added primary key constraint
27-
let mut new_constraints: Vec<TableConstraint> = table_def.constraints.clone();
28-
new_constraints.push(constraint.clone());
92+
let new_constraints = merge_constraint(&table_def.constraints, constraint);
2993

3094
let temp_table = format!("{}_temp", table);
3195

@@ -125,8 +189,7 @@ pub fn build_add_constraint(
125189
let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to add constraints.", table)))?;
126190

127191
// Create new constraints with the added foreign key constraint
128-
let mut new_constraints = table_def.constraints.clone();
129-
new_constraints.push(constraint.clone());
192+
let new_constraints = merge_constraint(&table_def.constraints, constraint);
130193

131194
let temp_table = format!("{}_temp", table);
132195

@@ -218,8 +281,7 @@ pub fn build_add_constraint(
218281
let table_def = current_schema.iter().find(|t| t.name == table).ok_or_else(|| QueryError::Other(format!("Table '{}' not found in current schema. SQLite requires current schema information to add constraints.", table)))?;
219282

220283
// Create new constraints with the added check constraint
221-
let mut new_constraints = table_def.constraints.clone();
222-
new_constraints.push(constraint.clone());
284+
let new_constraints = merge_constraint(&table_def.constraints, constraint);
223285

224286
let temp_table = format!("{}_temp", table);
225287

0 commit comments

Comments
 (0)