use vibesql_executor::{
CreateTableExecutor, DeleteExecutor, ExecutorError, InsertExecutor, UpdateExecutor,
};
use vibesql_parser::Parser;
use vibesql_storage::Database;
use vibesql_types::SqlValue;
fn run_sql(db: &mut Database, sql: &str) {
for sql_stmt in sql.split(';') {
let trimmed = sql_stmt.trim();
if trimmed.is_empty() {
continue;
}
let stmt = Parser::parse_sql(trimmed).expect("Failed to parse SQL");
execute_statement(&stmt, db);
}
}
fn run_update(db: &mut Database, sql: &str) -> Result<usize, ExecutorError> {
let stmt = Parser::parse_sql(sql).expect("Failed to parse UPDATE");
match stmt {
vibesql_ast::Statement::Update(u) => UpdateExecutor::execute(&u, db),
other => panic!("Expected UPDATE statement, got {:?}", other),
}
}
fn execute_statement(stmt: &vibesql_ast::Statement, db: &mut Database) {
use vibesql_ast::Statement;
match stmt {
Statement::CreateTable(s) => {
CreateTableExecutor::execute(s, db).expect("CREATE TABLE failed");
}
Statement::Insert(s) => {
InsertExecutor::execute(db, s).expect("INSERT failed");
}
Statement::Delete(s) => {
DeleteExecutor::execute(s, db).expect("DELETE failed");
}
Statement::Update(s) => {
UpdateExecutor::execute(s, db).expect("UPDATE failed");
}
Statement::CreateIndex(s) => {
vibesql_executor::CreateIndexExecutor::execute(s, db).expect("CREATE INDEX failed");
}
_ => panic!("Unsupported statement type in test helper"),
}
}
fn get_rows(db: &Database, table: &str) -> Vec<Vec<SqlValue>> {
db.get_table(table).expect("table not found").scan().iter().map(|r| r.values.to_vec()).collect()
}
fn get_live_rows(db: &Database, table: &str) -> Vec<Vec<SqlValue>> {
db.get_table(table)
.expect("table not found")
.scan_live()
.map(|(_, r)| r.values.to_vec())
.collect()
}
#[test]
fn pk_shift_decrement_succeeds() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
INSERT INTO p VALUES (0, 'zero'); \
INSERT INTO p VALUES (1, 'one');",
);
let count = run_update(&mut db, "UPDATE p SET a = a - 1").expect(
"UPDATE p SET a = a - 1 should succeed; intermediate UNIQUE collisions must be \
deferred until statement end (issue #5137)",
);
assert_eq!(count, 2);
let rows = get_rows(&db, "p");
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][0], SqlValue::Integer(-1));
assert_eq!(rows[1][0], SqlValue::Integer(0));
}
#[test]
fn ipk_shift_increment_errors_intermediate_collision() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO p VALUES (2, 'two'); \
INSERT INTO p VALUES (3, 'three');",
);
let result = run_update(&mut db, "UPDATE p SET a = a + 1");
let err = result.expect_err(
"IPK ascending +1 cascade must error on intermediate collision (sqlite3 3.51.0)",
);
assert!(
err.to_string().contains("UNIQUE constraint failed: p.a"),
"expected `UNIQUE constraint failed: p.a`, got: {}",
err
);
let pks: Vec<i64> = get_rows(&db, "p")
.iter()
.map(|r| match r[0] {
SqlValue::Integer(n) => n,
_ => panic!("expected integer pk"),
})
.collect();
assert_eq!(pks, vec![1, 2, 3]);
}
#[test]
fn pk_negation_succeeds() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE t(a INTEGER PRIMARY KEY, b TEXT); \
INSERT INTO t VALUES (1, 'one'); \
INSERT INTO t VALUES (2, 'two');",
);
let count = run_update(&mut db, "UPDATE t SET a = -a").expect("PK negation should succeed");
assert_eq!(count, 2);
let rows = get_rows(&db, "t");
let pks: Vec<i64> = rows
.iter()
.map(|r| match r[0] {
SqlValue::Integer(n) => n,
_ => panic!("expected integer pk"),
})
.collect();
assert_eq!(pks, vec![-1, -2]);
}
#[test]
fn composite_pk_shift_succeeds() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE c(a INTEGER, b INTEGER, v TEXT, PRIMARY KEY(a, b)); \
INSERT INTO c VALUES (1, 0, 'r1'); \
INSERT INTO c VALUES (1, 1, 'r2'); \
INSERT INTO c VALUES (1, 2, 'r3');",
);
let count = run_update(&mut db, "UPDATE c SET b = b - 1").expect(
"Composite PK shift on column b should succeed — final keys (1,-1)(1,0)(1,1) are \
unique",
);
assert_eq!(count, 3);
let rows = get_rows(&db, "c");
let pairs: Vec<(i64, i64)> = rows
.iter()
.map(|r| match (&r[0], &r[1]) {
(SqlValue::Integer(a), SqlValue::Integer(b)) => (*a, *b),
_ => panic!("expected integer columns"),
})
.collect();
assert_eq!(pairs, vec![(1, -1), (1, 0), (1, 1)]);
}
#[test]
fn unique_index_shift_succeeds() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE u(id INTEGER PRIMARY KEY, k INTEGER); \
CREATE UNIQUE INDEX u_k ON u(k); \
INSERT INTO u VALUES (1, 10); \
INSERT INTO u VALUES (2, 20); \
INSERT INTO u VALUES (3, 30);",
);
let count = run_update(&mut db, "UPDATE u SET k = k - 10")
.expect("Shift on a UNIQUE-indexed column must succeed when final state is unique");
assert_eq!(count, 3);
let rows = get_rows(&db, "u");
let ks: Vec<i64> = rows
.iter()
.map(|r| match r[1] {
SqlValue::Integer(n) => n,
_ => panic!("expected integer k"),
})
.collect();
assert_eq!(ks, vec![0, 10, 20]);
}
#[test]
fn genuine_collision_still_fails() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE t(id INTEGER PRIMARY KEY, a INTEGER UNIQUE); \
INSERT INTO t VALUES (1, 10); \
INSERT INTO t VALUES (2, 20);",
);
let result = run_update(&mut db, "UPDATE t SET a = 5 WHERE id IN (1, 2)");
assert!(result.is_err(), "Genuine UNIQUE final-state collision must still error");
let msg = format!("{:?}", result.unwrap_err());
assert!(
msg.contains("UNIQUE constraint failed") || msg.contains("multiple rows"),
"Error should mention UNIQUE; got {}",
msg
);
let rows = get_rows(&db, "t");
let avals: Vec<i64> = rows
.iter()
.map(|r| match r[1] {
SqlValue::Integer(n) => n,
_ => panic!(),
})
.collect();
assert_eq!(avals, vec![10, 20]);
}
#[test]
fn collision_with_unmoved_row_still_fails() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE t(a INTEGER PRIMARY KEY, b TEXT); \
INSERT INTO t VALUES (10, 'ten'); \
INSERT INTO t VALUES (20, 'twenty');",
);
let result = run_update(&mut db, "UPDATE t SET a = 10 WHERE a = 20");
assert!(
result.is_err(),
"UPDATE landing on an unmoved row's PK must still produce a UNIQUE error"
);
}
#[test]
fn no_op_assignment_succeeds() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE t(a INTEGER PRIMARY KEY, b TEXT); \
INSERT INTO t VALUES (1, 'one'); \
INSERT INTO t VALUES (2, 'two');",
);
let count = run_update(&mut db, "UPDATE t SET a = a").expect("a = a should succeed");
assert_eq!(count, 2);
let rows = get_rows(&db, "t");
let pks: Vec<i64> = rows
.iter()
.map(|r| match r[0] {
SqlValue::Integer(n) => n,
_ => panic!(),
})
.collect();
assert_eq!(pks, vec![1, 2]);
}
#[test]
fn update_from_pk_shift_succeeds() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE delta(id INTEGER, shift INTEGER); \
INSERT INTO p VALUES (0, 'zero'); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO delta VALUES (0, -1); \
INSERT INTO delta VALUES (1, -1);",
);
let count =
run_update(&mut db, "UPDATE p SET a = a + delta.shift FROM delta WHERE p.a = delta.id")
.expect(
"UPDATE FROM with PK shift driven by joined delta should succeed; intermediate \
UNIQUE collisions must be deferred until statement end (issue #5140)",
);
assert_eq!(count, 2);
let rows = get_rows(&db, "p");
assert_eq!(rows.len(), 2);
assert_eq!(rows[0][0], SqlValue::Integer(-1));
assert_eq!(rows[0][1], SqlValue::Varchar("zero".into()));
assert_eq!(rows[1][0], SqlValue::Integer(0));
assert_eq!(rows[1][1], SqlValue::Varchar("one".into()));
}
#[test]
fn update_from_pk_negation_succeeds() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE t(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE m(id INTEGER, mult INTEGER); \
INSERT INTO t VALUES (1, 'one'); \
INSERT INTO t VALUES (2, 'two'); \
INSERT INTO m VALUES (1, -1); \
INSERT INTO m VALUES (2, -1);",
);
let count = run_update(&mut db, "UPDATE t SET a = t.a * m.mult FROM m WHERE t.a = m.id")
.expect("UPDATE FROM with PK negation should succeed");
assert_eq!(count, 2);
let pks: Vec<i64> = get_rows(&db, "t")
.iter()
.map(|r| match r[0] {
SqlValue::Integer(n) => n,
_ => panic!("expected integer pk"),
})
.collect();
assert_eq!(pks, vec![-1, -2]);
}
#[test]
fn update_from_genuine_collision_still_fails() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE delta(id INTEGER, target INTEGER); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO p VALUES (2, 'two'); \
INSERT INTO delta VALUES (1, 99); \
INSERT INTO delta VALUES (2, 99);",
);
let result =
run_update(&mut db, "UPDATE p SET a = delta.target FROM delta WHERE p.a = delta.id");
assert!(
result.is_err(),
"Genuine UNIQUE final-state collision via UPDATE FROM must still error"
);
let msg = format!("{:?}", result.unwrap_err());
assert!(
msg.contains("UNIQUE constraint failed") || msg.contains("multiple rows"),
"Error should mention UNIQUE; got {}",
msg
);
let pks: Vec<i64> = get_rows(&db, "p")
.iter()
.map(|r| match r[0] {
SqlValue::Integer(n) => n,
_ => panic!(),
})
.collect();
assert_eq!(pks, vec![1, 2]);
}
#[test]
fn update_from_collision_with_unmoved_row_still_fails() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE delta(id INTEGER, target INTEGER); \
INSERT INTO p VALUES (10, 'ten'); \
INSERT INTO p VALUES (20, 'twenty'); \
INSERT INTO delta VALUES (20, 10);",
);
let result =
run_update(&mut db, "UPDATE p SET a = delta.target FROM delta WHERE p.a = delta.id");
assert!(
result.is_err(),
"UPDATE FROM landing on an unmoved row's PK must still produce a UNIQUE error"
);
}
#[test]
fn update_from_unique_index_shift_succeeds() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE u(id INTEGER PRIMARY KEY, k INTEGER); \
CREATE UNIQUE INDEX u_k ON u(k); \
CREATE TABLE delta(id INTEGER, shift INTEGER); \
INSERT INTO u VALUES (1, 10); \
INSERT INTO u VALUES (2, 20); \
INSERT INTO u VALUES (3, 30); \
INSERT INTO delta VALUES (1, -10); \
INSERT INTO delta VALUES (2, -10); \
INSERT INTO delta VALUES (3, -10);",
);
let count =
run_update(&mut db, "UPDATE u SET k = u.k + delta.shift FROM delta WHERE u.id = delta.id")
.expect(
"UPDATE FROM shift on a UNIQUE-indexed column must succeed when final state is unique",
);
assert_eq!(count, 3);
let ks: Vec<i64> = get_rows(&db, "u")
.iter()
.map(|r| match r[1] {
SqlValue::Integer(n) => n,
_ => panic!("expected integer k"),
})
.collect();
assert_eq!(ks, vec![0, 10, 20]);
}
#[test]
fn update_from_no_match_succeeds_unchanged() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE delta(id INTEGER, shift INTEGER); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO p VALUES (2, 'two');",
);
let count =
run_update(&mut db, "UPDATE p SET a = a + delta.shift FROM delta WHERE p.a = delta.id")
.expect("UPDATE FROM with empty join result should succeed with 0 affected rows");
assert_eq!(count, 0);
let pks: Vec<i64> = get_rows(&db, "p")
.iter()
.map(|r| match r[0] {
SqlValue::Integer(n) => n,
_ => panic!(),
})
.collect();
assert_eq!(pks, vec![1, 2]);
}
#[test]
fn update_from_non_pk_change_still_works() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE delta(id INTEGER, label TEXT); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO p VALUES (2, 'two'); \
INSERT INTO delta VALUES (1, 'uno'); \
INSERT INTO delta VALUES (2, 'dos');",
);
let count = run_update(&mut db, "UPDATE p SET b = delta.label FROM delta WHERE p.a = delta.id")
.expect("UPDATE FROM on non-PK column should succeed");
assert_eq!(count, 2);
let labels: Vec<String> = get_rows(&db, "p")
.iter()
.map(|r| match &r[1] {
SqlValue::Varchar(s) => s.to_string(),
_ => panic!(),
})
.collect();
assert_eq!(labels, vec!["uno", "dos"]);
}
#[test]
fn update_from_composite_pk_shift_succeeds() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE c(a INTEGER, b INTEGER, v TEXT, PRIMARY KEY(a, b)); \
CREATE TABLE delta(id INTEGER, shift INTEGER); \
INSERT INTO c VALUES (1, 0, 'r1'); \
INSERT INTO c VALUES (1, 1, 'r2'); \
INSERT INTO c VALUES (1, 2, 'r3'); \
INSERT INTO delta VALUES (0, -1); \
INSERT INTO delta VALUES (1, -1); \
INSERT INTO delta VALUES (2, -1);",
);
let count =
run_update(&mut db, "UPDATE c SET b = c.b + delta.shift FROM delta WHERE c.b = delta.id")
.expect(
"UPDATE FROM composite-PK shift on column b should succeed — final keys \
(1,-1)(1,0)(1,1) are unique even though intermediate states duplicate.",
);
assert_eq!(count, 3);
let rows = get_rows(&db, "c");
let pairs: Vec<(i64, i64)> = rows
.iter()
.map(|r| match (&r[0], &r[1]) {
(SqlValue::Integer(a), SqlValue::Integer(b)) => (*a, *b),
_ => panic!("expected integer columns"),
})
.collect();
assert_eq!(pairs, vec![(1, -1), (1, 0), (1, 1)]);
}
#[test]
fn update_from_or_ignore_skips_pk_collision() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE delta(id INTEGER, target INTEGER); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO p VALUES (2, 'two'); \
INSERT INTO delta VALUES (1, 2);",
);
let count = run_update(
&mut db,
"UPDATE OR IGNORE p SET a = delta.target FROM delta WHERE p.a = delta.id",
)
.expect("UPDATE OR IGNORE ... FROM must skip PK collisions, not error");
assert_eq!(count, 0);
let rows = get_rows(&db, "p");
let pks: Vec<i64> = rows
.iter()
.map(|r| match r[0] {
SqlValue::Integer(n) => n,
_ => panic!(),
})
.collect();
pks.iter().for_each(|p| assert!(*p == 1 || *p == 2));
assert_eq!(pks.len(), 2);
}
#[test]
fn update_from_or_ignore_partial_skip() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE delta(id INTEGER, target INTEGER); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO p VALUES (2, 'two'); \
INSERT INTO p VALUES (3, 'three'); \
INSERT INTO delta VALUES (1, 2); \
INSERT INTO delta VALUES (3, 10);",
);
let count = run_update(
&mut db,
"UPDATE OR IGNORE p SET a = delta.target FROM delta WHERE p.a = delta.id",
)
.expect("UPDATE OR IGNORE ... FROM must skip only colliding rows");
assert_eq!(count, 1, "exactly one (non-colliding) update should apply");
let pks: Vec<i64> = get_rows(&db, "p")
.iter()
.map(|r| match r[0] {
SqlValue::Integer(n) => n,
_ => panic!(),
})
.collect();
let mut sorted = pks.clone();
sorted.sort();
assert_eq!(sorted, vec![1, 2, 10]);
}
#[test]
fn update_from_or_ignore_skips_unique_index_collision() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(id INTEGER PRIMARY KEY, code INTEGER); \
CREATE UNIQUE INDEX idx_p_code ON p(code); \
CREATE TABLE delta(target_id INTEGER, new_code INTEGER); \
INSERT INTO p VALUES (1, 100); \
INSERT INTO p VALUES (2, 200); \
INSERT INTO delta VALUES (1, 200);",
);
let count = run_update(
&mut db,
"UPDATE OR IGNORE p SET code = delta.new_code FROM delta WHERE p.id = delta.target_id",
)
.expect("UPDATE OR IGNORE ... FROM must skip UNIQUE index collisions");
assert_eq!(count, 0);
let codes: Vec<i64> = get_rows(&db, "p")
.iter()
.map(|r| match r[1] {
SqlValue::Integer(n) => n,
_ => panic!(),
})
.collect();
let mut sorted = codes.clone();
sorted.sort();
assert_eq!(sorted, vec![100, 200]);
}
#[test]
fn update_from_or_ignore_skips_not_null_violation() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(id INTEGER PRIMARY KEY, b TEXT NOT NULL); \
CREATE TABLE delta(target_id INTEGER, new_b TEXT); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO p VALUES (2, 'two'); \
INSERT INTO delta VALUES (1, NULL); \
INSERT INTO delta VALUES (2, 'TWO');",
);
let count = run_update(
&mut db,
"UPDATE OR IGNORE p SET b = delta.new_b FROM delta WHERE p.id = delta.target_id",
)
.expect("UPDATE OR IGNORE ... FROM must skip NOT NULL violations");
assert_eq!(count, 1, "only the non-NULL update applies");
let rows = get_rows(&db, "p");
let mut pairs: Vec<(i64, String)> = rows
.iter()
.map(|r| match (&r[0], &r[1]) {
(SqlValue::Integer(i), SqlValue::Varchar(s)) => (*i, s.to_string()),
_ => panic!("unexpected row shape: {:?}", r),
})
.collect();
pairs.sort();
assert_eq!(pairs, vec![(1, "one".to_string()), (2, "TWO".to_string())]);
}
#[test]
fn update_from_or_replace_evicts_colliding_row() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE delta(id INTEGER, target INTEGER); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO p VALUES (2, 'two'); \
INSERT INTO delta VALUES (1, 2);",
);
let count = run_update(
&mut db,
"UPDATE OR REPLACE p SET a = delta.target FROM delta WHERE p.a = delta.id",
)
.expect("UPDATE OR REPLACE ... FROM must evict colliding rows and apply update");
assert_eq!(count, 1, "one update applied");
let rows = get_live_rows(&db, "p");
let live: Vec<(i64, String)> = rows
.iter()
.map(|r| match (&r[0], &r[1]) {
(SqlValue::Integer(i), SqlValue::Varchar(s)) => (*i, s.to_string()),
_ => panic!("unexpected row shape: {:?}", r),
})
.collect();
assert_eq!(live, vec![(2, "one".to_string())]);
}
#[test]
fn update_from_or_replace_cross_update_collision() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY, b TEXT); \
CREATE TABLE delta(id INTEGER, target INTEGER); \
INSERT INTO p VALUES (1, 'one'); \
INSERT INTO p VALUES (2, 'two'); \
INSERT INTO delta VALUES (1, 99); \
INSERT INTO delta VALUES (2, 99);",
);
let count = run_update(
&mut db,
"UPDATE OR REPLACE p SET a = delta.target FROM delta WHERE p.a = delta.id",
)
.expect("UPDATE OR REPLACE ... FROM must resolve cross-update collisions");
assert_eq!(count, 1, "only the surviving update is counted");
let live: Vec<(i64, String)> = get_live_rows(&db, "p")
.iter()
.map(|r| match (&r[0], &r[1]) {
(SqlValue::Integer(i), SqlValue::Varchar(s)) => (*i, s.to_string()),
_ => panic!("unexpected row shape: {:?}", r),
})
.collect();
assert_eq!(live.len(), 1, "exactly one row survives the cross-update collision");
assert_eq!(live[0].0, 99);
}
#[test]
fn update_from_or_ignore_all_skipped_returns_zero() {
let mut db = Database::new();
run_sql(
&mut db,
"CREATE TABLE p(a INTEGER PRIMARY KEY); \
CREATE TABLE delta(id INTEGER, target INTEGER); \
INSERT INTO p VALUES (1); \
INSERT INTO p VALUES (2); \
INSERT INTO delta VALUES (1, 2); \
INSERT INTO delta VALUES (2, 1);",
);
let count = run_update(
&mut db,
"UPDATE OR IGNORE p SET a = delta.target FROM delta WHERE p.a = delta.id",
)
.expect("UPDATE OR IGNORE ... FROM with all-skip must return 0, not error");
assert_eq!(count, 0);
let pks: Vec<i64> = get_rows(&db, "p")
.iter()
.map(|r| match r[0] {
SqlValue::Integer(n) => n,
_ => panic!(),
})
.collect();
let mut sorted = pks.clone();
sorted.sort();
assert_eq!(sorted, vec![1, 2]);
}