#!/usr/bin/env tclsh set testdir [file dirname $argv0] source $testdir/tester.tcl do_execsql_test_on_specific_db {:memory:} upsert-pk-update { CREATE TABLE t (id INTEGER PRIMARY KEY, name); INSERT INTO t VALUES (1,'old'); INSERT INTO t VALUES (1,'new') ON CONFLICT DO UPDATE SET name = excluded.name; SELECT * FROM t; } {1|new} do_execsql_test_on_specific_db {:memory:} upsert-pk-do-nothing { CREATE TABLE t (id INTEGER PRIMARY KEY, name); INSERT INTO t VALUES (1,'new'); INSERT INTO t VALUES (1,'ignored') ON CONFLICT DO NOTHING; SELECT * FROM t; } {1|new} do_execsql_test_on_specific_db {:memory:} upsert-unique-update { CREATE TABLE u (a, b, c); CREATE UNIQUE INDEX u_a ON u(a); INSERT INTO u VALUES (1,10,100); INSERT INTO u VALUES (1,20,200) ON CONFLICT(a) DO UPDATE SET b = excluded.b, c = excluded.c; SELECT * FROM u; } {1|20|200} do_execsql_test_on_specific_db {:memory:} upsert-unique-do-nothing { CREATE TABLE u (a, b, c); CREATE UNIQUE INDEX u_a ON u(a); INSERT INTO u VALUES (1,10,100); INSERT INTO u VALUES (2,30,300) ON CONFLICT(a) DO NOTHING; SELECT * FROM u ORDER BY a; } {1|10|100 2|30|300} do_execsql_test_on_specific_db {:memory:} upsert-where-guard-no-change { CREATE TABLE g (a UNIQUE, b); INSERT INTO g VALUES (1,'x'); INSERT INTO g VALUES (1,'y') ON CONFLICT(a) DO UPDATE SET b = excluded.b WHERE b IS NULL; SELECT * FROM g; } {1|x} do_execsql_test_on_specific_db {:memory:} upsert-where-guard-apply { CREATE TABLE g (a UNIQUE, b); INSERT INTO g VALUES (1,NULL); INSERT INTO g VALUES (1,'y') ON CONFLICT(a) DO UPDATE SET b = excluded.b WHERE b IS NULL; SELECT * FROM g; } {1|y} do_execsql_test_on_specific_db {:memory:} upsert-selfref-and-excluded { CREATE TABLE s (a UNIQUE, b, c); INSERT INTO s VALUES (1,10,'old'); INSERT INTO s VALUES (1,99,'new') ON CONFLICT(a) DO UPDATE SET b = b + 1, c = excluded.c; SELECT * FROM s; } {1|11|new} do_execsql_test_on_specific_db {:memory:} upsert-values-mixed-insert-update { CREATE TABLE m (a UNIQUE, b); INSERT INTO m VALUES (1,'one'); INSERT INTO m VALUES (1,'uno'), (2,'dos') ON CONFLICT(a) DO UPDATE SET b = excluded.b; SELECT * FROM m ORDER BY a; } {1|uno 2|dos} do_execsql_test_on_specific_db {:memory:} upsert-select-single { CREATE TABLE s1 (a UNIQUE, b); INSERT INTO s1 VALUES (1,'old'); INSERT INTO s1 SELECT 1,'NEW' ON CONFLICT(a) DO UPDATE SET b = excluded.b; SELECT * FROM s1; } {1|NEW} do_execsql_test_on_specific_db {:memory:} upsert-composite-target-orderless { CREATE TABLE c (a, b, val); CREATE UNIQUE INDEX c_ab ON c(a,b); INSERT INTO c VALUES (1,1,'x'); INSERT INTO c VALUES (1,1,'y') ON CONFLICT(b,a) DO UPDATE SET val = excluded.val; SELECT val FROM c WHERE a=1 AND b=1; } {y} do_execsql_test_on_specific_db {:memory:} upsert-collate-nocase { CREATE TABLE nc (name TEXT COLLATE NOCASE UNIQUE, v); INSERT INTO nc VALUES ('Alice', 1); INSERT INTO nc VALUES ('aLiCe', 2) ON CONFLICT(name COLLATE NOCASE) DO UPDATE SET v = excluded.v; SELECT * FROM nc; } {Alice|2} do_execsql_test_on_specific_db {:memory:} upsert-returning-update { CREATE TABLE r (id INTEGER PRIMARY KEY, name); INSERT INTO r VALUES (1,'a'); INSERT INTO r VALUES (1,'b') ON CONFLICT DO UPDATE SET name = excluded.name RETURNING id, name; } {1|b} do_execsql_test_on_specific_db {:memory:} upsert-returning-insert { CREATE TABLE r2 (id INTEGER PRIMARY KEY, name); INSERT INTO r2 VALUES (2,'c') ON CONFLICT DO UPDATE SET name = excluded.name RETURNING id, name; } {2|c} do_execsql_test_on_specific_db {:memory:} upsert-returning-do-nothing-empty { CREATE TABLE r3 (id INTEGER PRIMARY KEY, name); INSERT INTO r3 VALUES (2,'orig'); INSERT INTO r3 VALUES (2,'ignored') ON CONFLICT DO NOTHING RETURNING id, name; } {} do_execsql_test_on_specific_db {:memory:} upsert-rowid-in-set { CREATE TABLE rid (id INTEGER PRIMARY KEY, name); INSERT INTO rid VALUES (5,'foo'); INSERT INTO rid VALUES (5,'bar') ON CONFLICT DO UPDATE SET name = printf('id=%d', rowid); SELECT * FROM rid; } {5|id=5} do_execsql_test_in_memory_any_error upsert-notnull-violation { CREATE TABLE nn (a UNIQUE, b NOT NULL); INSERT INTO nn VALUES (1,'x'); INSERT INTO nn VALUES (1,'y') ON CONFLICT(a) DO UPDATE SET b = NULL; } do_execsql_test_on_specific_db {:memory:} upsert-updates-other-unique-key { CREATE TABLE idx (a UNIQUE, b UNIQUE); INSERT INTO idx VALUES (1,1); INSERT INTO idx VALUES (1,2) ON CONFLICT(a) DO UPDATE SET b = excluded.b; SELECT * FROM idx; } {1|2} do_execsql_test_in_memory_any_error upsert-target-mismatch-errors { CREATE TABLE tm (a, b UNIQUE); INSERT INTO tm VALUES (1,1); INSERT INTO tm VALUES (2,1) ON CONFLICT(a) DO UPDATE SET a = excluded.a; -- conflict is on b, target is a → error } do_execsql_test_on_specific_db {:memory:} upsert-omitted-target-matches-pk { CREATE TABLE pkalias (a INTEGER PRIMARY KEY, b); INSERT INTO pkalias VALUES (42,'old'); INSERT INTO pkalias (a,b) VALUES (42,'new') ON CONFLICT DO UPDATE SET b = excluded.b; SELECT * FROM pkalias; } {42|new} do_execsql_test_on_specific_db {:memory:} upsert-rowvalue-set { CREATE TABLE rv (a INTEGER PRIMARY KEY, b, c); INSERT INTO rv VALUES (1,'x','y'); INSERT INTO rv VALUES (1,'B','C') ON CONFLICT DO UPDATE SET (b,c) = (excluded.b, excluded.c); SELECT * FROM rv; } {1|B|C} do_execsql_test_on_specific_db {:memory:} upsert-where-excluded-vs-target { CREATE TABLE wh (a UNIQUE, b); INSERT INTO wh VALUES (1,5); INSERT INTO wh VALUES (1,3) ON CONFLICT(a) DO UPDATE SET b = excluded.b WHERE excluded.b > b; -- 3 > 5 → no INSERT INTO wh VALUES (1,10) ON CONFLICT(a) DO UPDATE SET b = excluded.b WHERE excluded.b > b; -- 10 > 5 → yes SELECT * FROM wh; } {1|10} do_execsql_test_in_memory_any_error upsert-invalid-qualified-lhs { CREATE TABLE bad (a UNIQUE, b); INSERT INTO bad VALUES (1,'x'); INSERT INTO bad VALUES (1,'y') ON CONFLICT(a) DO UPDATE SET excluded.b = 'z'; } do_execsql_test_on_specific_db {:memory:} upsert-values-returning-mixed { CREATE TABLE mix (k UNIQUE, v); INSERT INTO mix VALUES (1,'one'); INSERT INTO mix VALUES (1,'uno'), (2,'dos') ON CONFLICT(k) DO UPDATE SET v = excluded.v RETURNING k, v ; } {1|uno 2|dos} do_execsql_test_on_specific_db {:memory:} upsert-collate-implicit-match { CREATE TABLE ci (name TEXT COLLATE NOCASE, v); -- no explicit collate on index CREATE UNIQUE INDEX ci_name ON ci(name); INSERT INTO ci VALUES ('Alice', 1); INSERT INTO ci VALUES ('aLiCe', 2) ON CONFLICT(name COLLATE NOCASE) DO UPDATE SET v = excluded.v; SELECT * FROM ci; } {Alice|2} # Composite index requires exact coverage, targeting too few columns must not match. do_execsql_test_in_memory_any_error upsert-composite-target-too-few { CREATE TABLE ct (a, b, val); CREATE UNIQUE INDEX ct_ab ON ct(a,b); INSERT INTO ct VALUES (1,1,'x'); INSERT INTO ct VALUES (1,1,'y') ON CONFLICT(a) DO UPDATE SET val = excluded.val; -- only "a" given → no match → error } # Composite index requires exact coverage, targeting too few columns must not match. do_execsql_test_on_specific_db {:memory:} upsert-multilple-conflict-targets { CREATE TABLE ct (id INTEGER PRIMARY KEY, x UNIQUE, y UNIQUE, z DEFAULT NULL); INSERT INTO ct(id, x, y) VALUES (1, 'x', 'y'); INSERT INTO ct(id, x, y) VALUES (2, 'a', 'b'); INSERT INTO ct(id, x, y) VALUES (3, '!', '@'); INSERT INTO ct(id, x, y) VALUES (4, 'x', 'y1'), (5, 'a1', 'b'), (3, '_', '_') ON CONFLICT(x) DO UPDATE SET x = excluded.x || '-' || x, y = excluded.y || '@' || y, z = 'x' ON CONFLICT(y) DO UPDATE SET x = excluded.x || '+' || x, y = excluded.y || '!' || y, z = 'y' ON CONFLICT DO UPDATE SET x = excluded.x || '#' || x, y = excluded.y || '%' || y, z = 'fallback'; SELECT * FROM ct; } {1|x-x|y1@y|x 2|a1+a|b!b|y 3|_#!|_%@|fallback } # Qualified target (t.a) should match unique index on a. do_execsql_test_on_specific_db {:memory:} upsert-qualified-target { CREATE TABLE qt (a UNIQUE, b); INSERT INTO qt VALUES (1,'old'); INSERT INTO qt VALUES (1,'new') ON CONFLICT(qt.a) DO UPDATE SET b = excluded.b; SELECT * FROM qt; } {1|new} # Non-simple target expression is not allowed (e.g., lower(name)) → no match → error. do_execsql_test_in_memory_any_error upsert-invalid-target-expression { CREATE TABLE it (name, v); CREATE UNIQUE INDEX it_name ON it(name); INSERT INTO it VALUES ('x',1); INSERT INTO it VALUES ('x',2) ON CONFLICT(lower(name)) DO UPDATE SET v = excluded.v; } # WHERE with three-valued logic: b < excluded.b is NULL if b IS NULL, should NOT update. do_execsql_test_on_specific_db {:memory:} upsert-where-null-3vl-no-update { CREATE TABLE w3 (a UNIQUE, b); INSERT INTO w3 VALUES (1, NULL); INSERT INTO w3 VALUES (1, 5) ON CONFLICT(a) DO UPDATE SET b = excluded.b WHERE b < excluded.b; SELECT * FROM w3; } {1|} # WHERE false on PK conflict → behaves like DO NOTHING. do_execsql_test_on_specific_db {:memory:} upsert-pk-where-false { CREATE TABLE pw (id INTEGER PRIMARY KEY, name); INSERT INTO pw VALUES (7,'keep'); INSERT INTO pw VALUES (7,'drop') ON CONFLICT DO UPDATE SET name = excluded.name WHERE 0; SELECT * FROM pw; } {7|keep} # WHERE referencing both target and EXCLUDED with arithmetic. do_execsql_test_on_specific_db {:memory:} upsert-where-combo { CREATE TABLE wc (a UNIQUE, b); INSERT INTO wc VALUES (1, 10); INSERT INTO wc VALUES (1, 12) ON CONFLICT(a) DO UPDATE SET b = excluded.b WHERE excluded.b >= b + 2; -- 12 >= 10 + 2 → yes SELECT * FROM wc; } {1|12} # Invalid EXCLUDED reference should error. do_execsql_test_in_memory_error_content upsert-invalid-excluded-column { CREATE TABLE xe (a UNIQUE, v); INSERT INTO xe VALUES (1, 'ok'); INSERT INTO xe VALUES (1, 'nope') ON CONFLICT(a) DO UPDATE SET v = excluded.not_a_column; } {".*no such column.*"} # DO UPDATE changes the *conflicting key column* to a different unique value. do_execsql_test_on_specific_db {:memory:} upsert-update-conflicting-key { CREATE TABLE uk (a UNIQUE, b); INSERT INTO uk VALUES (1,'old'); INSERT INTO uk VALUES (1,'new') ON CONFLICT(a) DO UPDATE SET a = 2, b = excluded.b; SELECT * FROM uk; } {2|new} # DO UPDATE that writes the same values (no-op) should still succeed. do_execsql_test_on_specific_db {:memory:} upsert-noop-update-ok { CREATE TABLE nu (a UNIQUE, b); INSERT INTO nu VALUES (5,'same'); INSERT INTO nu VALUES (5,'irrelevant') ON CONFLICT(a) DO UPDATE SET b = b; -- no change SELECT * FROM nu; } {5|same} # DO UPDATE that would violate a different UNIQUE constraint should error. do_execsql_test_in_memory_any_error upsert-update-causes-second-unique-violation { CREATE TABLE uv (a UNIQUE, b UNIQUE); INSERT INTO uv VALUES (1, 10); INSERT INTO uv VALUES (2, 20); INSERT INTO uv VALUES (1, 20) ON CONFLICT(a) DO UPDATE SET b = excluded.b; # would duplicate b=20 from row a=2 } # Multi-row VALUES with mixed conflict/non-conflict and WHERE filter in the DO UPDATE. do_execsql_test_on_specific_db {:memory:} upsert-multirow-mixed-where { CREATE TABLE mm (k UNIQUE, v); INSERT INTO mm VALUES (1,'one'); INSERT INTO mm VALUES (1,'two'), (2,'dos'), (1,'zzz') ON CONFLICT(k) DO UPDATE SET v = excluded.v WHERE excluded.v != 'zzz'; -- skip the 'zzz' update SELECT * FROM mm ORDER BY k; } {1|two 2|dos} # Omitted target with UNIQUE index: confirm it updates. do_execsql_test_on_specific_db {:memory:} upsert-omitted-target-updates-unique { CREATE TABLE ou (a, b); CREATE UNIQUE INDEX ou_a ON ou(a); INSERT INTO ou VALUES (3,'x'); INSERT INTO ou VALUES (3,'y') ON CONFLICT DO UPDATE SET b = excluded.b; SELECT * FROM ou; } {3|y} do_execsql_test_on_specific_db {:memory:} upsert-current-qualified.1 { CREATE TABLE dq (a INTEGER UNIQUE, b TEXT); INSERT INTO dq VALUES (1,'old'); INSERT INTO dq VALUES (1,'new') ON CONFLICT(dq.a) DO UPDATE SET b = dq.b || '-' || excluded.b; SELECT * FROM dq; } {1|old-new} do_execsql_test_on_specific_db {:memory:} upsert-multicol-set.1 { CREATE TABLE dq (a INTEGER UNIQUE, b TEXT); INSERT INTO dq VALUES (1,'old'); INSERT INTO dq VALUES (1,'new') ON CONFLICT(a) DO UPDATE SET (`a`,`b`) = (`excluded`.`a`, `excluded`.`b`); SELECT * FROM dq; } {1|new} do_execsql_test_on_specific_db {:memory:} upsert-where-predicate.1 { CREATE TABLE dq (a INTEGER UNIQUE, b TEXT); INSERT INTO dq VALUES (1,'old'); INSERT INTO dq VALUES (1,'old') ON CONFLICT(a) DO UPDATE SET b = excluded.b WHERE dq.b != excluded.b; SELECT * FROM dq; } {1|old} do_execsql_test_on_specific_db {:memory:} upsert-doubly-qualified-target { CREATE TABLE dq (a UNIQUE, b); INSERT INTO dq VALUES (1,'old'); INSERT INTO dq VALUES (1,'new') ON CONFLICT(main.dq.a) DO UPDATE SET b = excluded.b; SELECT * FROM dq; } {1|new} do_execsql_test_on_specific_db {:memory:} upsert-targets-chain { CREATE TABLE dq (a UNIQUE, b UNIQUE, c UNIQUE, value TEXT); CREATE UNIQUE INDEX dq_ab ON dq(a, b); INSERT INTO dq VALUES ('a1', 'a2', 'a3', 'aaa'); INSERT INTO dq VALUES ('b1', 'b2', 'b3', 'bbb'); INSERT INTO dq VALUES ('c1', 'c2', 'c3', 'ccc'); INSERT INTO dq VALUES ('d1', 'd2', 'd3', 'ddd'); INSERT INTO dq VALUES ('a1', 'a2', 'a3', 'upd1'), ('b1', 'b1', 'b1', 'upd2'), ('c2', 'c2', 'c2', 'upd3'), ('d3', 'd3', 'd3', 'upd4') ON CONFLICT (a, b) DO UPDATE SET value = excluded.value || '-a' ON CONFLICT (a) DO UPDATE SET value = excluded.value || '-b' ON CONFLICT (b) DO UPDATE SET value = excluded.value || '-c' ON CONFLICT DO UPDATE SET value = excluded.value || '-d'; SELECT * FROM dq; } {a1|a2|a3|upd1-a b1|b2|b3|upd2-b c1|c2|c3|upd3-c d1|d2|d3|upd4-d} # https://github.com/tursodatabase/turso/issues/3384 do_execsql_test_on_specific_db {:memory:} upsert-non-rowid-pk-target { create table phonebook(name text primary key, phonenumber text, validDate date); insert into phonebook values ('Alice','704-545-3333','2018-10-10'); insert into phonebook values ('Alice','704-111-1111','2018-10-20') on conflict (name) do update set phonenumber=excluded.phonenumber, validDate=excluded.validDate; SELECT phonenumber, validDate FROM phonebook; } {704-111-1111|2018-10-20} # TODO: uncomment these when we support collations in indexes # (right now it errors on Parse Error: cannot use expressions in CREATE INDEX) # # Target specifies BINARY but the unique index is NOCASE: target should NOT match, so expect error # do_execsql_test_in_memory_any_error upsert-collate-target-mismatch { # CREATE TABLE cm (name TEXT, v); # CREATE UNIQUE INDEX cm_name_nocase ON cm(name COLLATE NOCASE); # INSERT INTO cm VALUES ('Alice', 1); # INSERT INTO cm VALUES ('aLiCe', 2) # ON CONFLICT(name COLLATE BINARY) DO UPDATE SET v = excluded.v; # } # # do_execsql_test_on_specific_db {:memory:} upsert-collate-omitted-target-matches { # CREATE TABLE co (name TEXT, v); # CREATE UNIQUE INDEX co_name_nocase ON co(name COLLATE NOCASE); # INSERT INTO co VALUES ('Alice', 1); # INSERT INTO co VALUES ('aLiCe', 9) # ON CONFLICT DO UPDATE SET v = excluded.v; # SELECT * FROM co; # } {Alice|9} # # # do_execsql_test_on_specific_db {:memory:} upsert-composite-collate-orderless { # CREATE TABLE cc (name TEXT, city TEXT, val); # CREATE UNIQUE INDEX cc_nc ON cc(name COLLATE NOCASE, city); # INSERT INTO cc VALUES ('Alice','SF','old'); # INSERT INTO cc VALUES ('aLiCe','SF','new') # ON CONFLICT(city, name COLLATE NOCASE) DO UPDATE SET val = excluded.val; # SELECT * FROM cc; # } {Alice|SF|new}