From 88a675a0f2ef2d7f6c0356bf51bded7342555d1a Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Wed, 27 Aug 2025 16:36:20 -0600 Subject: [PATCH 1/3] Document ART index limitation with concurrent transactions and deletes --- docs/1.2/sql/indexes.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/1.2/sql/indexes.md b/docs/1.2/sql/indexes.md index 2e3da684ce8..7e2829e5e52 100644 --- a/docs/1.2/sql/indexes.md +++ b/docs/1.2/sql/indexes.md @@ -108,4 +108,25 @@ Violates foreign key constraint because key "id: 1" is still referenced by a for ``` The reason for this is because DuckDB does not yet support “looking ahead”. -During the `INSERT`, it is unaware it will reinsert the foreign key value as part of the `UPDATE` rewrite. \ No newline at end of file +During the `INSERT`, it is unaware it will reinsert the foreign key value as part of the `UPDATE` rewrite. + +### Constraint Checking After Delete With Concurrent Transactions + +When a delete is committed on a table with an ART index, data can only be removed from the index when no further transactions exist that refer to the deleted entry. This means if you perform a delete transaction, a subsequent transaction which inserts a record with the same key as the deleted record can fail with a constraint error if there is a concurrent transaction referencing the deleted record. Pseudocode to demonstrate: + +``` +// Assume "someTable" is a table with an ART index preventing duplicates +tx1 = duckdbTxStart() +someRecord = duckdb(tx1, "select * from someTable using sample 1 rows") + +tx2 = duckdbTxStart() +duckdbDelete(tx2, someRecord) +duckdbTxCommit(tx2) + +// At this point someRecord is deleted, but the ART index is not updated, so the following would fail with a constraint error: +// tx3 = duckdbTxStart() +// duckdbInsert(tx3, someRecord) +// duckdbTxCommit(tx3) + +duckdbTxCommit(tx1) // Following this, the above insert would succeed because the ART index was allowed to update +``` From bd2b25fdbd537b04b1b5cdb494c02aeda8e3f4ad Mon Sep 17 00:00:00 2001 From: Gabor Szarnyas Date: Mon, 15 Sep 2025 10:13:13 +0200 Subject: [PATCH 2/3] Update docs/1.2/sql/indexes.md --- docs/1.2/sql/indexes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/1.2/sql/indexes.md b/docs/1.2/sql/indexes.md index 7e2829e5e52..0298f9d5983 100644 --- a/docs/1.2/sql/indexes.md +++ b/docs/1.2/sql/indexes.md @@ -114,7 +114,7 @@ During the `INSERT`, it is unaware it will reinsert the foreign key value as par When a delete is committed on a table with an ART index, data can only be removed from the index when no further transactions exist that refer to the deleted entry. This means if you perform a delete transaction, a subsequent transaction which inserts a record with the same key as the deleted record can fail with a constraint error if there is a concurrent transaction referencing the deleted record. Pseudocode to demonstrate: -``` +```cpp // Assume "someTable" is a table with an ART index preventing duplicates tx1 = duckdbTxStart() someRecord = duckdb(tx1, "select * from someTable using sample 1 rows") From 438809586c198beb8ad32396e920a4ac9a715854 Mon Sep 17 00:00:00 2001 From: Calum Sieppert Date: Mon, 15 Sep 2025 10:20:59 -0600 Subject: [PATCH 3/3] Enhance documentation on constraint checking after deletes Add foreign key limitation --- docs/1.2/sql/indexes.md | 48 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/1.2/sql/indexes.md b/docs/1.2/sql/indexes.md index 0298f9d5983..64a5a8ea08a 100644 --- a/docs/1.2/sql/indexes.md +++ b/docs/1.2/sql/indexes.md @@ -112,10 +112,16 @@ During the `INSERT`, it is unaware it will reinsert the foreign key value as par ### Constraint Checking After Delete With Concurrent Transactions -When a delete is committed on a table with an ART index, data can only be removed from the index when no further transactions exist that refer to the deleted entry. This means if you perform a delete transaction, a subsequent transaction which inserts a record with the same key as the deleted record can fail with a constraint error if there is a concurrent transaction referencing the deleted record. Pseudocode to demonstrate: +When a delete is committed on a table with an index, data can only be removed from the index when no further transactions exist that refer to the deleted entry. This means that for indices which enforce constraint violations, if you perform a delete transaction, constraint checking can fail for a subsequent transaction which inserts a record with the same key as the deleted record if there is a concurrent transaction referencing the deleted record. Note that constraint violations are only relevant for primary key, foreign key, and UNIQUE indexes. + +There are two main ways that constraint checking can fail: + +#### Over-eager unique constraint checking + +For uniqueness constraints, inserts can fail when they should succeed: ```cpp -// Assume "someTable" is a table with an ART index preventing duplicates +// Assume "someTable" is a table with an index enforcing uniqueness tx1 = duckdbTxStart() someRecord = duckdb(tx1, "select * from someTable using sample 1 rows") @@ -130,3 +136,41 @@ duckdbTxCommit(tx2) duckdbTxCommit(tx1) // Following this, the above insert would succeed because the ART index was allowed to update ``` + +#### Under-eager foreign key constraint checking + +For foreign key constraints, inserts can succeed when they should fail: + +```cpp +// Setup: Create a primary table with UUID primary key and a secondary table with foreign key reference +primaryId = generateNewGUID() +conn = duckdbConnectInMemory() + +// Create tables and insert initial record in primary table +duckdb(conn, "CREATE TABLE primary_table (id UUID PRIMARY KEY)") +duckdb(conn, "CREATE TABLE secondary_table (primary_id UUID, FOREIGN KEY (primary_id) REFERENCES primary_table(id))") +duckdbInsert(conn, "primary_table", {id: primaryId}) + +// Start transaction tx1 which will read from primary_table +tx1 = duckdbTxStart(conn) +readRecord = duckdb(tx1, "SELECT id FROM primary_table LIMIT 1") +// Note: tx1 remains open, holding locks/resources + +// Outside of tx1, delete the record from primary_table +duckdbDelete(conn, "primary_table", {id: primaryId}) + +// Try to insert into secondary_table with foreign key reference to the now-deleted primary record +// This succeeds because tx1 is still open and the constraint isn't fully enforced yet +duckdbInsert(conn, "secondary_table", {primary_id: primaryId}) + +// Commit tx1, releasing any locks/resources +duckdbTxCommit(tx1) + +// Verify the primary record is indeed deleted +count = duckdb(conn, "SELECT COUNT() FROM primary_table WHERE id = $primaryId",{primaryId: primaryId}) +assert(count == 0, "Record should be deleted") + +// Verify the secondary record with the foreign key reference exists, an inconsistent state +count = duckdb(conn, "SELECT COUNT() FROM secondary_table WHERE primary_id = $primaryId",{primaryId: primaryId}) +assert(count == 1, "Foreign key reference should exist") +```