From 4052c7be6c26c23b9e4daa824fb76da3e572a060 Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Fri, 25 Sep 2026 13:28:37 +0300 Subject: [PATCH] fix(mysql): request CLIENT_FOUND_ROWS so idempotent UPDATE reports matched rows (Closes #889) MySQL reports rows changed by default, so an UPDATE that rewrites a cell with its existing value returns affectedRows = 0. expectDmlMatchedRows treated that as a stale row and rolled back the whole staged save. Advertise CLIENT_FOUND_ROWS in the vendored mysql_client handshake (both the plain response and the SSL request) so affectedRows means matched rows, as it already does for PostgreSQL and SQLite. The exact-one-row check stays intact. --- third_party/mysql_client/CHANGELOG.md | 4 +++ .../packet/packet_handshake_response_41.dart | 1 + .../packet/packet_ssl_request.dart | 1 + .../mysql_client/test/mysql_packet_test.dart | 30 +++++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/third_party/mysql_client/CHANGELOG.md b/third_party/mysql_client/CHANGELOG.md index 0369330..59f59c7 100644 --- a/third_party/mysql_client/CHANGELOG.md +++ b/third_party/mysql_client/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased (Querya patch) + +- Request `CLIENT_FOUND_ROWS` in the client handshake so `affectedRows` is the number of matched rows, not changed rows + ## 0.0.27 - Add timeoutMs param to pool constructor diff --git a/third_party/mysql_client/lib/src/mysql_protocol/packet/packet_handshake_response_41.dart b/third_party/mysql_client/lib/src/mysql_protocol/packet/packet_handshake_response_41.dart index 52fd600..8af7b3d 100644 --- a/third_party/mysql_client/lib/src/mysql_protocol/packet/packet_handshake_response_41.dart +++ b/third_party/mysql_client/lib/src/mysql_protocol/packet/packet_handshake_response_41.dart @@ -5,6 +5,7 @@ import 'package:mysql_client/mysql_protocol.dart'; import 'package:mysql_client/mysql_protocol_extension.dart'; const _supportedCapabitilies = mysqlCapFlagClientProtocol41 | + mysqlCapFlagClientFoundRows | mysqlCapFlagClientSecureConnection | mysqlCapFlagClientPluginAuth | mysqlCapFlagClientPluginAuthLenEncClientData | diff --git a/third_party/mysql_client/lib/src/mysql_protocol/packet/packet_ssl_request.dart b/third_party/mysql_client/lib/src/mysql_protocol/packet/packet_ssl_request.dart index 800e6ad..cb3b54e 100644 --- a/third_party/mysql_client/lib/src/mysql_protocol/packet/packet_ssl_request.dart +++ b/third_party/mysql_client/lib/src/mysql_protocol/packet/packet_ssl_request.dart @@ -3,6 +3,7 @@ import 'package:buffer/buffer.dart'; import 'package:mysql_client/mysql_protocol.dart'; const _supportedCapabitilies = mysqlCapFlagClientProtocol41 | + mysqlCapFlagClientFoundRows | mysqlCapFlagClientSecureConnection | mysqlCapFlagClientPluginAuth | mysqlCapFlagClientPluginAuthLenEncClientData | diff --git a/third_party/mysql_client/test/mysql_packet_test.dart b/third_party/mysql_client/test/mysql_packet_test.dart index a09159a..d1efb02 100644 --- a/third_party/mysql_client/test/mysql_packet_test.dart +++ b/third_party/mysql_client/test/mysql_packet_test.dart @@ -629,6 +629,36 @@ void main() { ); }); + test("client handshake requests CLIENT_FOUND_ROWS", () { + final handshake = MySQLPacket.decodeInitialHandshake( + Uint8List.fromList( + HEX.decode( + '4d0000000a352e372e33352d3338007b000000181e73526349597c00ffff080200ffc1150000000000000000000007317a2531721d587825181d006d7973716c5f6e61746976655f70617373776f726400', + ), + ), + ).payload as MySQLPacketInitialHandshake; + + final response = MySQLPacketHandshakeResponse41.createWithNativePassword( + username: 'root', + password: 'secret', + initialHandshakePayload: handshake, + ); + final sslRequest = MySQLPacketSSLRequest.createDefault( + initialHandshakePayload: handshake, + connectWithDB: false, + ); + + // So UPDATE reports matched (not changed) rows, like PostgreSQL / SQLite. + expect( + response.capabilityFlags & mysqlCapFlagClientFoundRows, + mysqlCapFlagClientFoundRows, + ); + expect( + sslRequest.capabilityFlags & mysqlCapFlagClientFoundRows, + mysqlCapFlagClientFoundRows, + ); + }); + test("testing response ok packet", () { final buffer = Uint8List.fromList(HEX.decode('0700000200000002000000')); final packet = MySQLPacket.decodeGenericPacket(buffer);