Uploaded image for project: 'MariaDB Server'
  1. MariaDB Server
  2. MDEV-41386

DuckDB: row-by-row UPDATE/DELETE on tables without a primary key skips rows with NULLs and changes all duplicates

    XMLWordPrintable

Details

    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:

      1. 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).
      2. 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.
      3. 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.

      Attachments

        Activity

          People

            drrtuy Roman
            drrtuy Roman
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

            Dates

              Created:
              Updated:

              Time Tracking

                Estimated:
                Original Estimate - 0d
                0d
                Remaining:
                Remaining Estimate - 1h
                1h
                Logged:
                Time Spent - Not Specified
                Not Specified

                Git Integration

                  Error rendering 'com.xiplink.jira.git.jira_git_plugin:git-issue-webpanel'. Please contact your Jira administrators.