Skip to content

Commit 482b89f

Browse files
authored
Quote reserved words in ON CONFLICT (#425)
1 parent a64ec05 commit 482b89f

File tree

4 files changed

+104
-4
lines changed

4 files changed

+104
-4
lines changed

butane_core/src/db/pg.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ fn change_column(table: &ATable, old: &AColumn, new: &AColumn) -> Result<String>
866866
Ok(result)
867867
}
868868

869+
/// Write SQL that performs an insert or update.
869870
pub fn sql_insert_or_replace_with_placeholders(
870871
table: &str,
871872
columns: &[Column],
@@ -882,13 +883,24 @@ pub fn sql_insert_or_replace_with_placeholders(
882883
n + 1
883884
});
884885
write!(w, ")").unwrap();
885-
write!(w, " ON CONFLICT ({}) DO ", pkcol.name()).unwrap();
886+
write!(
887+
w,
888+
" ON CONFLICT ({}) DO ",
889+
helper::quote_reserved_word(pkcol.name())
890+
)
891+
.unwrap();
886892
if columns.len() > 1 {
887893
write!(w, "UPDATE SET (").unwrap();
888894
helper::list_columns(columns, w);
889895
write!(w, ") = (").unwrap();
890896
columns.iter().fold("", |sep, c| {
891-
write!(w, "{}excluded.{}", sep, c.name()).unwrap();
897+
write!(
898+
w,
899+
"{}excluded.{}",
900+
sep,
901+
helper::quote_reserved_word(c.name())
902+
)
903+
.unwrap();
892904
", "
893905
});
894906
write!(w, ")").unwrap();

butane_core/src/db/sqlite.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -885,7 +885,12 @@ pub fn sql_insert_or_update(table: &str, columns: &[Column], pkcol: &Column, w:
885885
", "
886886
});
887887
write!(w, ")").unwrap();
888-
write!(w, " ON CONFLICT ({}) DO ", pkcol.name()).unwrap();
888+
write!(
889+
w,
890+
" ON CONFLICT ({}) DO ",
891+
helper::quote_reserved_word(pkcol.name())
892+
)
893+
.unwrap();
889894
if columns.len() > 1 {
890895
write!(w, "UPDATE SET (").unwrap();
891896
helper::list_columns(columns, w);

butane_core/src/db/turso.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,12 @@ pub fn sql_insert_or_update(table: &str, columns: &[Column], pkcol: &Column, w:
11851185
", "
11861186
});
11871187
write!(w, ")").unwrap();
1188-
write!(w, " ON CONFLICT ({}) DO ", pkcol.name()).unwrap();
1188+
write!(
1189+
w,
1190+
" ON CONFLICT ({}) DO ",
1191+
helper::quote_reserved_word(pkcol.name())
1192+
)
1193+
.unwrap();
11891194
if columns.len() > 1 {
11901195
write!(w, "UPDATE SET (").unwrap();
11911196
helper::list_columns(columns, w);

butane_core/tests/autopk.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,81 @@ async fn auto_increment(conn: ConnectionAsync) {
8282
assert!(matches!(pk_val2, SqlVal::Int(_)), "Second PK should be Int");
8383
assert_ne!(pk_val, pk_val2, "PKs should be different");
8484
}
85+
86+
/// Test that `insert_or_replace` correctly quotes reserved word primary keys in ON CONFLICT clause.
87+
#[butane_test(nomigrate)]
88+
async fn reserved_word(conn: ConnectionAsync) {
89+
// Create table with "order" as primary key (a reserved SQL keyword)
90+
let pkcol = Column::new("order", SqlType::BigInt);
91+
let bar_column = Column::new("bar", SqlType::Text);
92+
93+
let mut table = ATable::new("reserved_pkey_test".to_string());
94+
95+
// Create "order" column as primary key with auto-increment
96+
let order_col = AColumn::new(
97+
pkcol.name(),
98+
DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::BigInt)),
99+
false, // not nullable
100+
true, // is primary key
101+
true, // auto-increment (this is what we're testing!)
102+
false, // not unique (pk already implies unique)
103+
None, // no default
104+
None, // no foreign key
105+
);
106+
table.add_column(order_col);
107+
108+
// Create bar column
109+
let bar_col = AColumn::new(
110+
bar_column.name(),
111+
DeferredSqlType::KnownId(TypeIdentifier::Ty(SqlType::Text)),
112+
false, // not nullable
113+
false, // not pk
114+
false, // not auto
115+
false, // not unique
116+
None, // no default
117+
None, // no foreign key
118+
);
119+
table.add_column(bar_col);
120+
121+
// Generate and execute CREATE TABLE
122+
let backend = conn.backend();
123+
let adb = ADB::default();
124+
let create_sql = backend
125+
.create_migration_sql(&adb, vec![Operation::AddTable(table)])
126+
.unwrap();
127+
128+
conn.execute(&create_sql).await.unwrap();
129+
130+
// Now test insert_or_replace which should properly quote the "order" column in the ON CONFLICT clause
131+
let columns = [pkcol.clone(), bar_column.clone()];
132+
let values = [SqlValRef::BigInt(1), SqlValRef::Text("first")];
133+
134+
// First insert
135+
conn.insert_or_replace("reserved_pkey_test", &columns, &pkcol, &values)
136+
.await
137+
.unwrap();
138+
139+
// Second insert with same pk should update
140+
let values2 = [SqlValRef::BigInt(1), SqlValRef::Text("updated")];
141+
conn.insert_or_replace("reserved_pkey_test", &columns, &pkcol, &values2)
142+
.await
143+
.unwrap();
144+
145+
// Verify only one row exists after the upsert.
146+
// TODO: There is a bug in turso that fails when using COUNT(anything) on this table.
147+
let query_columns = [pkcol.clone(), bar_column.clone()];
148+
let mut result = conn
149+
.query("reserved_pkey_test", &query_columns, None, None, None, None)
150+
.await
151+
.unwrap();
152+
153+
let mut count = 0;
154+
while result.next().unwrap().is_some() {
155+
count += 1;
156+
}
157+
assert_eq!(
158+
count, 1,
159+
"Expected exactly 1 row after upsert, found {}",
160+
count
161+
);
162+
}

0 commit comments

Comments
 (0)