Details
-
Bug
-
Status: Open (View Workflow)
-
Major
-
Resolution: Unresolved
-
None
-
None
Description
For DuckDB tables without a primary key (possible only with duckdb_require_primary_key=OFF), the row-by-row DML path generates a wrong WHERE clause. This path is used when the statement is not pushed down as a whole, e.g. when the table has triggers, and on a replica applying row events.
DMLConvertor::fill_index_fields_for_where() (storage/duckdb/convertor/dml_convertor.cc) falls back to all columns as the row identity when there is no primary key. Three problems follow:
- append_field_value_to_sql() emits NULL for a NULL value, producing col = NULL, which never matches. The row is neither updated nor deleted, while MariaDB counts it as changed (triggers fire).
- UpdateConvertor::append_where_value() / DeleteConvertor::append_where_value() redirect field->ptr to the old row, but field->is_null() reads the NULL bit from record[0] (the new row). So the NULL-ness of the WHERE value comes from the wrong image; e.g. an UPDATE ... SET b = NULL is generated as WHERE b = NULL and is not applied.
- Exact duplicate rows are all matched, so DELETE ... LIMIT 1 removes every copy.
How to repeat (triggers force the row-by-row path):
SET GLOBAL duckdb_require_primary_key= OFF; |
CREATE TABLE t (a INT, b INT) ENGINE=DuckDB; |
CREATE TABLE audit (ev CHAR(1)) ENGINE=InnoDB; |
CREATE TRIGGER t_ad AFTER DELETE ON t FOR EACH ROW INSERT INTO audit VALUES ('D'); |
CREATE TRIGGER t_au AFTER UPDATE ON t FOR EACH ROW INSERT INTO audit VALUES ('U'); |
INSERT INTO t VALUES (1, NULL), (2, 2), (3, NULL), (4, 4), (5, 5), (5, 5); |
DELETE FROM t WHERE a = 1; |
UPDATE t SET b = 30 WHERE a = 3; |
UPDATE t SET b = NULL WHERE a = 4; |
DELETE FROM t WHERE a = 5 LIMIT 1; |
SELECT * FROM t ORDER BY a, b; |
SELECT ev, COUNT(*) FROM audit GROUP BY ev ORDER BY ev; |
Actual result:
a b
|
1 NULL
|
2 2
|
3 NULL
|
4 4
|
 |
ev COUNT(*)
|
D 2
|
U 2
|
Expected: (2,2), (3,30), (4,NULL), (5,5).
Suggested fix: emit IS NULL for NULL values and take the NULL flag from the old row (e.g. field->is_null_in_record(old_row)); for duplicates, either identify the row by DuckDB rowid captured during the scan or reject the operation when more than one row is affected.
Found while working on MDEV-40957; the limitation is documented in storage/duckdb/docs/architecture.md (Path 3) and README.md.