{
 "number": 33033,
 "repo": "bitcoin/bitcoin",
 "url": "https://github.com/bitcoin/bitcoin/pull/33033",
 "title": "wallet, sqlite: Encapsulate SQLite statements in a RAII class",
 "author": "achow101",
 "author_association": "MEMBER",
 "created_at": "2025-07-21T22:43:08Z",
 "updated_at": "2026-09-17T10:21:18Z",
 "age_days": 422,
 "draft": false,
 "labels": [],
 "milestone": null,
 "base": "master",
 "head_sha": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
 "head_ref": "sqlite-stmt-raii",
 "head_repo": "achow101/bitcoin",
 "head_history": [
  {
   "t": "2025-07-22T03:05:45Z",
   "sha": "7fe3a8ed9cc2c987e36a8aeb0a2cdf76fe5b1610"
  },
  {
   "t": "2025-07-22T03:16:53Z",
   "sha": "e1bf41a5aa1b90659034e441446569cda507069f"
  },
  {
   "t": "2025-07-22T06:11:43Z",
   "sha": "2c1edccfc5b5a1ef792248cba3b7291c3ee55fb6"
  },
  {
   "t": "2025-07-22T17:00:53Z",
   "sha": "b2c824a3ce0877f2cb7cbed1731088ed8ba6171a"
  },
  {
   "t": "2025-07-24T20:30:04Z",
   "sha": "a8cf39607e9f3425a99492e3bb7d798f8a139cb8"
  },
  {
   "t": "2025-07-24T20:50:46Z",
   "sha": "1829b3512d7bf430ec44daa801eb8fadf46cd30b"
  },
  {
   "t": "2025-11-17T21:26:02Z",
   "sha": "0af7c2a11819a21e11abf34b4229c5c21150d827"
  },
  {
   "t": "2025-12-10T18:42:29Z",
   "sha": "637770cdfe03507d2d267367fa0ced1586a5c49d"
  },
  {
   "t": "2026-01-02T22:11:18Z",
   "sha": "71b7c848e485ba4fdba6c0d0b6d7b33bd58c99d3"
  },
  {
   "t": "2026-01-02T23:50:18Z",
   "sha": "c5a5d062f6a96747589faddc80ab2575dfa76454"
  },
  {
   "t": "2026-01-03T01:25:14Z",
   "sha": "0fcc63f6616b40e2880b7b95cc561ec2c18eed34"
  },
  {
   "t": "2026-01-03T02:37:47Z",
   "sha": "d283190138f110131e9a27370707252066695236"
  },
  {
   "t": "2026-01-03T03:14:55Z",
   "sha": "1682818a7b19c7c391fe4a75d96f620b1ac5127c"
  },
  {
   "t": "2026-08-26T19:48:47Z",
   "sha": "c31076ed9a2b1b7b08a178e8c6c054facc38e45d"
  },
  {
   "t": "2026-08-26T21:15:47Z",
   "sha": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3"
  }
 ],
 "additions": 230,
 "deletions": 212,
 "changed_files": 2,
 "commit_count": 10,
 "size_bucket": "L",
 "mergeable_state": "clean",
 "bot": {
  "drahtbot": {
   "present": true,
   "reviews": {
    "concept_ack": [
     {
      "login": "rkrux",
      "url": "https://github.com/bitcoin/bitcoin/pull/33033#pullrequestreview-3404260829"
     }
    ],
    "stale_ack": [
     {
      "login": "w0xlt",
      "url": "https://github.com/bitcoin/bitcoin/pull/33033#pullrequestreview-3049382506"
     },
     {
      "login": "vasild",
      "url": "https://github.com/bitcoin/bitcoin/pull/33033#pullrequestreview-3487577017"
     },
     {
      "login": "pablomartin4btc",
      "url": "https://github.com/bitcoin/bitcoin/pull/33033#pullrequestreview-4790064267"
     }
    ]
   },
   "conflicts": [
    {
     "number": 33034,
     "title": "wallet: Store transactions in a separate sqlite table",
     "author": "achow101"
    }
   ]
  }
 },
 "acks_parsed": {
  "vasild": {
   "kind": "ack",
   "hash": "0af7c2a11819a21e11abf34b4229c5c21150d827",
   "t": "2025-11-20T12:23:54Z",
   "stale": true
  },
  "pablomartin4btc": {
   "kind": "ack",
   "hash": "1682818a7b19",
   "t": "2026-07-27T20:16:48Z",
   "stale": true
  }
 },
 "acks_tally": {
  "ack": 0,
  "stale_ack": 2,
  "concept_ack": 0,
  "approach_ack": 0,
  "nack": 0,
  "concept_nack": 0,
  "approach_nack": 0
 },
 "reviews": {
  "approved": 1,
  "changes_requested": 0,
  "distinct_reviewers": [
   "Eunovo",
   "davidgumberg",
   "maflcko",
   "pablomartin4btc",
   "rkrux",
   "sedited",
   "vasild",
   "w0xlt"
  ]
 },
 "signals": {
  "needs_rebase": false,
  "ci_failed": false,
  "mergeable_state": "clean",
  "last_author_activity": "2026-08-26T21:15:47Z",
  "last_reviewer_activity": "2026-07-27T20:16:48Z",
  "last_reviewer": "pablomartin4btc",
  "author_silent_days": 21,
  "waiting_on_author_days": 0,
  "days_since_update": 0
 },
 "refs": {
  "mentioned": [
   33034
  ],
  "depends_on": [],
  "fixes": [],
  "linked_issues": [],
  "references": [
   {
    "number": 33034,
    "type": "pull",
    "state": "open",
    "merged": false,
    "merged_at": null,
    "title": "wallet: Store transactions in a separate sqlite table"
   }
  ],
  "conflicts": [
   33034
  ]
 },
 "stack": {
  "shares_commits_with": [],
  "based_on": [],
  "base_for": []
 },
 "review_paths": [
  "src/util/transaction_identifier.h",
  "src/wallet/sqlite.cpp",
  "src/wallet/sqlite.h"
 ],
 "body": "SQLite statements are C objects which can be long lived, so having them be initialized in a constructor, and cleanup everything in a destructor, makes the sqlite code cleaner and easier to read. This also lets us have the various functions needed to interact with sqlite statements be member functions.\n\nThis will be particularly useful in future work which makes use of more complicated SQL statements.",
 "commits": [
  {
   "sha": "4ace00b4c6cd015901f5dcf6e503601c73b54605",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Add SQLiteStatement RAII class\n\nThis class will be used to encapsulate a sqlite3_stmt"
  },
  {
   "sha": "244554b20f8425357dfa4abdc177624192e0f1dd",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Make Column template function"
  },
  {
   "sha": "9f5de78b165647d3433f5fa6dffb4e1d73a08fd8",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Refactor ReadPragmaInteger to use SQLiteStatement"
  },
  {
   "sha": "445a7a5e6fccc49fcd1231195ce0077807194b00",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Use SQLiteStatement in PRAGMA integrity_check"
  },
  {
   "sha": "f4dfa898c39cc53dc00403750a39c7f9f689eb18",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Use SQLiteStatement in check_main_stmt"
  },
  {
   "sha": "9ae93762fc4fbe3e3744d6bd9e2916fbde2f8790",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Have SQLiteCursor store SQLiteStatement"
  },
  {
   "sha": "74d1409e333cd8a02bbe272f88b4d065b19e1159",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Replace remaining sqlite3_stmt usage with SQLiteStatement"
  },
  {
   "sha": "84dd0c3848b51e03bffae8050ae23994c194cdbe",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Refactor common writing code from WriteKey and ExecStatement\n\nWriteKey and ExecStatement use the same code for the actual execution of\nthe statement. This is refactored into a separate function, also called\nExecStatement, and the original ExecStatement renamed to\nExecEraseStatement as it is only used by the erase functions."
  },
  {
   "sha": "526df3bfe92c7075d4358f81dcd5cdbeb25f9c01",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Inline BindBlobToStatement and SpanFromBlob"
  },
  {
   "sha": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "date": "2026-08-26T21:15:41Z",
   "message": "sqlite: Construct SQLiteStatements when needed\n\nInstead of constructing all SQLiteStatements when SQLiteBatch is\nconstructed, construct them once when they are needed before each read,\nwrite, or erase operation. Once constructed, the statement will persist\nfor the lifetime of the SQLiteBatch to be reused across multiple\nstatements."
  }
 ],
 "timeline": [
  {
   "t": "2025-07-22T03:05:45Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "7fe3a8ed9cc2c987e36a8aeb0a2cdf76fe5b1610"
  },
  {
   "t": "2025-07-22T03:16:53Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "e1bf41a5aa1b90659034e441446569cda507069f"
  },
  {
   "t": "2025-07-22T06:11:43Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "2c1edccfc5b5a1ef792248cba3b7291c3ee55fb6"
  },
  {
   "t": "2025-07-22T08:07:39Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "nit: you can  use `ALWAYS_FALSE` for this, so that the workaround docs for the  C++23 defect report are in one place only."
  },
  {
   "t": "2025-07-22T17:00:53Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "b2c824a3ce0877f2cb7cbed1731088ed8ba6171a"
  },
  {
   "t": "2025-07-22T17:01:15Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2221607401,
   "text": "Done"
  },
  {
   "t": "2025-07-23T21:00:19Z",
   "kind": "review_comment",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "path": "src/util/transaction_identifier.h",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "Is there any specific reason not to use `unsigned char` directly, like `std::span<const unsigned char > sp)` ?"
  },
  {
   "t": "2025-07-23T21:15:55Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/util/transaction_identifier.h",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2226679396,
   "text": "In theory we could change `uint256`'s `value_type`, and we wouldn't need to make any changes here in that case."
  },
  {
   "t": "2025-07-23T23:03:08Z",
   "kind": "review",
   "who": "w0xlt",
   "assoc": "CONTRIBUTOR",
   "state": "COMMENTED",
   "commit": "b2c824a3ce0877f2cb7cbed1731088ed8ba6171a",
   "text": "Looks good to me\n\nACK https://github.com/bitcoin/bitcoin/pull/33033/commits/b2c824a3ce0877f2cb7cbed1731088ed8ba6171a"
  },
  {
   "t": "2025-07-24T07:13:54Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "encapsualtes -> encapsulates [spelling error in comment \u201cencapsualtes\u201d makes the word misspelled]"
  },
  {
   "t": "2025-07-24T07:58:10Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/util/transaction_identifier.h",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2226679396,
   "text": "I don't understand this change. The commit message just says \"util: Add value_type and span constructor to (W)Txid\", but it doesn't say why or where this is needed. Compiling without this (locally) seems to pass.\n\nAlso, conceptually, it seems to go in the opposite direction that we want to go in. This makes implicit construction from any byte span to `transaction_identifier` possible again. The constructor `transaction_identifier(const uint256& wrapped)` is private and will still catch those cases, but if this implementation detail is changed in the future, I presume direct and implicit construction from uint256 will be possible again.\n\nI think it would be good to explain this change a bit better for reviewers."
  },
  {
   "t": "2025-07-24T17:40:19Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/util/transaction_identifier.h",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2226679396,
   "text": "In `SQLiteStatement::Column<T>()`, one of the ways we construct the appropriate data from a blob is to do `reinterpret_cast<T::value_type>`. `Txid` met all of the requirements for using that constructor, except that it didn't have a `value_type`, hence this commit adding it.\n\nBut it looks like I didn't include that commit in this PR. Rather, it's https://github.com/bitcoin/bitcoin/pull/33034/commits/afe99b3b7df6c658fb4068c6418dfd7ebf151460 in #33034."
  },
  {
   "t": "2025-07-24T19:50:09Z",
   "kind": "review_comment",
   "who": "maflcko",
   "assoc": "MEMBER",
   "path": "src/util/transaction_identifier.h",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2226679396,
   "text": "I see. Though, it may still be good to make this one `explicit` to avoid silent uint256 conversions in the future?\n\nAlso, `Txid::data` is `std::byte*`, so `value_type` should probably be `std::byte` as well.\n\nI haven't tried, but I presume both modifications should compile with afe99b3b7df6c658fb4068c6418dfd7ebf151460"
  },
  {
   "t": "2025-07-24T20:30:04Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "a8cf39607e9f3425a99492e3bb7d798f8a139cb8"
  },
  {
   "t": "2025-07-24T20:49:51Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/util/transaction_identifier.h",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2226679396,
   "text": "I've dropped this commit from this PR and moved it to #33034. Also dropped `value_type` and made the constructor just take `span<const std::byte>` and that seems to work."
  },
  {
   "t": "2025-07-24T20:50:46Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b"
  },
  {
   "t": "2025-07-24T20:50:47Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2227638927,
   "text": "Done"
  },
  {
   "t": "2025-10-31T13:23:16Z",
   "kind": "review_comment",
   "who": "rkrux",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.h",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "In d13ffed94ad5f5d488f8d72e4d5d7f1c3fb27c2b \"sqlite: Have SQLiteCursor store SQLiteStatement\"\n\nIt doesn't seem to be used anymore here; builds and tests fine without it.\n\n ```diff\n--- a/src/wallet/sqlite.h\n+++ b/src/wallet/sqlite.h\n@@ -12,7 +12,6 @@\n\n struct bilingual_str;\n\n-struct sqlite3_stmt;\n struct sqlite3;\n\n namespace wallet {\n```"
  },
  {
   "t": "2025-10-31T14:22:57Z",
   "kind": "review",
   "who": "rkrux",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "text": "Started reviewing; I'm not opposed to this introduction at the moment and I will get more clarity soon in the review.\n\nInitial Concept ACK 1829b3512d7bf430ec44daa801eb8fadf46cd30b\n\nThese 3 commits from #33034 looks pretty generic and related enough. Can they be included in this PR?\n- 818989093d9e6b8ee765c21630f7342b99af7fa0 \"sqlite: Make SQLiteStatement::Bind a template function\"\n- 07cac74e1340c7e771850633dd9418ff3efa5bb7 \"sqlite: Make SQLiteDatabase::Column an std::optional\"\n- ff47eea527e58071bdaff5c2acbc98ee97294021 \"sqlite: Add additional blob types to SQLiteStatement::Column\""
  },
  {
   "t": "2025-11-12T10:04:52Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "Looks like `m_db` can never be `nullptr` since it is initialized by the constructor to a presumably non-null value. Consider making this a reference `sqlite3& m_db;`. That would signal to the readers that it can never be null and will also make it impossible to have weird states of this class that somehow end up having a null member."
  },
  {
   "t": "2025-11-12T10:12:57Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "Just an observation:\n\nSome of the callers of `sqlite3_prepare_v2()` call `sqlite3_finalize()` on failure. The doc says that if prepare fails then [*ppStmt is set to NULL](https://sqlite.org/c3ref/prepare.html) and [Invoking sqlite3_finalize() on a NULL pointer is a harmless no-op](https://sqlite.org/c3ref/finalize.html).\n\nSo it is ok to omit the finalize call."
  },
  {
   "t": "2025-11-12T13:17:36Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "The class name is `SQLiteStatement`, is this intentionally `SQLiteDatabase`?"
  },
  {
   "t": "2025-11-12T13:18:44Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "I think it would be useful to print the statement as well (`stmt_text`)."
  },
  {
   "t": "2025-11-12T13:24:07Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": null,
   "text": "`Reset()` is not needed here?"
  },
  {
   "t": "2025-11-12T13:35:36Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": null,
   "text": "From https://sqlite.org/c3ref/column_blob.html:\n[quoted text omitted]\n\nMaybe at the beginning of the function, add:\n\n```cpp\nAssert(col < sqlite3_column_count());\n```"
  },
  {
   "t": "2025-11-12T13:52:18Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": null,
   "text": "What happens if `T` is unsigned? Maybe add https://en.cppreference.com/w/cpp/types/is_signed.html here: `std::integral<T> && std::is_signed<T>`? (and leave it to drop to \"else assert always false\" for unsigned `T`s)"
  },
  {
   "t": "2025-11-12T14:14:55Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "Previously on error `ReadPragmaInteger()` would have returned `nullopt`. Now it would `throw`. Its only caller is `SQLiteDatabase::Verify()` which would have returned `false` and now would let the exception propagate. The only caller of `SQLiteDatabase::Verify()` is `MakeSQLiteDatabase()`. Before, on `Verify()` failure (return `false`) `MakeSQLiteDatabase()` would have set `status = DatabaseStatus::FAILED_VERIFY` and now (exception) it would set `status = DatabaseStatus::FAILED_LOAD`. Is this ok?"
  },
  {
   "t": "2025-11-12T14:21:58Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "Now `ReadPragmaInteger()` would return `nullopt` on some errors and `throw` on others. What about making it to only `throw`? Then its return type would change from `std::optional<int>` to `int` and its callers would not need to check `if (!read_result.has_value()) ...`.\n\nI guess also `SQLiteStatement::Step()` can `throw` if the return value of `sqlite3_step()` is not `SQLITE_ROW` or `SQLITE_DONE`."
  },
  {
   "t": "2025-11-12T14:43:59Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": null,
   "text": "`sqlite3_column_text()` can return `nullptr`, better check that. The previous caller did check:\n\n```diff\n-        const char* msg = (const char*)sqlite3_column_text(stmt, 0);\n-        if (!msg) {\n-            error = ...\n```"
  },
  {
   "t": "2025-11-12T14:54:05Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "nit, here and in the other constructor:\n\n```cpp\nSQLiteCursor::SQLiteCursor(sqlite3* db, const std::string& stmt_text)\n    : m_cursor_stmt(std::make_unique<SQLiteStatement>(db, stmt_text)) {}\n```"
  },
  {
   "t": "2025-11-12T15:18:58Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": null,
   "text": "The second element in the pair (`stmt_description`) is not used. This snippet can be reduced to:\n\n```cpp\n    for (auto stmt : {&m_read_stmt, &m_insert_stmt, &m_overwrite_stmt, &m_delete_stmt, &m_delete_prefix_stmt}) {\n        stmt->reset();\n    }\n```\n\nOr simple stupid:\n```cpp\n    m_read_stmt.reset();\n    m_insert_stmt.reset();\n    m_overwrite_stmt.reset();\n    m_delete_stmt.reset();\n    m_delete_prefix_stmt.reset();\n```"
  },
  {
   "t": "2025-11-12T15:28:27Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": null,
   "text": "Some of the callers of `sqlite3_finalize()` used to check its return value. Here in the destructor it is too late to signal an error to the caller.\n\nFrom: https://sqlite.org/c3ref/finalize.html\n[quoted text omitted]\n\nI am not sure what is the best way to handle this. Ignoring the return value does not look good.\n\nMaybe assert that `sqlite3_finalize()` returns `SQLITE_OK`? And also somehow make sure that if \"the most recent evaluation of statement S failed\", then this is handled already, before the destructor is called. That would mean if `sqlite3_step()`, called from `Step()` fails (result is not `SQLITE_ROW` or `SQLITE_DONE`) then to call `sqlite3_finalize()` from inside `Step()`?"
  },
  {
   "t": "2025-11-12T16:21:35Z",
   "kind": "review",
   "who": "vasild",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "text": "Approach ACK 1829b3512d7bf430ec44daa801eb8fadf46cd30b"
  },
  {
   "t": "2025-11-14T06:02:56Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 2518767761,
   "text": "Maybe (not tested):\n\n```cpp\n    int Step()\n    {\n        const int ret = sqlite3_step(m_stmt);\n        if (ret == SQLITE_ROW || ret == SQLITE_DONE) {\n            return ret;\n        }\n        sqlite3_finalize(m_stmt);\n        m_stmt = nullptr;\n        // then the destructor can assert(sqlite3_finalize(m_stmt) == SQLITE_OK);\n    }\n```\nRelated to https://github.com/bitcoin/bitcoin/pull/33033#discussion_r2518504627"
  },
  {
   "t": "2025-11-17T21:11:47Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 2518767761,
   "text": "[quoted text omitted]\n\nThis text reads to me as `sqlite3_finalize` propagating any previous errors, e.g. from `sqlite3_step`. Since any errors from that should already be handled, I think it's ok to ignore the result of finalize. I think the previous code is actually incorrect since the error result is not actually a result of finalize failing."
  },
  {
   "t": "2025-11-17T21:26:02Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "0af7c2a11819a21e11abf34b4229c5c21150d827"
  },
  {
   "t": "2025-11-17T21:26:15Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.h",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2481404565,
   "text": "Done"
  },
  {
   "t": "2025-11-17T21:26:22Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2517689053,
   "text": "Done"
  },
  {
   "t": "2025-11-17T21:26:41Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2518277982,
   "text": "Done"
  },
  {
   "t": "2025-11-17T21:26:45Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2518281428,
   "text": "Done"
  },
  {
   "t": "2025-11-17T21:26:53Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 2518298601,
   "text": "Removed"
  },
  {
   "t": "2025-11-17T21:26:58Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 2518340792,
   "text": "Done"
  },
  {
   "t": "2025-11-17T21:27:04Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 2518397420,
   "text": "Done"
  },
  {
   "t": "2025-11-17T21:27:27Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2518476250,
   "text": "I've changed this to catch the exception and return `std::nullopt`."
  },
  {
   "t": "2025-11-17T21:27:42Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2518504627,
   "text": "Changed to catch the exception."
  },
  {
   "t": "2025-11-17T21:28:25Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": 2518594590,
   "text": "Done, returns an empty string now.\n\nBut the only time `sqlite3_column_text` should return NULL is if the column contains a NULL value. Empty strings are not stored as NULL."
  },
  {
   "t": "2025-11-17T21:28:37Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2518633025,
   "text": "Done"
  },
  {
   "t": "2025-11-17T21:28:43Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1829b3512d7bf430ec44daa801eb8fadf46cd30b",
   "in_reply_to": 2518729492,
   "text": "Done"
  },
  {
   "t": "2025-11-20T12:23:54Z",
   "kind": "review",
   "who": "vasild",
   "assoc": "MEMBER",
   "state": "APPROVED",
   "commit": "0af7c2a11819a21e11abf34b4229c5c21150d827",
   "text": "ACK 0af7c2a11819a21e11abf34b4229c5c21150d827"
  },
  {
   "t": "2025-11-20T12:38:39Z",
   "kind": "review_comment",
   "who": "vasild",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 2518767761,
   "text": "Yeah, I was thinking the same. It is just super odd to ignore database errors (in general). I guess the sqlite API is odd here.\n\nEdit: if you retouch, maybe change `int Step()` to `[[nodiscard]] int Step()` to hint the callers to not ignore the return value."
  },
  {
   "t": "2025-12-10T18:42:29Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "637770cdfe03507d2d267367fa0ced1586a5c49d"
  },
  {
   "t": "2025-12-24T22:15:06Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "6941435aadfc65aa423e73e44a9c6747b1023a01",
   "in_reply_to": null,
   "text": "nit: IMO this is neater with `ExecEraseStatement` removed and\n\n```cpp\n    if (!m_database.m_db) return false;\n    assert(stmt);\n\n    // Bind: leftmost parameter in statement is index 1\n    if (!stmt->Bind(1, blob, \"key\")) return false;\n```\n\nrepeated in `EraseKey` and `ErasePrefix`,  in total it's only ~2 more LOC.\n\n `ExecEraseStatement()`  internally has nothing to do with erasing, except that it happens to be the case that both eraseprefix and erasekey statements only have one parameter."
  },
  {
   "t": "2025-12-24T23:56:00Z",
   "kind": "review_comment",
   "who": "davidgumberg",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "138e334d505bacbb48e2059757b471543b738c34",
   "in_reply_to": null,
   "text": "I wonder if the introduction of `SQLiteStatement` means we can do better than initializing every statement as a member of every batch: Given that we are `Reset()`'ing statements every single time they are used, and AFAICT batches tend to be short-lived, I think it would be cheaper and less complicated to initialize statement objects just before they are used in the various `Read*()` `Erase*()` , and it takes better advantage of the fact that statements are RAII than manually calling a closing function in Batch's destructor and asserting at the beginning of every `WalletBatch()` function that the relevant `stmt` object exists, here's one take of how something like this could be done:\n\nhttps://github.com/davidgumberg/bitcoin/commit/b67e0301fd30564188470a29d3c10df2ff561e9c"
  },
  {
   "t": "2026-01-02T22:09:24Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "6941435aadfc65aa423e73e44a9c6747b1023a01",
   "in_reply_to": 2646309999,
   "text": "Done"
  },
  {
   "t": "2026-01-02T22:11:09Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "138e334d505bacbb48e2059757b471543b738c34",
   "in_reply_to": 2646362930,
   "text": "We still want to have the statements persist across multiple queries in a single `SQLiteBatch` to avoid sqlite having to parse the same statement text over and over. However, we can construct the `SQLiteStatement` on first use instead of always, so I've added a commit to do that."
  },
  {
   "t": "2026-01-02T22:11:18Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "71b7c848e485ba4fdba6c0d0b6d7b33bd58c99d3"
  },
  {
   "t": "2026-01-02T23:50:18Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "c5a5d062f6a96747589faddc80ab2575dfa76454"
  },
  {
   "t": "2026-01-03T01:25:14Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "0fcc63f6616b40e2880b7b95cc561ec2c18eed34"
  },
  {
   "t": "2026-01-03T02:37:47Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "d283190138f110131e9a27370707252066695236"
  },
  {
   "t": "2026-01-03T03:14:55Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c"
  },
  {
   "t": "2026-07-24T07:46:53Z",
   "kind": "comment",
   "who": "sedited",
   "assoc": "MEMBER",
   "text": "Ping @davidgumberg @vasild for hopefully another round of review."
  },
  {
   "t": "2026-07-24T12:47:40Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/33033/commits/b79895c87a3e50de09f7a61887f1389030a94823:\n\nI wonder if `Bind` should throw an `Exception` instead? It doesn't look like we can recover from a failure to `sqlite3_bind_blob`."
  },
  {
   "t": "2026-07-24T12:58:49Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": null,
   "text": "https://github.com/bitcoin/bitcoin/pull/33033/commits/bfa96f8c12ba40fbc66499fa72251bec9e3367e1:\n\nI think it's useful to document that `SQLiteStatement` can only handle one statement in `stmt_text` or, preferably, provide the `pzTail` argument and throw an error if `pzTail` is not Null after `sqlite3_prepare_v2` (this indicates that there is uncompiled SQL code left)."
  },
  {
   "t": "2026-07-24T13:01:08Z",
   "kind": "review",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "text": "https://github.com/bitcoin/bitcoin/pull/33033/commits/1682818a7b19c7c391fe4a75d96f620b1ac5127c:\n\nLooks good to me. Left some comments."
  },
  {
   "t": "2026-07-24T19:09:50Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 3645398459,
   "text": "Not all callers of this will have exception handling, but they all should have handling if it fails."
  },
  {
   "t": "2026-07-24T19:13:38Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": 3645464121,
   "text": "If I need to retouch."
  },
  {
   "t": "2026-07-27T07:36:05Z",
   "kind": "review_comment",
   "who": "Eunovo",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 3645398459,
   "text": "Looks like a good candidate for `[[nodiscard]]`"
  },
  {
   "t": "2026-07-27T18:04:35Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.h",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": null,
   "text": "nit: `ExecEraseStatement` is never used nor implemented.\n```suggestion\n```"
  },
  {
   "t": "2026-07-27T18:08:11Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": null,
   "text": "nit:\n```suggestion\n            return std::string(text, size);\n```"
  },
  {
   "t": "2026-07-27T18:20:38Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": null,
   "text": "nit (more consistently within the repo):\n```suggestion\n    } catch (const std::runtime_error& e) {\n```"
  },
  {
   "t": "2026-07-27T18:21:13Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": null,
   "text": "nit:\n```suggestion\n        return pragma_read_stmt.Column<int>(0);\n```"
  },
  {
   "t": "2026-07-27T18:49:10Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": null,
   "text": "nit: same as [here](https://github.com/bitcoin/bitcoin/pull/33033/changes#r3659824157)."
  },
  {
   "t": "2026-07-27T18:55:54Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": null,
   "text": "worth considering a `LogWarning` or at least a doc comment?"
  },
  {
   "t": "2026-07-27T20:15:53Z",
   "kind": "review_comment",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.h",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": null,
   "text": "nit: per [doc/developer-notes.md](https://github.com/bitcoin/bitcoin/blob/master/doc/developer-notes.md), prefer `enum class` over plain `enum`.\n```suggestion\n    enum class SQLiteStatementType\n```"
  },
  {
   "t": "2026-07-27T20:16:48Z",
   "kind": "review",
   "who": "pablomartin4btc",
   "assoc": "MEMBER",
   "state": "COMMENTED",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "text": "ACK 1682818a7b19\n\nLeft some inline nits. Two broader observations I think worth noting:\n- `GetNewPrefixCursor` bind failure is now a throw instead of `nullptr` -\nPreviously `GetNewPrefixCursor` returned `nullptr` when `BindBlobToStatement` failed, and callers handled that. Now the `SQLiteCursor` constructor throws `std::runtime_error` if either bind call fails, and no caller catches it \u2014 so it propagates up the stack. Also, since this PR introduces the behaviour change, wouldn't be natural to clean up the now-dead null-checks in `walletdb.cpp` (lines 193, 470, 523)?\n\n- `sqlite3_prepare_v2` ignores trailing SQL (`pzTail`) -\nThe `SQLiteStatement` constructor passes `nullptr` as the `pzTail` out-parameter, so if `stmt_text` ever contained more than one statement (e.g. a stray semicolon \u2014 `\"INSERT INTO main VALUES(?,?); DROP TABLE main\"`), SQLite would silently prepare only the first and discard the rest (@Eunovo [raised](https://github.com/bitcoin/bitcoin/pull/33033/changes#r3645464121) it also)."
  },
  {
   "t": "2026-08-26T19:40:11Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 3660058172,
   "text": "No. A column that stores a string could contain nothing in it if there is no string to store, which is equivalent to the empty string."
  },
  {
   "t": "2026-08-26T19:48:47Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "c31076ed9a2b1b7b08a178e8c6c054facc38e45d"
  },
  {
   "t": "2026-08-26T19:49:08Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 3645398459,
   "text": "Done"
  },
  {
   "t": "2026-08-26T19:49:14Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": 3645464121,
   "text": "Done"
  },
  {
   "t": "2026-08-26T19:49:27Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.h",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
   "in_reply_to": 3659721163,
   "text": "Removed"
  },
  {
   "t": "2026-08-26T19:49:33Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": 3659743808,
   "text": "Done"
  },
  {
   "t": "2026-08-26T19:49:36Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": 3659824157,
   "text": "Done"
  },
  {
   "t": "2026-08-26T19:49:45Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.cpp",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": 3659827736,
   "text": "Done"
  },
  {
   "t": "2026-08-26T19:49:51Z",
   "kind": "review_comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "path": "src/wallet/sqlite.h",
   "commit": "1682818a7b19c7c391fe4a75d96f620b1ac5127c",
   "in_reply_to": 3660597709,
   "text": "Done"
  },
  {
   "t": "2026-08-26T19:51:13Z",
   "kind": "comment",
   "who": "achow101",
   "assoc": "MEMBER",
   "text": "[quoted text omitted]\n\n`GetNewPrefixCursor` can still return `nullptr`."
  },
  {
   "t": "2026-08-26T21:15:47Z",
   "kind": "force_push",
   "who": "achow101",
   "commit": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3"
  }
 ],
 "labels_log": [
  {
   "t": "2025-07-22T02:33:16Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2025-07-22T09:49:13Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2025-12-02T14:56:04Z",
   "action": "labeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2025-12-10T19:52:36Z",
   "action": "unlabeled",
   "label": "Needs rebase",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-02T23:35:56Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-01-03T04:31:31Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-26T21:16:38Z",
   "action": "labeled",
   "label": "CI failed",
   "who": "DrahtBot"
  },
  {
   "t": "2026-08-26T22:51:03Z",
   "action": "unlabeled",
   "label": "CI failed",
   "who": "DrahtBot"
  }
 ],
 "state_log": [],
 "text_chars": 14356,
 "text_tokens_estimate": 3589,
 "changed_paths": [
  "src/wallet/sqlite.cpp",
  "src/wallet/sqlite.h"
 ],
 "files": [
  {
   "path": "src/wallet/sqlite.cpp",
   "add": 206,
   "del": 197
  },
  {
   "path": "src/wallet/sqlite.h",
   "add": 24,
   "del": 15
  }
 ],
 "test_lines": 0,
 "git": {
  "head": "4ba56542a854e8f7bb5d7e64e178d6d5d852f8e3",
  "head_matches_backup": true,
  "base": "a24110cef7fa36138620f1876cd0722944618d07",
  "commits": [
   {
    "sha": "4ace00b4c6",
    "subject": "sqlite: Add SQLiteStatement RAII class",
    "files": 1,
    "add": 49,
    "del": 0
   },
   {
    "sha": "244554b20f",
    "subject": "sqlite: Make Column template function",
    "files": 1,
    "add": 19,
    "del": 2
   },
   {
    "sha": "9f5de78b16",
    "subject": "sqlite: Refactor ReadPragmaInteger to use SQLiteStatement",
    "files": 1,
    "add": 13,
    "del": 17
   },
   {
    "sha": "445a7a5e6f",
    "subject": "sqlite: Use SQLiteStatement in PRAGMA integrity_check",
    "files": 1,
    "add": 29,
    "del": 29
   },
   {
    "sha": "f4dfa898c3",
    "subject": "sqlite: Use SQLiteStatement in check_main_stmt",
    "files": 1,
    "add": 3,
    "del": 10
   },
   {
    "sha": "9ae93762fc",
    "subject": "sqlite: Have SQLiteCursor store SQLiteStatement",
    "files": 2,
    "add": 26,
    "del": 41
   },
   {
    "sha": "74d1409e33",
    "subject": "sqlite: Replace remaining sqlite3_stmt usage with SQLiteStatement",
    "files": 2,
    "add": 43,
    "del": 67
   },
   {
    "sha": "84dd0c3848",
    "subject": "sqlite: Refactor common writing code from WriteKey and ExecStatement",
    "files": 2,
    "add": 30,
    "del": 36
   },
   {
    "sha": "526df3bfe9",
    "subject": "sqlite: Inline BindBlobToStatement and SpanFromBlob",
    "files": 1,
    "add": 17,
    "del": 29
   },
   {
    "sha": "4ba56542a8",
    "subject": "sqlite: Construct SQLiteStatements when needed",
    "files": 2,
    "add": 34,
    "del": 14
   }
  ],
  "patch_truncated": false
 },
 "input_hash": "d54a3b618b9a7f67",
 "extracted_at": "2026-09-17T16:15:31+00:00"
}