This is an automated email from the ASF dual-hosted git repository.

lidavidm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new 4721373fd1 fix(c/driver/sqlite): only count rows_affected when 
applicable (#4826)
4721373fd1 is described below

commit 4721373fd1626c23a67302b3472529b52e8e86bf
Author: David Li <[email protected]>
AuthorDate: Wed Sep 30 16:18:55 2026 -0700

    fix(c/driver/sqlite): only count rows_affected when applicable (#4826)
    
    sqlite3_total_changes isn't updated by certain DDL statements,
    so don't count it to avoid reporting a stale count.
    
    Depends on #4825.
    Closes #4820.
    
    Generated-by: GPT-6 Astra
---
 c/driver/sqlite/sqlite.cc                  | 19 +++++-
 c/driver/sqlite/sqlite_test.cc             | 95 ++++++++++++++++++++++++++++++
 c/driver/sqlite/validation/tests/sqlite.py |  1 +
 3 files changed, 113 insertions(+), 2 deletions(-)

diff --git a/c/driver/sqlite/sqlite.cc b/c/driver/sqlite/sqlite.cc
index 8d98a5f3aa..b24cff80f3 100644
--- a/c/driver/sqlite/sqlite.cc
+++ b/c/driver/sqlite/sqlite.cc
@@ -33,6 +33,17 @@
 #include "driver/framework/status.h"
 #include "driver/sqlite/statement_reader.h"
 
+#if SQLITE_VERSION_NUMBER < 3037000
+// Polyfill for older versions
+sqlite3_int64 sqlite3_total_changes64(sqlite3* db) {
+  return static_cast<sqlite3_int64>(sqlite3_total_changes(db));
+}
+
+sqlite3_int64 sqlite3_changes64(sqlite3* db) {
+  return static_cast<sqlite3_int64>(sqlite3_changes(db));
+}
+#endif
+
 namespace adbc::sqlite {
 
 using driver::Result;
@@ -1138,12 +1149,16 @@ class SqliteStatement : public 
driver::Statement<SqliteStatement> {
         }
       }
 
+      const sqlite3_int64 total_changes = sqlite3_total_changes64(conn_);
       while (sqlite3_step(stmt_) == SQLITE_ROW) {
         output_rows++;
       }
 
-      if (sqlite3_column_count(stmt_) == 0) {
-        changed_rows += sqlite3_changes(conn_);
+      // NOTE(apache/arrow-adbc#4820): only count sqlite3_total_changes when
+      // it actually changed from before
+      if (sqlite3_column_count(stmt_) == 0 &&
+          sqlite3_total_changes64(conn_) != total_changes) {
+        changed_rows += sqlite3_changes64(conn_);
       }
 
       if (!binder_.schema.release) break;
diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc
index d1ba96533b..32e96296a2 100644
--- a/c/driver/sqlite/sqlite_test.cc
+++ b/c/driver/sqlite/sqlite_test.cc
@@ -680,6 +680,101 @@ TEST_F(SqliteStatementTest, SqlIngestNameEscaping) {
   ASSERT_EQ(3, rows_affected);
 }
 
+TEST_F(SqliteStatementTest, RowsAffectedDdlAndDml) {
+  ASSERT_THAT(AdbcStatementNew(&connection, &statement, &error),
+              adbc_validation::IsOkStatus(&error));
+
+  const std::vector<std::pair<const char*, int64_t>> queries = {
+      {"CREATE TABLE affected_rows (value INTEGER)", 0},
+      {"INSERT INTO affected_rows VALUES (1), (2), (3)", 3},
+      {"CREATE TABLE affected_rows_other (value INTEGER)", 0},
+      {"CREATE INDEX affected_rows_idx ON affected_rows(value)", 0},
+      {"DROP TABLE affected_rows_other", 0},
+      {"UPDATE affected_rows SET value = value + 10 WHERE value = 1", 1},
+      {"UPDATE affected_rows SET value = value + 10 WHERE value = 99", 0},
+      {"DELETE FROM affected_rows WHERE value = 2", 1},
+      {"DELETE FROM affected_rows WHERE value = 99", 0},
+      {"CREATE TABLE affected_rows_audit (value INTEGER)", 0},
+      {"CREATE TRIGGER affected_rows_trigger AFTER INSERT ON affected_rows "
+       "BEGIN INSERT INTO affected_rows_audit VALUES (new.value); END",
+       0},
+      {"INSERT INTO affected_rows VALUES (4), (5), (6)", 3},
+      {"SELECT * FROM affected_rows_audit", 3},
+      {"DELETE FROM affected_rows", 5},
+      {"DROP INDEX affected_rows_idx", 0},
+      {"DROP TABLE affected_rows", 0},
+      {"DROP TABLE affected_rows_audit", 0},
+  };
+  for (const auto& [query, expected_rows] : queries) {
+    SCOPED_TRACE(query);
+    ASSERT_THAT(AdbcStatementSetSqlQuery(&statement, query, &error),
+                adbc_validation::IsOkStatus(&error));
+    int64_t rows_affected = -1;
+    ASSERT_THAT(AdbcStatementExecuteQuery(&statement, nullptr, &rows_affected, 
&error),
+                adbc_validation::IsOkStatus(&error));
+    ASSERT_EQ(expected_rows, rows_affected);
+  }
+}
+
+TEST_F(SqliteStatementTest, RowsAffectedBoundParameters) {
+  ASSERT_THAT(AdbcStatementNew(&connection, &statement, &error),
+              adbc_validation::IsOkStatus(&error));
+  ASSERT_THAT(AdbcStatementSetSqlQuery(
+                  &statement, "CREATE TABLE affected_rows_bound (value 
INTEGER)", &error),
+              adbc_validation::IsOkStatus(&error));
+  int64_t rows_affected = -1;
+  ASSERT_THAT(AdbcStatementExecuteQuery(&statement, nullptr, &rows_affected, 
&error),
+              adbc_validation::IsOkStatus(&error));
+  ASSERT_EQ(0, rows_affected);
+
+  auto bind_values = [&](std::vector<std::optional<int64_t>> values) {
+    adbc_validation::Handle<struct ArrowSchema> schema;
+    adbc_validation::Handle<struct ArrowArray> array;
+    struct ArrowError na_error = {};
+    ASSERT_THAT(
+        adbc_validation::MakeSchema(&schema.value, {{"value", 
NANOARROW_TYPE_INT64}}),
+        adbc_validation::IsOkErrno());
+    ASSERT_THAT(adbc_validation::MakeBatch<int64_t>(&schema.value, 
&array.value,
+                                                    &na_error, values),
+                adbc_validation::IsOkErrno(&na_error));
+    ASSERT_THAT(AdbcStatementBind(&statement, &array.value, &schema.value, 
&error),
+                adbc_validation::IsOkStatus(&error));
+  };
+
+  ASSERT_THAT(AdbcStatementSetSqlQuery(
+                  &statement, "INSERT INTO affected_rows_bound VALUES (?)", 
&error),
+              adbc_validation::IsOkStatus(&error));
+  ASSERT_THAT(AdbcStatementPrepare(&statement, &error),
+              adbc_validation::IsOkStatus(&error));
+  for (int execution = 0; execution < 2; execution++) {
+    ASSERT_NO_FATAL_FAILURE(bind_values({1, 2, 3}));
+    ASSERT_THAT(AdbcStatementExecuteQuery(&statement, nullptr, &rows_affected, 
&error),
+                adbc_validation::IsOkStatus(&error));
+    ASSERT_EQ(3, rows_affected);
+  }
+
+  ASSERT_THAT(
+      AdbcStatementSetSqlQuery(
+          &statement, "UPDATE affected_rows_bound SET value = value + 10 WHERE 
value = ?",
+          &error),
+      adbc_validation::IsOkStatus(&error));
+  ASSERT_NO_FATAL_FAILURE(bind_values({1, 99, 3}));
+  ASSERT_THAT(AdbcStatementExecuteQuery(&statement, nullptr, &rows_affected, 
&error),
+              adbc_validation::IsOkStatus(&error));
+  ASSERT_EQ(4, rows_affected);
+
+  ASSERT_THAT(AdbcStatementSetSqlQuery(
+                  &statement, "DELETE FROM affected_rows_bound WHERE value = 
?", &error),
+              adbc_validation::IsOkStatus(&error));
+  ASSERT_NO_FATAL_FAILURE(bind_values({11, 99, 13}));
+  ASSERT_THAT(AdbcStatementExecuteQuery(&statement, nullptr, &rows_affected, 
&error),
+              adbc_validation::IsOkStatus(&error));
+  ASSERT_EQ(4, rows_affected);
+
+  ASSERT_THAT(quirks()->DropTable(&connection, "affected_rows_bound", &error),
+              adbc_validation::IsOkStatus(&error));
+}
+
 // -- SQLite Specific Tests ------------------------------------------
 
 constexpr size_t kInferRows = 16;
diff --git a/c/driver/sqlite/validation/tests/sqlite.py 
b/c/driver/sqlite/validation/tests/sqlite.py
index ae8a49eee4..24b40aa1e6 100644
--- a/c/driver/sqlite/validation/tests/sqlite.py
+++ b/c/driver/sqlite/validation/tests/sqlite.py
@@ -42,6 +42,7 @@ class SQLiteQuirks(model.DriverQuirks):
         statement_get_parameter_schema=True,
         statement_prepare=True,
         statement_rows_affected=True,
+        statement_rows_affected_ddl=True,
         current_catalog="main",
         current_schema="",
         secondary_catalog="secondary",

Reply via email to