Skip to content

Conversation

@achow101
Copy link
Member

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.

This will be particularly useful in future work which makes use of more complicated SQL statements.

@DrahtBot
Copy link
Contributor

DrahtBot commented Jul 21, 2025

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage & Benchmarks

For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/33033.

Reviews

See the guideline for information on the review process.

Type Reviewers
Concept ACK rkrux
Stale ACK w0xlt, vasild

If your review is incorrectly listed, please copy-paste <!--meta-tag:bot-skip--> into the comment that the bot should ignore.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #33034 (wallet: Store transactions in a separate sqlite table by achow101)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

LLM Linter (✨ experimental)

Possible places where named args for integral literals may be used (e.g. func(x, /*named_arg=*/0) in C++, and func(x, named_arg=0) in Python):

  • sqlite3_prepare_v2(&m_db, stmt_text.c_str(), -1, &m_stmt, nullptr) in src/wallet/sqlite.cpp

2026-01-03

@DrahtBot
Copy link
Contributor

🚧 At least one of the CI tasks failed.
Task previous releases, depends DEBUG: https://github.com/bitcoin/bitcoin/runs/46428794695
LLM reason (✨ experimental): The CI failure is caused by a static assertion failure in the sqlite.cpp file during compilation.

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@achow101 achow101 force-pushed the sqlite-stmt-raii branch 3 times, most recently from e1bf41a to 2c1edcc Compare July 22, 2025 06:11
Copy link
Contributor

@w0xlt w0xlt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

ACK b2c824a

Copy link
Contributor

@rkrux rkrux left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Started reviewing; I'm not opposed to this introduction at the moment and I will get more clarity soon in the review.

Initial Concept ACK 1829b3512d7bf430ec44daa801eb8fadf46cd30b

These 3 commits from #33034 looks pretty generic and related enough. Can they be included in this PR?

  • 818989093d9e6b8ee765c21630f7342b99af7fa0 "sqlite: Make SQLiteStatement::Bind a template function"
  • 07cac74e1340c7e771850633dd9418ff3efa5bb7 "sqlite: Make SQLiteDatabase::Column an std::optional"
  • ff47eea527e58071bdaff5c2acbc98ee97294021 "sqlite: Add additional blob types to SQLiteStatement::Column"

Copy link
Contributor

@vasild vasild left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach ACK 1829b3512d7bf430ec44daa801eb8fadf46cd30b

Comment on lines 72 to 74
int res = sqlite3_prepare_v2(m_db, stmt_text.c_str(), -1, &m_stmt, nullptr);
if (res != SQLITE_OK) {
throw std::runtime_error(strprintf(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an observation:

Some 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 and Invoking sqlite3_finalize() on a NULL pointer is a harmless no-op.

So it is ok to omit the finalize call.

~SQLiteStatement()
{
Reset();
sqlite3_finalize(m_stmt);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

From: https://sqlite.org/c3ref/finalize.html

If the most recent evaluation of statement S failed, then sqlite3_finalize(S) returns the appropriate error code or extended error code

I am not sure what is the best way to handle this. Ignoring the return value does not look good.

Maybe 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()?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe (not tested):

    int Step()
    {
        const int ret = sqlite3_step(m_stmt);
        if (ret == SQLITE_ROW || ret == SQLITE_DONE) {
            return ret;
        }
        sqlite3_finalize(m_stmt);
        m_stmt = nullptr;
        // then the destructor can assert(sqlite3_finalize(m_stmt) == SQLITE_OK);
    }

Related to #33033 (comment)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the most recent evaluation of statement S failed, then sqlite3_finalize(S) returns the appropriate error code or extended error code

I am not sure what is the best way to handle this. Ignoring the return value does not look good.

This 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.

Copy link
Contributor

@vasild vasild Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Edit: if you retouch, maybe change int Step() to [[nodiscard]] int Step() to hint the callers to not ignore the return value.

Copy link
Contributor

@vasild vasild left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 0af7c2a11819a21e11abf34b4229c5c21150d827

@DrahtBot
Copy link
Contributor

DrahtBot commented Jan 2, 2026

🚧 At least one of the CI tasks failed.
Task test max 6 ancestor commits: https://github.com/bitcoin/bitcoin/actions/runs/20667709652/job/59343191929
LLM reason (✨ experimental): Compilation failed: use of undeclared identifier 'blob' in wallet/sqlite.cpp causing build error.

Hints

Try to run the tests locally, according to the documentation. However, a CI failure may still
happen due to a number of reasons, for example:

  • Possibly due to a silent merge conflict (the changes in this pull request being
    incompatible with the current code in the target branch). If so, make sure to rebase on the latest
    commit of the target branch.

  • A sanitizer issue, which can only be found by compiling with the sanitizer and running the
    affected test.

  • An intermittent issue.

Leave a comment here, if you need help tracking down a confusing failure.

@achow101 achow101 force-pushed the sqlite-stmt-raii branch 2 times, most recently from 0fcc63f to d283190 Compare January 3, 2026 02:37
Instead of constructing all SQLiteStatements when SQLiteBatch is
constructed, construct them once when they are needed before each read,
write, or erase operation. Once constructed, the statement will persist
for the lifetime of the SQLiteBatch to be reused across multiple
statements.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants