Report changed rows for updates - #458
Draft
JanJakes wants to merge 1 commit into
Draft
Conversation
Count actual OLD and NEW values with a connection-private temporary trigger. The trigger invokes an internal SQLite function that updates the driver's affected-row count directly, allowing the translated UPDATE to select joined values and evaluate assignments only once.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
This PR fixes affected-row reporting for translated
UPDATEstatements. The driver now returns the number of rows whose values actually changed, matching MySQL semantics instead of SQLite's matched-row behavior. In particular, a no-op update now returns0rather than1.OLDandNEWvalues with binary semantics.UPDATE, including non-deterministic expressions and values selected from joined tables.IGNORE, and temporary-table updates, with cleanup after both successful and failed statements.How it works
SQLite's affected-row result counts rows matched by an
UPDATE, even when every assignment leaves the stored values unchanged. The driver therefore creates a connection-private temporary trigger for the duration of each update. The trigger invokes an internal SQLite function only when an assigned value changes, incrementing the driver's affected-row counter directly. It is always removed in afinallyblock.Approaches considered
The available approaches range from simple SQLite counters to native integration:
SQLite counters and statement output.
changes(),total_changes(),PRAGMA count_changes, PDO's native row count, and plainRETURNINGall report rows processed by SQLite, including no-op assignments.RETURNINGexposes post-update values but does not supportOLD.column, so it cannot identify actual changes by itself. Metadata and statistics facilities such asdata_version,dbstat, update hooks, and page or WAL counters do not retain the required row-levelOLDandNEWvalues.Filter the original
UPDATEto changed rows. Adding a null-safe difference predicate makes SQLite's native count correct in one statement and avoids a second scan. However, it suppresses update triggers for matched no-op rows, evaluates assignment expressions again, and can changeLIMIT,IGNORE, or joined-update behavior.Run a preflight
SELECT COUNT(*). Counting rows whose current values differ before executing the original update is straightforward for deterministic, filtered, single-table updates. It can diverge when expressions are volatile, constraints cause rows to be ignored, joins provide multiple source candidates, or the original query contains more complex selection semantics.Materialize candidates and use
UPDATE … RETURNING. A materialized CTE can preserve old values and computed assignments, whileRETURNINGexposes successfully updated rows. This keeps the count tied to the update, but requires materializing every candidate, merging with existing CTEs, preserving exact row identity, and substantially rewriting joined updates.Wrap assignments in stateful SQLite functions. Functions could capture old values while returning computed assignments unchanged, with
RETURNINGused to compare final values. Passing values through PHP risks type coercion, while row identity, ignored rows, legacy SQLite support, and per-statement state make the design complex.Compare
OLDandNEWin a temporary trigger. This observes the values from the actual update execution, naturally handling volatile expressions, joins, ignored rows, affinity, and no-op assignments. A single-row temporary counter table would keep all state in SQLite but require nested table DML for every changed row. Calling a connection-local function instead avoids that table work and increments the driver's counter directly, which is the approach used here.Use SQLite's native pre-update hook.
sqlite3_preupdate_hook()is the cleanest engine-level mechanism because it exposes old and new column values during the update. It is compile-time optional and unavailable through PDO, so adopting it would require native integration beyond this driver.