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

DuckDB: intercept UPDATE/DELETE at scan init under ROW/MIXED-unsafe binlog to push down the whole statement

    XMLWordPrintable

Details

    Description

      Problem

      When the effective binlog format of a statement is ROW (explicit binlog_format=ROW, or MIXED switched to row because the statement was marked unsafe), the server disables the direct UPDATE/DELETE path (!binlog_is_row guard in mysql_delete() / mysql_update()) and falls back to the row-by-row handler path. For DuckDB this path is expensive (one engine round trip per row through DeleteConvertor / the mixed DeltaAppender) and any indexed WHERE predicate fails with error 1031, because ha_duckdb::index_read_map() is a HA_ERR_WRONG_COMMAND stub.

      Note that MIXED switches to row for every DML on a table whose column has a non-deterministic DEFAULT expression: TABLE_SHARE::non_determinstic_insert is set for e.g. id UUID DEFAULT(uuid()) at open time (sql/table.cc) and THD::decide_logging_format() then marks any statement with CF_CAN_GENERATE_ROW_EVENTS unsafe (sql/sql_class.cc, BINLOG_STMT_UNSAFE_SYSTEM_FUNCTION). So the canonical blog-post table hits the slow row path on every UPDATE/DELETE whenever the binary log is enabled.

      Proposed solution: whole-statement interception at first scan, ColumnStore-style

      ColumnStore already implements this pattern in-tree (storage/columnstore/columnstore/dbcon/mysql/ha_mcs_impl.cpp):

      • impl_rnd_init(): when the statement is an UPDATE/DELETE and the current table is the modified target, execute the whole statement inside the engine (doUpdateDelete()) and return.
      • impl_rnd_next(): for such statements immediately return HA_ERR_END_OF_FILE, so the server's row loop processes zero rows and never calls ha_update_row() / ha_delete_row() / index lookups.

      For DuckDB the same interception is cheap because the whole-statement pushdown machinery already exists (direct_update_rows() / direct_delete_rows() forward thd_query_string() to DuckDB, including the forwarded-SQL safety checks):

      • ha_duckdb::rnd_init(): if thd->lex->sql_command is UPDATE/DELETE, the table is locked for write, the THD is not a replication applier and the statement is not marked unsafe – execute the original statement through the existing pushdown, remember the interception in DuckdbThdContext, return success.
      • ha_duckdb::rnd_next(): when the interception flag is set, return HA_ERR_END_OF_FILE.

      Binlog correctness (the crux)

      Executing the statement inside the engine produces no row events, and under ROW format the server does not log the statement either, so the change would silently vanish from the binary log. Therefore:

      • If the statement is deterministic (row format came from explicit binlog_format=ROW or from the table-DEFAULT false positive, and lex->is_stmt_unsafe() shows no statement-level unsafety): after successful pushdown, emit the original statement into the binlog via THD::binlog_query(STMT_QUERY_TYPE, ...) from an engine helper compiled with MYSQL_SERVER (this is exactly what AliSQL does from sql_update.cc / sql_delete.cc, here done engine-side with no core changes). Guard: binlog open, not under wsrep/Galera (statement injection would bypass certification).
      • If the statement itself is non-deterministic (e.g. SET col = uuid()): do NOT intercept; fall back to the row-by-row path, which generates correct row events (crash-free after MDEV-40957).

      Replica-side row events are out of scope here: they carry no statement text at all and are handled per-row by the read-free apply from MDEV-41197.

      Known trade-off to decide on

      On the intercepted path the server's own row counter stays 0, so the client receives "0 rows affected" although the data changed (ColumnStore has the same behaviour on this fallback path; its primary path is the direct API which reports counts correctly). THD::set_row_count_func() can fix ROW_COUNT() but not the OK packet. If accurate affected-rows reporting is required, the alternative is a small server-side hook relaxing the !binlog_is_row guard for deterministic statements instead of the rnd_init interception.

      Test plan

      • MTR: table with UUID DEFAULT(uuid()) PRIMARY KEY, binlog enabled, MIXED and explicit ROW: UPDATE/DELETE with indexed and non-indexed WHERE succeed, affected data verified, binlog contains the statement event (mysqlbinlog / SHOW BINLOG EVENTS).
      • Replication MTR: master intercepts, replica (statement event) applies and matches master.
      • Non-deterministic UPDATE (SET col = uuid()) is not intercepted and still row-logged correctly.
      • Interception disabled for replication applier threads and under wsrep.

      Attachments

        Activity

          People

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

            Dates

              Created:
              Updated:

              Git Integration

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