[sqlite] Formal review of Boost.SQLite begins

Dear Boost community, The review of Boost.SQLite by Klemens Morgenstern begins today, Monday, August 25, and runs through Wednesday, September 3, 2025. Boost.SQLite provides a safer and more expressive C++ interface to the SQLite3 C-API to simplify usage and reduce errors. You can read the documentation at: https://klemens.dev/sqlite and study or try out the code at: https://github.com/klemens-morgenstern/sqlite This is the second review attempt. The first concluded without a result due to too few submitted reviews. Anyone with experience in C++ or SQLite, at any level, is encouraged to contribute. In your review, please indicate how much time you spent on the evaluation. Here are some questions you might find helpful when writing your review: - What is your evaluation of the design? - What is your evaluation of the implementation? - What is your evaluation of the documentation? - What is your evaluation of the potential usefulness of the library? Do you already use it in industry? - Did you try to use the library? With which compiler(s)? Did you have any problems? - How much effort did you put into your evaluation? A glance? A quick reading? In-depth study? - Are you knowledgeable about the problem domain? At the end of your review, please state clearly whether you ACCEPT, REJECT, or CONDITIONALLY ACCEPT (with explicit conditions) the proposed Boost.SQLite library. Your feedback is critical to the success of this review. Thank you in advance for your time, expertise, and contributions. Best regards, Mohammad Nejati Review Manager for the proposed Boost.SQLite

On Mon, Aug 25, 2025 at 6:53 AM Mohammad Nejati [ashtum] via Boost <boost@lists.boost.org> wrote:
This is the second review attempt. The first concluded without a result due to too few submitted reviews.
Hi. Could we please have a summary a changes, if any, compared to the 1st review? Any review feedback incorporated?

On Mon, Aug 25, 2025 at 1:55 AM Dominique Devienne via Boost < boost@lists.boost.org> wrote:
Hi. Could we please have a summary a changes, if any, compared to the 1st review? Any review feedback incorporated?
It's definitely worth bringing up that this question should be addressed at some point in time. A summary of any changes from the previous review would be a huge boon for reviewers this time around. - Christian

On Mon, Aug 25, 2025 at 4:54 AM Dominique Devienne via Boost < boost@lists.boost.org> wrote:
On Mon, Aug 25, 2025 at 6:53 AM Mohammad Nejati [ashtum] via Boost <boost@lists.boost.org> wrote:
This is the second review attempt. The first concluded without a result due to too few submitted reviews.
Hi. Could we please have a summary a changes, if any, compared to the 1st review? Any review feedback incorporated?
For the first change I made a mistake: I replaced the doxygen docs with sqlite and I forgot to update the link: it's currently at http://klemens.dev/sqlite-new/ . Mohammad, can I update the link during the review? Otherwise - bunch of fixes, e.g. missing default-constructor. - json support is header only, so it is not a dependency for compiling sqlite. - static_result_set has a strict mode, where the columns must match exactly. In non-strict mode the struct can have extra fields. - a sqlite::connection can be constructed from a filesystem::path - transaction destructors are only not noexcept for very old sqlite versions - added a delete_function function - the aggregate and window functions are stateful (i.e. the type gets constructed when the function is used) ( https://github.com/klemens-morgenstern/sqlite/pull/4/commits/c29ec35717fcc61... )
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/TDMWGPUX...

On Wed, Aug 27, 2025 at 6:14 AM Klemens Morgenstern via Boost <boost@lists.boost.org> wrote:
For the first change I made a mistake: I replaced the doxygen docs with sqlite and I forgot to update the link: it's currently at http://klemens.dev/sqlite-new/ . Mohammad, can I update the link during the review?
Yes, please go ahead.

śr., 27 sie 2025 o 06:37 Mohammad Nejati [ashtum] via Boost < boost@lists.boost.org> napisał(a):
On Wed, Aug 27, 2025 at 6:14 AM Klemens Morgenstern via Boost <boost@lists.boost.org> wrote:
For the first change I made a mistake: I replaced the doxygen docs with sqlite and I forgot to update the link: it's currently at http://klemens.dev/sqlite-new/ . Mohammad, can I update the link during the review?
Yes, please go ahead.
However, as Dominique has pointed out, the above link gives 404. Are we making a review process based on the unintended documentation shape? Regards, &rzej;
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/CIRHRETV...

On Wed, Aug 27, 2025 at 5:43 PM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
śr., 27 sie 2025 o 06:37 Mohammad Nejati [ashtum] via Boost < boost@lists.boost.org> napisał(a):
On Wed, Aug 27, 2025 at 6:14 AM Klemens Morgenstern via Boost <boost@lists.boost.org> wrote:
For the first change I made a mistake: I replaced the doxygen docs with sqlite and I forgot to update the link: it's currently at http://klemens.dev/sqlite-new/ . Mohammad, can I update the link during the review?
Yes, please go ahead.
However, as Dominique has pointed out, the above link gives 404. Are we making a review process based on the unintended documentation shape?
Sorry for the confusion - I mean updating the docs, not updating the link: https://klemens.dev/sqlite now points to the correct doc. I also readded sqlite-new pointing to the same document.
Regards, &rzej;
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at:
https://lists.boost.org/archives/list/boost@lists.boost.org/message/CIRHRETV...
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/WJJXOHFA...

On Wed, Aug 27, 2025 at 4:42 AM Klemens Morgenstern via Boost <boost@lists.boost.org> wrote:
On Mon, Aug 25, 2025 at 4:54 AM Dominique Devienne via Boost < it's currently at http://klemens.dev/sqlite-new/ .
http://klemens.dev/sqlite-new/ gives me 404: Page not found https://klemens.dev/sqlite/ works, but is it the new one? (probably not, says Generated on Thu Nov 14 2024 at the bottom) Also, your https://klemens.dev/portfolio/ page doesn't even list SQLite, FWIW.
Otherwise - bunch of fixes, e.g. missing default-constructor. - [etc...]
Thanks. --DD

Below is my initial review (with one typo edited). I believe many of the comments were incorporated. I look forward to using this in production. Thank you all for the time you've invested. SB # boost_sqlite review Please accept my apologies for the poor formatting. And a bit thank you to everyone involved in this process. I have long wished for a sqlite wrapper with critical mass. ## Author's Background I am a professional C++ software developer. I have been a professional user and proponent of Boost libraries since the late 2000s. I have been using SQLite for more than 10 years and at one time wrote my own C++ wrapper. ## Initial Thoughts and Use Case At the time I saw the announcement for this review, I was actively evaluating libraries for a project I was (and continue to be) working on. I successfully used the boost_sqlite library in a limited capacity (create, insert, and pragmas only for on disk and in memory DBs). My first impression of the library is that it's extremely lightweight. This simplicity has value but also leaves the end user reinventing the wheel for certain repetitive operations. For example, using class enums for pragma get and set moves defect finding from runtime (bad) to compile time (better!). My second thought was how painful it was to include in my professional project. Our legacy software does not build Boost.json and, while it was trivial to add it for my sandbox app, it is an unnecessary extra dependency. ## Design Evaluation Simple, straight forward, and intuitive. I believe it meets my needs. ### Wishlist Here are some wishlist items I have. They may be inappropriate for this library and boost in general; however, they are still on my wishlist. #### connection It would be great if connection had a simple enum so I could do this: ```cpp boost::sqlite::connection mydb(boost::sqlite::memory); ``` Also, given C++17 `std::fstream` takes `std::filesystem::path`, it would be nice if `connection()` et all did as well. And I wouldn't complain if `boost::filesystem::path` was supported as well. ## Implementation Evaluation I reviewed this library as a user. ## Documentation Evaluation The readme.md file was adequate for me to quickly build a simple SQLite DB application. ## Potential Usefulness This is a useful library. I intend to incorporate this into professional projects as soon as it becomes available in a stable boost release. ## Personal Usage I used the following compiler in my development: gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 I included Boost and Boost.sqlite using CPM.cmake (fetchcontent). It was a trivial operation to include and link. ## Final Thoughts For the past few years I've lamented the lack of a Modern C++ SQLite wrapper with a critical mass of user backing. This library has the potential to fill that gap. I would like to see this library built without the json dependency. With or without the build dependency change, this library should be accepted into Boost. Scott Bailey On Sun, Aug 24, 2025 at 11:52 PM Mohammad Nejati [ashtum] via Boost < boost@lists.boost.org> wrote:
Dear Boost community,
The review of Boost.SQLite by Klemens Morgenstern begins today, Monday, August 25, and runs through Wednesday, September 3, 2025.
Boost.SQLite provides a safer and more expressive C++ interface to the SQLite3 C-API to simplify usage and reduce errors.
You can read the documentation at: https://klemens.dev/sqlite and study or try out the code at: https://github.com/klemens-morgenstern/sqlite
This is the second review attempt. The first concluded without a result due to too few submitted reviews.
Anyone with experience in C++ or SQLite, at any level, is encouraged to contribute. In your review, please indicate how much time you spent on the evaluation.
Here are some questions you might find helpful when writing your review:
- What is your evaluation of the design? - What is your evaluation of the implementation? - What is your evaluation of the documentation? - What is your evaluation of the potential usefulness of the library? Do you already use it in industry? - Did you try to use the library? With which compiler(s)? Did you have any problems? - How much effort did you put into your evaluation? A glance? A quick reading? In-depth study? - Are you knowledgeable about the problem domain?
At the end of your review, please state clearly whether you ACCEPT, REJECT, or CONDITIONALLY ACCEPT (with explicit conditions) the proposed Boost.SQLite library.
Your feedback is critical to the success of this review. Thank you in advance for your time, expertise, and contributions.
Best regards, Mohammad Nejati Review Manager for the proposed Boost.SQLite _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/FK4NIN2J...

BACKGROUND Vinnie asked me to take a look at this library and give my feedback. This is my first Boost review. As such this review might not take into consideration accepted Boost coding practices. Although I do not have any experience of SQLite I have been working in C++ for several years, and have myself written (proprietary) C++ wrappers around C libraries for accessing other relational databases (notably Sybase). I spent approximately 4 hours in total looking at this library. This was, therefore only a quick reading. The library does warrant a more in-depth review but I wanted to get my initial feedback to the author early. COMPILATION I compiled this using GCC-13 on ARM64 Ubuntu C++17. When I attempted to compile using C++11 and C++14 there were numerous compilation errors (most notably in example/multi_index.cpp). If code is not expected to compile under older C++ versions it should be wrapped in appropriate #if...#endif statements. Out of curiosity I tried building this against the boost version (1.83) that ships with Ubuntu 24 and found it fails to compile under C++20 (missing pfr/core_name.hpp). I suspect this is not a huge concern as most users building this library will also build the things on which it depends. DOCUMENTATION This is probably my largest concern. The documentation itself seems extremely light. As an example of the more "advanced" features such as virtual tables need worked examples. Without some basic knowledge of sqlite3 it would be hard to follow. Some of the reference descriptions appear to be unfinished (for example vtab::cursor has the description "Cursor needs the following member"), and many detailed descriptions are just a repetition of the single-line headline. I would also like to see the detailed descriptions provide information about what purpose it serves and how it should be used rather than simply stating what can be deduced by looking at the header. In addition, many users will look at the examples for inspiration, so those examples should have detailed step-by-step comments stating what is being done and why it is being done that way. INTERFACE DESIGN AND IMPLEMENTATION As a first pass the interface struck me as being quite clean. I did spot some issues (below) but none of them are major. One thing that struck me about the 'prepare' function is the choice of placeholders. `?`-based `?1`, `?2` are used for numbered placeholders, and `$`-based `$name1`, `$name2` for named placeholders. This feels slightly inconsistent, but it matches what sqlite already does, so this point can probably also be mitigated by pointing out in the docs that the syntax is driven by sqlite3 (with appropriate link). I notice that a number of classes expose the `handle` of the connection. Is there any reason for this to be part of the public interface. I would be tempted to keep it private (at least for the initial release until there is a proven use case) `cstring_ref.hpp` uses `#if defined(BOOST_NO_CXX17)` as a workaround for the lack of a `BOOST_CXX17_CONSTEXPR` keyword. The better solution might be to add `BOOST_CXX17_CONSTEXPR` into `boost/config.hpp` alongside `BOOST_CXX14_CONSTEXPR`. `allocator` has two constructors: {{{ constexpr allocator( const allocator& other ) noexcept {} template< class U > constexpr allocator( const allocator<U>& other ) noexcept {} }}} Is the first (non-templated) constructor required? Why does error_info need additonal overloads for `format` when `__GNUC__` is defined? Is there a compiler bug on old versions of the compiler? The fact that `row` and `field` hold raw pointers to the statement makes me worry about possible lifetime issues and UB if users start returning these objects from functions. There would be a slight overhead but some form of `weak_ptr`-based mechanism might be safer. I have some other concerns (such as the fact cstring_ref even needs to exist), but I accept that the author's hands are somewhat tied by the needs of the sqlite3 C interface. RECOMMENDATION I think this would be a useful library, and appears to be more full-featured that the alternatives listed at the foot of the documentation. My recommendation would be to Conditionally Accept on the basis that the documentation requires further work, and the examples lack user-friendly code comments. My interface/implementation concerns above are sufficiently minor to not warrant delaying progress. On 25 Aug 2025, at 5:54, "Mohammad Nejati [ashtum] via Boost" <boost@lists.boost.org> wrote: Dear Boost community, The review of Boost.SQLite by Klemens Morgenstern begins today, Monday, August 25, and runs through Wednesday, September 3, 2025. Boost.SQLite provides a safer and more expressive C++ interface to the SQLite3 C-API to simplify usage and reduce errors. You can read the documentation at: https://klemens.dev/sqlite and study or try out the code at: https://github.com/klemens-morgenstern/sqlite This is the second review attempt. The first concluded without a result due to too few submitted reviews. Anyone with experience in C++ or SQLite, at any level, is encouraged to contribute. In your review, please indicate how much time you spent on the evaluation. Here are some questions you might find helpful when writing your review: - What is your evaluation of the design? - What is your evaluation of the implementation? - What is your evaluation of the documentation? - What is your evaluation of the potential usefulness of the library? Do you already use it in industry? - Did you try to use the library? With which compiler(s)? Did you have any problems? - How much effort did you put into your evaluation? A glance? A quick reading? In-depth study? - Are you knowledgeable about the problem domain? At the end of your review, please state clearly whether you ACCEPT, REJECT, or CONDITIONALLY ACCEPT (with explicit conditions) the proposed Boost.SQLite library. Your feedback is critical to the success of this review. Thank you in advance for your time, expertise, and contributions. Best regards, Mohammad Nejati Review Manager for the proposed Boost.SQLite _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/FK4NIN2J...

On Tue, Aug 26, 2025 at 6:13 PM Mungo Gill via Boost <boost@lists.boost.org> wrote:
[...] I notice that a number of classes expose the `handle` of the connection. Is there any reason for this to be part of the public interface. I would be tempted to keep it private
FWIW, that would be an immediate no-go for such a library. You can't easily replace existing (older, crappier) existing wrappers (or plain C usage) in large bodies of C++ code already using SQLite, at least not wholesale. Thus access to raw SQLite handles is paramount.

On Tue, Aug 26, 2025 at 12:14 PM Mungo Gill via Boost <boost@lists.boost.org> wrote:
BACKGROUND Vinnie asked me to take a look at this library and give my feedback. This is my first Boost review. As such this review might not take into consideration accepted Boost coding practices. Although I do not have any experience of SQLite I have been working in C++ for several years, and have myself written (proprietary) C++ wrappers around C libraries for accessing other relational databases (notably Sybase). I spent approximately 4 hours in total looking at this library. This was, therefore only a quick reading. The library does warrant a more in-depth review but I wanted to get my initial feedback to the author early. COMPILATION I compiled this using GCC-13 on ARM64 Ubuntu C++17. When I attempted to compile using C++11 and C++14 there were numerous compilation errors (most notably in example/multi_index.cpp). If code is not expected to compile under older C++ versions it should be wrapped in appropriate #if...#endif statements. Out of curiosity I tried building this agains t the boost version (1.83) that ships with Ubuntu 24 and found it fails to compile under C++20 (missing pfr/core_name.hpp). I suspect this is not a huge concern as most users building this library will also build the things on which it depends. DOCUMENTATION This is probably my largest concern. The documentation itself seems extremely light. As an example of the more "advanced" features such as virtual tables need worked examples. Without some basic knowledge of sqlite3 it would be hard to follow. Some of the reference descriptions appear to be unfinished (for example vtab::cursor has the description "Cursor needs the following member"), and many detailed descriptions are just a repetition of the single-line headline. I would also like to see the detailed descriptions provide information about what purpose it serves and how it should be used rather than simply stating what can be deduced by looking at the header. In addition, many users will look at the examples for inspiration, so those examples should have detailed step-by-step comments stating what is being done and why it is being done that way.
I understand the concern. I think about it like this: there is no way one can implement a vtable without understanding sqlite3. If I were to write such a documentation, it would be 90% description of sqlite3.
INTERFACE DESIGN AND IMPLEMENTATION As a first pass the interface struck me as being quite clean. I did spot some issues (below) but none of them are major. One thing that struck me about the 'prepare' function is the choice of placeholders. `?`-based `?1`, `?2` are used for numbered placeholders, and `$`-based `$name1`, `$name2` for named placeholders. This feels slightly inconsistent, but it matches what sqlite already does, so this point can probably also be mitigated by pointing out in the docs that the syntax is driven by sqlite3 (with appropriate link). I notice that a number of classes expose the `handle` of the connection. Is there any reason for this to be part of the public interface.
Yes, so you can use it in legacy code or just use the sqlite3 API in a way that is not intended by the library. The problem with the second is that I'd need to provide a 100% implementation of sqlite for all edge cases sqlite supports. And even if I managed to that, any sqlite update could potentially add functions that my library doesn't have or that force redesigns. It is just easier to keep underlying APIs (especially when they're as well designed as sqlite's) open and instead of locking the user down.
I would be tempted to keep it private (at least for the initial release until there is a proven use case) `cstring_ref.hpp` uses `#if defined(BOOST_NO_CXX17)` as a workaroun d for the lack of a `BOOST_CXX17_CONSTEXPR` keyword. The better solution might be to add `BOOST_CXX17_CONSTEXPR` into `boost/config.hpp` alongside `BOOST_CXX14_CONSTEXPR`.
Yes, and I'll ask the boost/config maintainers after a successful review. I don't think it's a fair use of their time to change boost/config if it's not certain that it'll be used.
`allocator` has two constructors: {{{ constexpr allocator( const allocator& other ) noexcept {} template< class U > constexpr allocator( const allocator<U>& other ) noexcept {} }}} Is the first (non-templated) constructor required?
Compiler do funny things at time. IIRC MSVC did not handle the missing copy right.
Why does error_info need additonal overloads for `format` when `__GNUC__` is defined? Is there a compiler bug on old versions of the compiler?
There's an attribute to warn when the format string is invalid.
The fact that `row` and `field` hold raw pointers to the statement makes me worry about possible lifetime issues and UB if users start returning these objects from functions. There would be a slight overhead but some form of `weak_ptr`-based mechanism might be safer.
That would introduce a lot of indirection, because the pointer itself comes from sqlite. If there was some reference counting mechanism in sqlite I'd use it.
I have some other concerns (such as the fact cstring_ref even needs to exist), but I accept that the author's hands are somewhat tied by the needs of the sqlite3 C interface.
I'd actually be happy to move the cstring_ref into boost/core, but it is essential to support null-terminated strings without too many copies.
RECOMME NDATION I think this would be a useful library, and appears to be more full-featured that the alternatives listed at the foot of the documentation. My recommendation would be to Conditionally Accept on the basis that the documentation requires further work, and the examples lack user-friendly code comments. My interface/implementation concerns above are sufficiently minor to not warrant delaying progress.
Thank you for your review!

On Tue, Aug 26, 2025 at 6:13 PM Mungo Gill via Boost <boost@lists.boost.org> wrote:
One thing that struck me about the 'prepare' function is the choice of placeholders. `?`-based `?1`, `?2` are used for numbered placeholders, and `$`-based `$name1`, `$name2` for named placeholders. This feels slightly inconsistent, but it matches what sqlite already does, so this point can probably also be mitigated by pointing out in the docs that the syntax is driven by sqlite3 (with appropriate link).
https://www.sqlite.org/lang_expr.html#parameters You do state that "I do not have any experience of SQLite" but remark on exposing handles and named params still are "unfair" to a wrapper library that can't (and shouldn't) change the SQL dialect of SQLite. Boost.SQLite's doc shouldn't be a rehash of the extensive SQLite docs. Anyone using that wrapper lib should have experience with SQLite. Especially with vtables indeed. Proper emulation of indexing in vtables is complicated in and of itself, even based on SQLite's doc. They could have improved since I did that on top of Boost.Multi-Index though. --DD

This is my first Boost review. I have many years of experience developing C++ code and have spent the last four years contributing to an open-source C++ library. I am familiar with SQLite, though I have not used features like virtual tables in production. ## Design - I find the layering clear and Boost-idiomatic: `connection`, `statement`, and `resultset` are well-defined, and the non-throwing interface that uses `boost::system::error_code` is easy to apply in real code. ## Implementation - I see a clean separation of concerns, consistent error handling, and careful lifetime management for vtables and result sets. - Issues identified: - 1. In `detail::convert_row`, `system::error_code ec` is passed by value, so assignments do not escape. Pass by reference to propagate errors. - 2. In `static_resultset.hpp`, `convert_field(boost::optional<T>&, const field&)` calls `target.emplace_back()`. `boost::optional` has `emplace()` only; this would fail on instantiation. - 3. In `json.hpp` serializer, growth logic mis-tracks total bytes and passes wrong sizes: subsequent `read` uses full capacity from `c + len(last_chunk)`, and final length uses only the last chunk. - 4. In `statement.hpp`, tuple binding uses `mp11::tuple_for_each(std::move(vec), ...)` even for `const std::tuple&` (no moves; move-only types fail). Also fix typos like "To few parameters provided". ## Documentation - The `readme.md` and Asciidoctor docs provide a good starting point and are easy to navigate. - Typos: - `readme.md` contains typos ("aggregrate", "multiple time") and a code snippet that prints the same tuple element twice (`std::get<0>`). Another snippet includes stray characters (`auto r = q.current();''`). - Header comments contain typos ("parametert", "statemens"). ## Potential usefulness - I consider the library highly useful: it provides a modern, Boost-aligned SQLite interface with robust support for advanced features that fills a notable gap in the ecosystem. ## Local build - I was able to configure and build the library with GCC 13.3.0; the tests/examples build options worked as expected. - Testing environment: - Ubuntu 24.04.3 LTS on WSL2 - GCC 13.3.0 - CMake: 3.28.3; Generator: GNU Make 4.3 - SQLite: 3.45.1 - Boost: 4c41118 (latest as of Aug 25) installed at `/opt/boost` ## Effort - I spent several hours reading headers, selected sources, build files, and documentation, and I verified the build locally. ## Decision: CONDITIONALLY ACCEPT - Address the issues detailed above (see `Implementation: Issues identified` and `Documentation: Typos`) before acceptance.

I am affiliated with the C++ Alliance and Vinnie Falco suggested I review this library. On Fri, Aug 29, 2025 at 2:36 PM haroldjbott--- via Boost < boost@lists.boost.org> wrote:
This is my first Boost review. I have many years of experience developing C++ code and have spent the last four years contributing to an open-source C++ library. I am familiar with SQLite, though I have not used features like virtual tables in production.
## Design - I find the layering clear and Boost-idiomatic: `connection`, `statement`, and `resultset` are well-defined, and the non-throwing interface that uses `boost::system::error_code` is easy to apply in real code.
## Implementation - I see a clean separation of concerns, consistent error handling, and careful lifetime management for vtables and result sets. - Issues identified: - 1. In `detail::convert_row`, `system::error_code ec` is passed by value, so assignments do not escape. Pass by reference to propagate errors. - 2. In `static_resultset.hpp`, `convert_field(boost::optional<T>&, const field&)` calls `target.emplace_back()`. `boost::optional` has `emplace()` only; this would fail on instantiation. - 3. In `json.hpp` serializer, growth logic mis-tracks total bytes and passes wrong sizes: subsequent `read` uses full capacity from `c + len(last_chunk)`, and final length uses only the last chunk. - 4. In `statement.hpp`, tuple binding uses `mp11::tuple_for_each(std::move(vec), ...)` even for `const std::tuple&` (no moves; move-only types fail). Also fix typos like "To few parameters provided".
## Documentation - The `readme.md` and Asciidoctor docs provide a good starting point and are easy to navigate. - Typos: - `readme.md` contains typos ("aggregrate", "multiple time") and a code snippet that prints the same tuple element twice (`std::get<0>`). Another snippet includes stray characters (`auto r = q.current();''`). - Header comments contain typos ("parametert", "statemens").
## Potential usefulness - I consider the library highly useful: it provides a modern, Boost-aligned SQLite interface with robust support for advanced features that fills a notable gap in the ecosystem.
## Local build - I was able to configure and build the library with GCC 13.3.0; the tests/examples build options worked as expected. - Testing environment: - Ubuntu 24.04.3 LTS on WSL2 - GCC 13.3.0 - CMake: 3.28.3; Generator: GNU Make 4.3 - SQLite: 3.45.1 - Boost: 4c41118 (latest as of Aug 25) installed at `/opt/boost`
## Effort - I spent several hours reading headers, selected sources, build files, and documentation, and I verified the build locally.
## Decision: CONDITIONALLY ACCEPT - Address the issues detailed above (see `Implementation: Issues identified` and `Documentation: Typos`) before acceptance. _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/CS3FT3E7...

On Mon, 25 Aug 2025 at 14:53, Mohammad Nejati [ashtum] via Boost <boost@lists.boost.org> wrote:
Boost.SQLite provides a safer and more expressive C++ interface to the SQLite3 C-API to simplify usage and reduce errors.
- What is your evaluation of the design?
This library is minimal, by design - but this needs to be considered in the context of the stated goal to reduce errors and simplify usage. I have some reservations. . It does (successfully) provide access to each of the facilities in SQLite3, to allow them to be used in an idiomatic manner (from a C++ perspective). The way this is done reduces opportunities for errors compared to direct use of the C API by providing basic RAII resource management of individual SQLite3 resources. It uses owning pointers with custom resources in encapsulating individual sqlite resource types (eg statements). On the other hand it appears to leave plenty of opportunity for footguns. Im not convinced some of these at least couldn't be eliminated, without a significant performance impact and with better, not worse, usability. The convenience and general ergonomics of the interface provided is fine: Treating the result of executing a statement as a range Ranges for access to the fields (columns) of results Conversions of individual column value types to and from idiomatic C++ eg scalar types and string_view Extending this to typed access rowset columns as tuples (position) or (described, pre C++20) struct field names Importantly, it provides a non-exception based way of reporting results.(and they genuinely are often results not "exceptions" so this is in general necessary and is enough to dissuade this user at least from using alternatives that provide only a throwing interface. It is disappointing that this is in conflict with some of the idiomatic usage features - iterators in particular. I'm also concerned that by providing a single class that allows for both a "sqlite3 way" and a "idiomatic C++ way" that mixed usage can create object states that are unexpected. Rather than saying "don't do that" why not prevent that through distinct types?
- What is your evaluation of the implementation? It's clean and minimal, well structured.
- What is your evaluation of the documentation? Inadequate regarding the concepts above. I'm all for relying on sqlite3 documentation for a lot of detail - but this does not mean that one should have to assume or read the implementation to determine how the interface presented by the library operates.
The examples show only basic, throwing, and non-RAII breaking usage of the library. And don't attempt to handle any unexpected results.
- What is your evaluation of the potential usefulness of the library?
It is very useful, because sqlite3 is incredibly useful, and making it easier to use and usage consistent and correct would be a boon.
Do you already use it in industry?
No but I use sqlite3.
- Did you try to use the library? With which compiler(s)? Did you have any problems?
I had no problems building and using the library using gcc 14.2.1 and boost 1.87 and sqlite 3.47.2 I built and ran the tests and examples and made some modifications to the examples to try things out.
- How much effort did you put into your evaluation? A glance? A quick reading? In-depth study?
A few hours. Which was enough for a reasonable depth of review given the relative simplicity of the library.. I did not look at the virtual table and extension support beyond looking at and running the examples and a brief review of the implementation. I do consider the support for these facilities to be important and useful - and this library is the best effort I have seen in making these facilities easier to use.
- Are you knowledgeable about the problem domain?
Somewhat. I'm not a huge user of sqlite, but have over the years written C++ and other language interfaces/bindings for diverse "databases" with fairly raw C APIs e.g as diverse as lmdb and MS ODBC./ SQL server.
At the end of your review, please state clearly whether you ACCEPT, REJECT, or CONDITIONALLY ACCEPT (with explicit conditions) the proposed Boost.SQLite library.
I recommend acceptance of this library only on the condition it has some footguns removed and any remaining better and clearly documented. I view the footgun removal as more than just idiot proofing. This domain demands treating all results equally. The problems that exist nin error handling would be mitigated by having a type able to indicate the result was an error without resorting to separate reference "out" parameters. Allowing closing and re-connecting the connection creates a conflict with RAII concepts - a 2 stage (but only once) initialisation (ie late connection, but no re-initialisation) would seem sufficient for satisfactory ergonomics. Serious consideration should be given to extending the lifespan of depended on, underlying, objects (statement, specifically, depended on by result) - this is likely a practical necessity for the deferred evaluation of error vs result described above. I've leapt into the review / conclusion "early" given the relatively short window - I would be happy to discuss this feedback - this isn't intended as a drive-by review...

On Sat, Aug 30, 2025 at 6:46 PM Darryl Green <darryl.green@gmail.com> wrote:
I recommend acceptance of this library only on the condition it has some footguns removed and any remaining better and clearly documented.
Thank you, Darryl, for your review. Your acceptance conditions are noted.

Hi, First, I want to thank Klemens Morgenstern for his work on this library and proposing it to boost, and Mohammad Nejati for managing the review. I have started to look at a the library from a user's perspective with limited sqlite3 experience. I ran into a few issues, where I would like to understand the rationale behind some of the design decisions: * The field type returns sqlite TEXT data as cstring_ref instead of string_view. The sqlite documentation guarantees it's NUL terminated, but it also recommends using e.g. sqlite3_column_bytes to determine the length of text data and does not explicitly forbid embedded NUL characters. * The connection type can be owning or non-owning. Why wouldn't one instead have distinct connection (owning) and connection_ref (non-owning) types? * The statement type can construct owning and non-owning resultsets. The former automatically finalize, the latter reset the statement. Again why are those two behaviors combined in the same type? * The library is designed around unique_ptr wrapping the sqlite3 C API handles with optional ownership. In particular, this seems at times quite dangerous for the sqlite3_stmt handle which is shared by statement, resultset, field, etc. Would the library be better served by for example reference counting such handles? If not, why? * What is the general thread safety guarantees of the library types? After all, it contains a mutex implementation wrapper. So multi-threaded application are probably a target for this library. Best regards Max

On Sun, Aug 31, 2025 at 8:44 AM Maximilian Riemensberger via Boost < boost@lists.boost.org> wrote:
Hi,
First, I want to thank Klemens Morgenstern for his work on this library and proposing it to boost, and Mohammad Nejati for managing the review.
I have started to look at a the library from a user's perspective with limited sqlite3 experience. I ran into a few issues, where I would like to understand the rationale behind some of the design decisions:
Thank you for participating in the review process.
* The field type returns sqlite TEXT data as cstring_ref instead of string_view. The sqlite documentation guarantees it's NUL terminated, but it also recommends using e.g. sqlite3_column_bytes to determine the length of text data and does not explicitly forbid embedded NUL characters.
I would recommend using the blob interface then. A string_view with null-terminators in the middle is also generally bad news.
* The connection type can be owning or non-owning. Why wouldn't one instead have distinct connection (owning) and connection_ref (non-owning) types?
Because otherwise the complexity goes up unnecessarily. A user can write a
function just taking `connection&` and not worry about this. In the library I have the `transaction` type for example. It holds a reference to the connection - if there was a second type like `connection_ref`, now I would have a second type of a transaction. I could of course add a connection_base and build a OOP construct, but that would just be a more inefficient & cumbersome version of the current solution.
* The statement type can construct owning and non-owning resultsets. The former automatically finalize, the latter reset the statement. Again why are those two behaviors combined in the same type?
Same reason as above. If a user wrote a manual converter for each row, he'd need to implement it twice. The point is that those two things just work:
T interpret_result(sqlite::resultset rs); auto rr = interpret_result(conn.prepare("...").execute(...)); auto s = conn.prepare("..."); auto r2 = interpret_result(s.execute(...)); Why should I force a user to write two different implementations of interpret_result? And the answer isn't OOP for the resultset, because that it cannot be passed by value.
* The library is designed around unique_ptr wrapping the sqlite3 C API handles with optional ownership. In particular, this seems at times quite dangerous for the sqlite3_stmt handle which is shared by statement, resultset, field, etc. Would the library be better served by for example reference counting such handles? If not, why?
There is no obvious place to put the counter. If sqlite had those, I'd use them.
* What is the general thread safety guarantees of the library types? After all, it contains a mutex implementation wrapper. So multi-threaded application are probably a target for this library.
Same as whatever sqlite is compiled for.
Best regards Max
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/UQV5X5HL...

On 8/31/25 18:24, Klemens Morgenstern via Boost wrote:
On Sun, Aug 31, 2025 at 8:44 AM Maximilian Riemensberger via Boost < boost@lists.boost.org> wrote:
* The field type returns sqlite TEXT data as cstring_ref instead of string_view. The sqlite documentation guarantees it's NUL terminated, but it also recommends using e.g. sqlite3_column_bytes to determine the length of text data and does not explicitly forbid embedded NUL characters.
I would recommend using the blob interface then. A string_view with null-terminators in the middle is also generally bad news.
Maybe. But std::string and std::string_view handle that case just fine. sqlite also handles it just fine as a column value (not in expressions). And since sqlite internally tracks, exposes and recommends using the length, I cannot see any good reason not to use string_view.
* The connection type can be owning or non-owning. Why wouldn't one instead have distinct connection (owning) and connection_ref (non-owning) types?
Because otherwise the complexity goes up unnecessarily. A user can write a function just taking `connection&` and not worry about this. In the library I have the `transaction` type for example. It holds a reference to the connection - if there was a second type like `connection_ref`, now I would have a second type of a transaction.
I see this point. However, a user that writes a function that takes `connection&` as an argument likely does not care about whether the connection is owning since the general expection would be that the connection is connected and live before the function is called and remains so until the function returns. All those functions can just take `connection_ref` by value (just like functions take a string_view or span or function_ref) and no duplication is necessary, assuming connection_ref is implicitly constructible from `connection`. For example, the transaction: class transaction { connection_ref conn_; public: explicit transaction(connection_ref conn); // Many more things } And I would then expect both of the following to compile just fine: // 1 connection c{":memory:"}; transaction t{c}; // 2 connection c{":memory:"}; connection_ref cr{c}; transaction t{cr}; and mean the same thing.
I could of course add a connection_base and build a OOP construct, but that would just be a more inefficient & cumbersome version of the current solution.
* The statement type can construct owning and non-owning resultsets. The former automatically finalize, the latter reset the statement. Again why are those two behaviors combined in the same type?
Same reason as above. If a user wrote a manual converter for each row, he'd need to implement it twice. The point is that those two things just work:
T interpret_result(sqlite::resultset rs);
auto rr = interpret_result(conn.prepare("...").execute(...));
auto s = conn.prepare("..."); auto r2 = interpret_result(s.execute(...));
Why should I force a user to write two different implementations of interpret_result? And the answer isn't OOP for the resultset, because that it cannot be passed by value.
Similar as for the connection: If the user function `interpret_result` does not take ownership of the resultset, that is the resultset that is passed into the function must outlive the return of the function, then interpret_result should only be written once and take resultset_ref as argument. resultset is implicitly convertible to resultset_ref. There is also no lifetime issue here since the return value of execute lives at least until the end of the semicolon (after interpret_result returns). T interpret_result(sqlite::resultset_ref rs); auto rr = interpret_result(conn.prepare("...").execute(...)); // Same thing written in two statements sqlite::resultset rs = conn.prepare("...").execute(...) auto rr = interpret_result(rs); sqlite::resultset_ref s = conn.prepare("..."); auto r2 = interpret_result(s.execute(...));
* The library is designed around unique_ptr wrapping the sqlite3 C API handles with optional ownership. In particular, this seems at times quite dangerous for the sqlite3_stmt handle which is shared by statement, resultset, field, etc. Would the library be better served by for example reference counting such handles? If not, why?
There is no obvious place to put the counter. If sqlite had those, I'd use them.
Fair enough.
* What is the general thread safety guarantees of the library types? After all, it contains a mutex implementation wrapper. So multi-threaded application are probably a target for this library.
Same as whatever sqlite is compiled for.
Ok. Searching through the sqlite docs basically gave me this page https://www.sqlite.org/threadsafe.html which kind of explains it. Best regards, Max

* The statement type can construct owning and non-owning resultsets. The former automatically finalize, the latter reset the statement. Again why are those two behaviors combined in the same type?
Same reason as above. If a user wrote a manual converter for each row, he'd need to implement it twice. The point is that those two things just work:
T interpret_result(sqlite::resultset rs);
auto rr = interpret_result(conn.prepare("...").execute(...));
auto s = conn.prepare("..."); auto r2 = interpret_result(s.execute(...));
Why should I force a user to write two different implementations of interpret_result? And the answer isn't OOP for the resultset, because that it cannot be passed by value.
Similar as for the connection: If the user function `interpret_result` does not take ownership of the resultset, that is the resultset that is passed into the function must outlive the return of the function, then interpret_result should only be written once and take resultset_ref as argument. resultset is implicitly convertible to resultset_ref. There is also no lifetime issue here since the return value of execute lives at least until the end of the semicolon (after interpret_result returns).
T interpret_result(sqlite::resultset_ref rs);
auto rr = interpret_result(conn.prepare("...").execute(...));
// Same thing written in two statements sqlite::resultset rs = conn.prepare("...").execute(...) auto rr = interpret_result(rs);
sqlite::resultset_ref s = conn.prepare("..."); auto r2 = interpret_result(s.execute(...));
I still think that having types that might be owning or not depending on a runtime condition is problematic. See my last review [1] for examples on the footguns that this may produce. I don't think there is much precedent of such a pattern in the standard library, taking out unique/shared pointers with custom deleters. The solution that Max is proposing here seems sound to me. Regards, Ruben. [1] https://lists.boost.org/archives/list/boost@lists.boost.org/message/57ZZ7IIW...

pon., 1 wrz 2025 o 12:03 Ruben Perez via Boost <boost@lists.boost.org> napisał(a):
* The statement type can construct owning and non-owning resultsets. The former automatically finalize, the latter reset the statement. Again why are those two behaviors combined in the same type?
Same reason as above. If a user wrote a manual converter for each row, he'd need to implement it twice. The point is that those two things just work:
T interpret_result(sqlite::resultset rs);
auto rr = interpret_result(conn.prepare("...").execute(...));
auto s = conn.prepare("..."); auto r2 = interpret_result(s.execute(...));
Why should I force a user to write two different implementations of interpret_result? And the answer isn't OOP for the resultset, because that it cannot be passed by value.
Similar as for the connection: If the user function `interpret_result` does not take ownership of the resultset, that is the resultset that is passed into the function must outlive the return of the function, then interpret_result should only be written once and take resultset_ref as argument. resultset is implicitly convertible to resultset_ref. There is also no lifetime issue here since the return value of execute lives at least until the end of the semicolon (after interpret_result returns).
T interpret_result(sqlite::resultset_ref rs);
auto rr = interpret_result(conn.prepare("...").execute(...));
// Same thing written in two statements sqlite::resultset rs = conn.prepare("...").execute(...) auto rr = interpret_result(rs);
sqlite::resultset_ref s = conn.prepare("..."); auto r2 = interpret_result(s.execute(...));
I still think that having types that might be owning or not depending on a runtime condition is problematic. See my last review [1] for examples on the footguns that this may produce. I don't think there is much precedent of such a pattern in the standard library, taking out unique/shared pointers with custom deleters. The solution that Max is proposing here seems sound to me.
On top of that, this controversial design aspect is not documented. After having read the docs I was not aware that this conditional ownership decision is implemented. As a user, I need to be able to reason about the ownership of the resources, I need to have all the information available. Regards, &rzej;
Regards, Ruben.
[1] https://lists.boost.org/archives/list/boost@lists.boost.org/message/57ZZ7IIW... _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/F62TWUTO...

On Mon, Sep 1, 2025 at 2:45 PM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
pon., 1 wrz 2025 o 12:03 Ruben Perez via Boost <boost@lists.boost.org> napisał(a):
* The statement type can construct owning and non-owning resultsets. The former automatically finalize, the latter reset the statement. Again why are those two behaviors combined in the same type?
Same reason as above. If a user wrote a manual converter for each row, he'd need to implement it twice. The point is that those two things just work:
T interpret_result(sqlite::resultset rs);
auto rr = interpret_result(conn.prepare("...").execute(...));
auto s = conn.prepare("..."); auto r2 = interpret_result(s.execute(...));
Why should I force a user to write two different implementations of interpret_result? And the answer isn't OOP for the resultset, because that it cannot be passed by value.
Similar as for the connection: If the user function `interpret_result` does not take ownership of the resultset, that is the resultset that is passed into the function must outlive the return of the function, then interpret_result should only be written once and take resultset_ref as argument. resultset is implicitly convertible to resultset_ref. There is also no lifetime issue here since the return value of execute lives at least until the end of the semicolon (after interpret_result returns).
T interpret_result(sqlite::resultset_ref rs);
auto rr = interpret_result(conn.prepare("...").execute(...));
// Same thing written in two statements sqlite::resultset rs = conn.prepare("...").execute(...) auto rr = interpret_result(rs);
sqlite::resultset_ref s = conn.prepare("..."); auto r2 = interpret_result(s.execute(...));
I still think that having types that might be owning or not depending on a runtime condition is problematic. See my last review [1] for examples on the footguns that this may produce. I don't think there is much precedent of such a pattern in the standard library, taking out unique/shared pointers with custom deleters. The solution that Max is proposing here seems sound to me.
On top of that, this controversial design aspect is not documented. After having read the docs I was not aware that this conditional ownership decision is implemented. As a user, I need to be able to reason about the ownership of the resources, I need to have all the information available.
It is in the docs for `statement` (https://klemens.dev/sqlite/#statement) including a big yellow warning sign. I of course don't consider my own design decisions controversial.
Regards, &rzej;
Regards, Ruben.
[1]
https://lists.boost.org/archives/list/boost@lists.boost.org/message/57ZZ7IIW...
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at:
https://lists.boost.org/archives/list/boost@lists.boost.org/message/F62TWUTO...
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/DGK4RKXF...

On Mon, 1 Sept 2025 at 05:26, Maximilian Riemensberger via Boost <boost@lists.boost.org> wrote:
On 8/31/25 18:24, Klemens Morgenstern via Boost wrote:
On Sun, Aug 31, 2025 at 8:44 AM Maximilian Riemensberger via Boost < boost@lists.boost.org> wrote:
* The connection type can be owning or non-owning. Why wouldn't one instead have distinct connection (owning) and connection_ref (non-owning) types?
Because otherwise the complexity goes up unnecessarily. A user can write a function just taking `connection&` and not worry about this. In the library I have the `transaction` type for example. It holds a reference to the connection - if there was a second type like `connection_ref`, now I would have a second type of a transaction.
I see this point. However, a user that writes a function that takes `connection&` as an argument likely does not care about whether the connection is owning since the general expectation would be that the connection is connected and live before the function is called and remains so until the function returns. All those functions can just take `connection_ref` by value (just like functions take a string_view or span or function_ref) and no duplication is necessary, assuming connection_ref is implicitly constructible from `connection`.
Exactly. Note this is one of the footguns I also referred to in my review and this is the type of solution/usage Id' imagine.
* The statement type can construct owning and non-owning resultsets. The former automatically finalize, the latter reset the statement. Again why are those two behaviors combined in the same type?
Same reason as above...
Similar as for the connection: If the user function `interpret_result` does not take ownership of the resultset, that is the resultset that is passed into the function must outlive the return of the function, then interpret_result should only be written once and take resultset_ref as argument. resultset is implicitly convertible to resultset_ref.
This too, is reasonable. Not to derail this conversation - but as its the only issue I raised that Max didn't also raise: The non-throwing throwing duality is particularly quirky in resultset, and somewhat coupled to owning resultsets - in that having a resultset that when being iterated over or otherwise used to extract rows, returns (contains) a result that is (something like) a std::expected or boost::outcome not a row (that if not there can't be returned hence the throwing behaviour of operator*). Id' like to see some discussion on that
* The library is designed around unique_ptr wrapping the sqlite3 C API handles with optional ownership. In particular, this seems at times quite dangerous for the sqlite3_stmt handle which is shared by statement, resultset, field, etc. Would the library be better served by for example reference counting such handles? If not, why? There is no obvious place to put the counter. If sqlite had those, I'd use them.
Fair enough.
Is it? Why can't such reference counting be implemented in types provided by the library, not sqlite3 itself? It is not unusual to do this for other resource handles that the underlying implementation doesn't reference count or otherwise lifetime manage.

On Mon, Sep 1, 2025 at 10:13 AM Darryl Green via Boost < boost@lists.boost.org> wrote:
On Mon, 1 Sept 2025 at 05:26, Maximilian Riemensberger via Boost <boost@lists.boost.org> wrote:
On 8/31/25 18:24, Klemens Morgenstern via Boost wrote:
On Sun, Aug 31, 2025 at 8:44 AM Maximilian Riemensberger via Boost < boost@lists.boost.org> wrote:
* The connection type can be owning or non-owning. Why wouldn't one instead have distinct connection (owning) and connection_ref
(non-owning)
types?
Because otherwise the complexity goes up unnecessarily. A user can write a function just taking `connection&` and not worry about this. In the library I have the `transaction` type for example. It holds a reference to the connection - if there was a second type like `connection_ref`, now I would have a second type of a transaction.
I see this point. However, a user that writes a function that takes `connection&` as an argument likely does not care about whether the connection is owning since the general expectation would be that the connection is connected and live before the function is called and remains so until the function returns. All those functions can just take `connection_ref` by value (just like functions take a string_view or span or function_ref) and no duplication is necessary, assuming connection_ref is implicitly constructible from `connection`.
Exactly. Note this is one of the footguns I also referred to in my review and this is the type of solution/usage Id' imagine.
* The statement type can construct owning and non-owning resultsets. The former automatically finalize, the latter reset the statement. Again why are those two behaviors combined in the same type?
Same reason as above...
Similar as for the connection: If the user function `interpret_result` does not take ownership of the resultset, that is the resultset that is passed into the function must outlive the return of the function, then interpret_result should only be written once and take resultset_ref as argument. resultset is implicitly convertible to resultset_ref.
This too, is reasonable.
Not to derail this conversation - but as its the only issue I raised that Max didn't also raise:
The non-throwing throwing duality is particularly quirky in resultset, and somewhat coupled to owning resultsets - in that having a resultset that when being iterated over or otherwise used to extract rows, returns (contains) a result that is (something like) a std::expected or boost::outcome not a row (that if not there can't be returned hence the throwing behaviour of operator*).
Id' like to see some discussion on that
Can you explain a bit more what you mean? There is a non-throwing API for reading rows, but it cannot be used nicely in a ranged for-loop.
* The library is designed around unique_ptr wrapping the sqlite3 C API handles with optional ownership. In particular, this seems at times quite dangerous for the sqlite3_stmt handle which is shared by statement, resultset, field, etc. Would the library be better served by for example reference counting such handles? If not, why? There is no obvious place to put the counter. If sqlite had those, I'd use them.
Fair enough.
Is it? Why can't such reference counting be implemented in types provided by the library, not sqlite3 itself? It is not unusual to do this for other resource handles that the underlying implementation doesn't reference count or otherwise lifetime manage.
It's overhead that many users would find undesirable.
_______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/6C5SI6EZ...

On Tue, 2 Sept 2025 at 00:21, Klemens Morgenstern via Boost <boost@lists.boost.org> wrote:
On Mon, Sep 1, 2025 at 10:13 AM Darryl Green via Boost < boost@lists.boost.org> wrote:
There is no obvious place to put the counter. If sqlite had those, I'd use them.
Fair enough.
Is it? Why can't such reference counting be implemented in types provided by the library, not sqlite3 itself? It is not unusual to do this for other resource handles that the underlying implementation doesn't reference count or otherwise lifetime manage.
It's overhead that many users would find undesirable.
So if sqllite3 had the overhead - you would pay the penalty / accept it - but you don't think users would pay it if your library added it? How many users does this library have to make that statement? How bad is the performance impact? I admit there will be some set of users who will measure every cycle - they can use the raw C API at their own peril - or potentially one could provide some raw and some "less raw" types. Perhaps it is practical to do the wrapping as a user of the library (ie implement "safesqlite" on top of this library) but at about that point I think for ergonomics and efficiency some potential library users would just "fork" it i.e. borrow / take inspiration from your code.

On Tue, 2 Sept 2025 at 00:21, Klemens Morgenstern via Boost <boost@lists.boost.org> wrote:
On Mon, Sep 1, 2025 at 10:13 AM Darryl Green via Boost < boost@lists.boost.org> wrote:
Not to derail this conversation - but as its the only issue I raised that Max didn't also raise:
The non-throwing throwing duality is particularly quirky in resultset, and somewhat coupled to owning resultsets - in that having a resultset that when being iterated over or otherwise used to extract rows, returns (contains) a result that is (something like) a std::expected or boost::outcome not a row (that if not there can't be returned hence the throwing behaviour of operator*).
Id' like to see some discussion on that
Can you explain a bit more what you mean?
There is a non-throwing API for reading rows, but it cannot be used nicely in a ranged for-loop.
Yes exactly. But if it were a range of std::expected<row_t,error_t> (or some type of similar concept) and if the iterator became equal to end() on hitting an error ..

On Sun, Aug 24, 2025 at 9:54 PM Mohammad Nejati [ashtum] via Boost < boost@lists.boost.org> wrote:
The review of Boost.SQLite by Klemens Morgenstern begins today, Monday, August 25, and runs through Wednesday, September 3, 2025.
Question for the author, do we have links to any projects which use this library? Thanks

On Thu, Sep 4, 2025, 10:16 PM Vinnie Falco via Boost <boost@lists.boost.org> wrote:
On Sun, Aug 24, 2025 at 9:54 PM Mohammad Nejati [ashtum] via Boost < boost@lists.boost.org> wrote:
The review of Boost.SQLite by Klemens Morgenstern begins today, Monday, August 25, and runs through Wednesday, September 3, 2025.
Question for the author, do we have links to any projects which use this library?
No. I use it, but that's not Open-Source.
Thanks _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/HBPHFWOO...

Dear Boost community, # Review of boost_sqlite First, I want to thank Klemens Morgenstern for his work on this library and proposing it to boost, and Mohammad Nejati for managing the review. I think a C++ sqlite3 wrapper is a tremendously useful library and given the widespread use of SQLite certainly a valuable addition to Boost.
- What is your evaluation of the design?
The library is a thin wrapper around sqlite3 adding C++ specific quality of life features such as RAII for resource management and a more idiomatic C++ interface around the bare C API. I particularly like: * Convenient parameter binding for statements * Boost Describe integration * Both throwing and non-throwing interfaces * Boost JSON integration (although I did not try that myself) However, I ran into a couple of issues with the design: * Owning vs view types: Some types are not clearly distinguished as owning or non-owning, which can lead to confusion and potential resource management issues. The `connection` and `resultset` types are two primary examples. This is particuarly troublesome since those types together with `statement` are at the core of interacting with SQLite. * `connection`: It has an explicit `take_ownership` parameter and the destructor (via unique_ptr) will maybe close the database. Instead, I would consider it much safer to have an owning `connection` and a non-owning `connection_ref` type. * `resultset`: The ownership problems with resultset and statement are quite obvious when looking at the contained unique_ptr deleter. It does completely different things depending on whether the resultset owns the underlying sqlite statement (finalize) or not (prepare for reuse). The problems associated with it are actually highlighted in the library documentation in a few places. I would encourage the author to rethink how the management of statements and the results that they produce. * Connection close behavior: The connection class `close` calls `sqlite_close` whereas the destructor (via the unique_ptr deleter) calls `sqlite3_close_v2`. Despite the similarity in name, those two functions behave quite differently if there are outstanding database resources or operations such as prepared statements: `sqlite3_close` will fail with `SQLITE_BUSY` if there are any outstanding resources, whereas `sqlite3_close_v2` will forcibly release all resources. This difference came at a bit of surprise to me since it is not spelled out in the reference that close might not actually close. * Transaction scoping rules: The scoping rules around transations, statements and resultset are suprising. The following example demonstrates that: ``` { sqlite::transaction t{conn}; // use a transaction to speed this up auto st1 = conn.prepare(R"(insert into libitems (length, content_type) values ($length, $content_type) returning id)"); auto st2 = conn.prepare(R"(insert into libpaths (path, libitem) values ($path, $libitem))"); //{ // uncomment this scope to make it work auto res = st1.execute<rowid>({{"length", 123}, {"content_type", "application/octet-stream"}}); rowid id = res.current(); st2.execute({{"path", "lib/path/to/file"}, {"libitem", id.id}}); //} t.commit(); } ``` The requirement of this extra scope around the resultset was quite surprising to me. Nowhere in the library documentation is this behavior explained. I would much rather prefer the scope of the transaction itself sort out the order of destruction to make sure it commits correctly without extra nested scopes. If that's not possible, at least the library documentation needs to make the user aware of those subtleties. * Use of cstring_ref instead of string_view for text field and values: This is flawed in my opinion. sqlite3 recommends to access TEXT fields using `sqlite3_column_text` followed by `sqlite3_column_bytes` to get the length. This indicates that the text column could very well contain NUL bytes (despite causing problems in expressions) making a C-style string the wrong choice. * Completeness: Quite a few functions acting on the `statement` type are missing and the statement type also does not expose its underlying handle.
- What is your evaluation of the documentation?
Insufficient. The introduction and introductory examples are reasonable. But the design and reference documentation is severely lacking. When trying to use the library, I eventually ended up not looking at the documentation any more. Instead, I read the source code to figure out the sqlite3 C API functions that are used and then read the sqlite3 C API documentation. Non-exhaustive list of documentation issues: * It mostly lacks precise documentation of pre and post conditions and invariants managed by the types. This is particulary bad for types that maybe or maybe not own resources like `connection` and `resultset`. * The invalidation rules are inadequately documented. For example, compare
The view types can be invalidated when the database changes or the next row is read by the query.
The field type does not own the statement/query it was produced by. It is a merely a view into the resultset. Reading the next row will change the values returned.
with https://sqlite.org/c3ref/column_blob.html I don't want to imply that the entire sqlite3 documentation needs to be copied, but the essential parts of how those invalidation rules apply to the C++ constructs should be stated. And a link to the relevant sqlite3 page would also be helpful. * Since it wraps the sqlite3 C API, it should clearly spell out and link to the sqlite3 types and functions that are used by the library. Just one example: `connection::close()` needs to specify that is calls `sqlite3_close` and not `sqlite3_close_v2` and/or repeat the corresponding sqlite3 documentation of its behavior, while `~connection()` should specify that it calls the latter. * There is not even a mention of conversion behavior for sqlite values. The reference documentation of value and field should at the very least make the user aware of potential conversions and link to the relevant sqlite3 documentation page that has the details of how conversions are done and when this might result in invalidation of previously returned string/blob references (if applicable). * The `transaction` documentation and examples should emphasize the scope requirements for statements and their results used within a transaction. For example, it seems that all resultset created during the transaction seem must be destroyed prior to calling `commit`. * I did not find any design documentation or rationale for the library's design decisions regarding ownership handling of sqlite3 resources. I have to read the implementation and guess the reasoning behind certain choices.
- What is your evaluation of the implementation?
I did only look at selected parts of the implementation whenever the docs were insufficient to explain the precise contracts of types and functions. What I saw was mostly what I would have expected from the thin wrapper around a C API. Random notes: * resultset: The destructor steps through the entire resultset rows. I don't think this is necessary since the sqlite3 docs would indicate that you can reset or finalize a statement at any time. * field access: Retrieving text fields should follow recommended sqlite3 practice: `sqlite3_column_text` followed by `sqlite3_column_bytes` to get the length. I also took a quick look at the tests. Those seem to be rather superficial. They don't cover much beyond the most basic usage scenarios for those types which I looked at.
- What is your evaluation of the potential usefulness of the library?
Potentially very useful. It does the tedious work of mapping the sqlite3 C API onto C++. I would use it if I need to work with sqlite from C++, but likely patched to avoid the ownership issues. Its usefulness is primarily limited by the lack of precise documentation which makes it very difficult to figure out the exact behavior of code that is written with the library.
Do you already use it in industry?
No.
- Did you try to use the library? With which compiler(s)? Did you have any problems?
Yes. I modified some of the examples to try out a few different queries and transaction related examples. I used debian 13 with its default gcc. Everything except the boost url dependent examples compiled.
- How much effort did you put into your evaluation? A glance? A quick reading? In-depth study?
A medium depth study from a user perspective. I spent a couple of hours on and off digging through the docs of the library (and the sqlite3 docs) and worked with the examples.
- Are you knowledgeable about the problem domain?
I'm proficient in C++ but have only had very limited exposure to the sqlite3 C API.
At the end of your review, please state clearly whether you ACCEPT, REJECT, or CONDITIONALLY ACCEPT (with explicit conditions) the proposed Boost.SQLite library.
Most of the issues raised in this review can be fixed. And most of it is "just" work. So I'm torn between conditionally accept and reject but encourage resubmission. The lack of documentation in particular precise reference documentation is the biggest issue in my opinion. It tips my recommendation towards REJECT but encourage resubmission. If the review manager decides to conditionally accept the library, I strongly recommend making significant documentation improvements the a condition for acceptance. Best regards Max On 8/25/25 06:52, Mohammad Nejati [ashtum] via Boost wrote:
Dear Boost community,
The review of Boost.SQLite by Klemens Morgenstern begins today, Monday, August 25, and runs through Wednesday, September 3, 2025.
Boost.SQLite provides a safer and more expressive C++ interface to the SQLite3 C-API to simplify usage and reduce errors.
You can read the documentation at: https://klemens.dev/sqlite and study or try out the code at: https://github.com/klemens-morgenstern/sqlite
This is the second review attempt. The first concluded without a result due to too few submitted reviews.
Anyone with experience in C++ or SQLite, at any level, is encouraged to contribute. In your review, please indicate how much time you spent on the evaluation.
Here are some questions you might find helpful when writing your review:
- What is your evaluation of the design? - What is your evaluation of the implementation? - What is your evaluation of the documentation? - What is your evaluation of the potential usefulness of the library? Do you already use it in industry? - Did you try to use the library? With which compiler(s)? Did you have any problems? - How much effort did you put into your evaluation? A glance? A quick reading? In-depth study? - Are you knowledgeable about the problem domain?
At the end of your review, please state clearly whether you ACCEPT, REJECT, or CONDITIONALLY ACCEPT (with explicit conditions) the proposed Boost.SQLite library.
Your feedback is critical to the success of this review. Thank you in advance for your time, expertise, and contributions.
Best regards, Mohammad Nejati Review Manager for the proposed Boost.SQLite _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/FK4NIN2J...

I don't have the time (and courage...) to write a review. But I feel compelled to comment on some of what I read. On Sun, Sep 7, 2025 at 10:58 PM Maximilian Riemensberger via Boost <boost@lists.boost.org> wrote:
* The `transaction` documentation and examples should emphasize the scope requirements for statements and their results used within a transaction.
That makes no sense to me. Your example with the scope made no sense to me. Yet I've been using SQLite for almost 20 years, in raw C and various C++ wrappers.
* I did not find any design documentation or rationale for the library's design decisions regarding ownership handling of sqlite3 resources.
Most reviewers have disclosed they don't have much experience with SQLite. This shows on this point. You often don't own the main connection object. Large and old code bases using SQLite have different subsystems, written by different people, each wanting to use its favorite flavor of SQLite. And that includes heavy weights like Qt and its Sql "module". We even have to use our own Qt SQLite Driver to be able to inject a connection owned by lower layers of our code. So my point is essential to recognize that Boost.SQLite, if it arrives at all, arrives very late, and has to integrate with the messy reality of own grown and OSS wrappers, sometimes on the *same* connections. And as Klemens wrote, you don't have to have to duplicate APIs you can into, or templatize them, just because the connection is owning or not. Whether the RAII or not the connection is the client-code's choice. And most code shouldn't care at all, and just use it.
* resultset: The destructor steps through the entire resultset rows. I don't think this is necessary since the sqlite3 docs would indicate that you can reset or finalize a statement at any time.
This also does not make sense to me, if it's true. You can abandon a partially read "resultset" at any time. I think maybe people don't realize that SQLite is an *embedded* DB engine. The very term resultset is misleading. There's no resultset type in SQLite, like there is in PostgreSQL's LIBPQ for example. The term resultset give the impression the result of a query is "materialized" somehow. It's NOT! An SQLite prepared statement is a bytecode program, compiled from SQL text. And when you _step() that statement, you start the execution of that program and move the PC ("Program Counter") to the generate the next row. I don't even think the row is itself "materialized", it could be just a reference to a particular row on a table btree page, "paged-in" in the page cache. And when you read a value from that row, SQLite dynamically decodes the row to get the requested value, optionally doing implicit conversion on that value, if the way its accessed by the client code differs from that value's internal type (which can be different from the declared type of the column or expression it came from!). So an SQLite statement is a compiled bytecode program, but it's also like a cursor or in C++ terms a coroutine that is resumed on every _step() call to advance to the next row. So it's not a resultset per se. I saw from review that Klemens chose .read_next() for _step(), which conveys the WRONG message IMHO. Such a name makes sense for MySQL or PostgreSQL, which are client-server RDBMSs with a well defined Network protocol over a socket, but there's no such thing in SQLite. Trying to coerce an SQLite's statement into Boost.SQLite statement, resultset, row, and field types, when it all comes from a single object in SQLite itself (and its related API), leads to these various abstractions all having to share the same handle, and brings about the lifetime issues that displeases reviewers.
* field access: Retrieving text fields should follow recommended sqlite3 practice: `sqlite3_column_text` followed by `sqlite3_column_bytes` to get the length.
Completely agree on that one. There's really not much difference between an SQLite blob and text value, at the storage level, in both cases SQLite knows the size. Yes, built-in SQLite SQL function assume null-terminated string (and the SQLite APIs ensures all text buffer are null-terminated), you can insert/select them w/o any issues, so assuming null-termination for strings is a mistake in the API, and it's also inefficient to strlen in O(N) when SQLite knows the size in O(1). SQLite is single-threaded, through and through. Statements are coroutines. You can have several statements, and _step() their rows in any order, w/o issues, as long as it's all done serially, and you decide whether SQLite ensures that for you, or not, when you know you're already single-threaded from your own code. Because it's single threaded, it can get away with the connection acting as a kind of singleton, so when you insert rows in ROWID tables for example, you don't ask the last-inserted-rowid from the statement, but from the connection. And a connection knows / tracks all its statements internally, there's even an API to iterate unfinished statements. So you could even "leak" your statements in the C++ sense, and "fix" that later, by iterating them via the connection, and finalizing them. Or do that behind the back of C++ wrappers that think they "own" the statement, and result in double-finalize, which may be harmless and result in just an SQLITE_MISUSE, or something more nefarious. It seems to me many reviewers looks at this review from the high ground of experienced C++ developers, w/o knowing the messy realities of SQLite. And perhaps missing the point I'm afraid. I don't know what the right answer is, what the right API is. But IMHO, with SQLite, any "resultset" abstraction will be "leaky" I'm afraid. Ownership will be "messy", from having to integrate with existing code bases and other wrappers, and from lifetimes which are not purely C++ based. Anyways, enough of a rant. Sorry about that... --DD

On Mon, Sep 8, 2025 at 12:35 PM Dominique Devienne via Boost <boost@lists.boost.org> wrote:
And as Klemens wrote, you don't have to have to duplicate APIs you can into, or templatize them, just because the connection is owning or not.
There was a suggestion to add `connection_ref`, which is implicitly constructible from `connection`. Practically, when we pass a connection, the callee is aware of its ownership, because if it receives an owned connection, it probably needs to transfer it somewhere, unless we are dealing with coroutines or similar models, where the caller cannot preserve ownership of an object during execution, but that is a different story.
I think maybe people don't realize that SQLite is an *embedded* DB engine. The very term resultset is misleading. There's no resultset type in SQLite, like there is in PostgreSQL's LIBPQ for example. The term resultset give the impression the result of a query is "materialized" somehow. It's NOT!
Yes, I think the problem boils down to the fact that a resultset is tied to a statement, and trying to make it an owning type causes confusion around lifetime management and gives the false impression that multiple resultsets can exist for a single statement. It might be better to merge the functionality of resultset to statement and leave resultset as a reference type that is constructible from a statement (and possibly rename it to resultset_ref). This way, usages like the following will remain possible without having to pass a statement& around: void print(std::ostream &os, sqlite::resultset rw)

On Mon, Sep 8, 2025 at 12:13 PM Mohammad Nejati <ashtumashtum@gmail.com> wrote:
On Mon, Sep 8, 2025 at 12:35 PM Dominique Devienne via Boost <boost@lists.boost.org> wrote:
I think maybe people don't realize that SQLite is an *embedded* DB engine. The very term resultset is misleading. There's no resultset type in SQLite, like there is in PostgreSQL's LIBPQ for example. The term resultset give the impression the result of a query is "materialized" somehow. It's NOT!
Yes, I think the problem boils down to the fact that a resultset is tied to a statement, and trying to make it an owning type causes confusion around lifetime management and gives the false impression that multiple resultsets can exist for a single statement. It might be better to merge the functionality of resultset to statement and leave resultset as a reference type that is constructible from a statement (and possibly rename it to resultset_ref). This way, usages like the following will remain possible without having to pass a statement& around: void print(std::ostream &os, sqlite::resultset rw)
[sqlite3pp][1] has a statement class, that's the base abstract wrapper, adding RAII and typesafety (and overloading to drive higher abstractions). And separate command and query derived concrete classes, with "streaming" getters and "setters" (to bind) as syntax sugar. i.e. there's no resultset at all. The "statement" remains the only object. There are more types on the query side, to support foreach loops, but not in a way that blur the real picture with a made-up resultset. Yes, it's an old wrapper that hasn't kept pace. It's more abstraction are closer to the reality of SQLite IMHO. FWIW. PS: BTW, there's no such thing as named-binding in SQLite per-se. There's binding-by-index, and an API to get the index of a placeholder name. So basically name-binding is always more "overhead". I never used it... Smaller API surface too, unlike what sqlite3pp does, and I suspect Klemens'. [1]: https://github.com/iwongu/sqlite3pp/blob/master/src/sqlite3pp.h

On Mon, 8 Sept 2025 at 21:10, Dominique Devienne via Boost <boost@lists.boost.org> wrote:
[sqlite3pp][1] has a statement class, that's the base abstract wrapper, adding RAII and typesafety (and overloading to drive higher abstractions). And separate command and query derived concrete classes, with "streaming" getters and "setters" (to bind) as syntax sugar.
i.e. there's no resultset at all. The "statement" remains the only object. There are more types on the query side, to support foreach loops, but not in a way that blur the real picture with a made-up resultset.
Yes, it's an old wrapper that hasn't kept pace. It's more abstraction are closer to the reality of SQLite IMHO. FWIW.
This sounds reasonable and avoids/resolves the area that feels most surprising in the proposed library, where the real sqlite model leaks through the somewhat forced mapping to a distinct resultset. The leakage is a pile of "don't do that" that would be obvious - if there wasn't a distinct object. Iterating over the (possibly non-existent) results of a statement seems fine. The statement being busy is a state not a distinct object. And viewing the step as forward iterating the statement itself, and there is no ïs it a ref or .. of course it is. There isn't anything else you can get from a statement other than results. This also relates to the patchy support for non throwing usage - is it a result or an exception" should be a simple question to answer. Throwing for a logic error where you have e.g. tried to iterate over a statement that hasn't been prepared but NOT throwing where the result of step is a legitimate "not a row" result like BUSY or even SCHEMA (change has invalidated the statement) fits better with my expectations at least, re using any SQL database. The fact in a simple enough program/usage you can ensure it is impossible to get these when using sqlite (rather than it being simply broken to assume you wont get them in a shared access database) doesn't mean the library interface should make more advanced usage ugly and require use of a more or completely raw sqlite3 interface.

On Mon, Sep 8, 2025 at 2:31 PM Darryl Green <darryl.green@gmail.com> wrote:
The fact in a simple enough program/usage you can ensure it is impossible to get these when using sqlite (rather than it being simply broken to assume you wont get them in a shared access database) doesn't mean the library interface should make more advanced usage ugly and require use of a more or completely raw sqlite3 interface.
My own feeling is that an SQLite wrapper should have two levels of wrapping. One that sticks to SQLite's API almost 100%, with RAII types for handles, and classes that expose binding and getting value (and pushing results from custom functions or vtables) in a typesafe manner, with overloading where necessary. Supports std::string_view for text and std::span<std::byte> for blob. Expose explicitly static (by-ref) vs dynamic (i.e. copy) binding. NULL as a datatype. And another layer, on top, with higher level abstractions, implemented on the lower-level wrapping of SQLite's API. std::ref<T> binding means by-ref binding (no copy is SQLite, the client code knows the (large) memory lives at least until the _step() call). std::optional<T> when getting a result, instead of throwing on NULL as a guard-rail / hand-holding. Etc... And the even higher-level magic of getting rows directly into structs within foreach loops. The first layer should be usable on its own, be verbose and explicit like SQLite is. But basically have zero to almost no overhead. Should basically disappear once compiled. I looked at the vtable example, and saw it returns boost::variant2. Which means an extra copy compared to using [sqlite3_result_blob][1] directly. And hide the destructor object you can give SQLite to manage the lifetime of what you give it. (please correct me if I'm wrong). I'm not saying the approach using variant2 is wrong, for some people. But it's not zero overhead, and IMHO a Boost-quality wrapper should be very overhead for SQLite, w/o having to resort back to pure C and the official API. In my vision of the dual-level wrapping, many types don't control lifetime. They're temporaries over raw handles to add safety and convenient. Regarding the examples, I'd want more useful things. I.e. for table-valued functions, a better [CArray][2] for C++ That works around the lack of arrays in SQLite. You can't do SELECT ... WHERE col = ANY($1) like you can in PostgreSQL libpq, biding a single value that's an array (of PKs for example). I.e. put the use of the table-valued function over a C++ contain in a useful SQL context, that's very common to people resorting to WHERE col in (?, ?, ?, ....) Instead of integrating with Boost.JSON, which is only a tiny convenient to hide the serialization / deserialization, write a competitor to SQLite's own built-in [JSON1][3] to demonstrate Boost.JSON is faster than SQLite's own JSON parser (or is it???), while at the same time providing excellent examples to eponymous tables. An example using Boost.Multi-Index that automatically is able to implement best_index() based on the meta-programming and the indexes exposed by the BMI, kinda like want we did years ago but crudely (Steven helped with the meta-programming at the time I recall). It knows whether the index is UNIQUE or NOT, whether it's hash-based or ordered, and based on that, synthetize or at least help synthetize it. I.e. advantage magic stuff that's almost a 3rd level of wrapping, bridging SQLite and Multi-Index. And then there are things around SQLite that go beyond wrapping the SQLite API. For example, I wanted access to header values that there are no pragmas for. But instead of redoing the parsing myself, I used tricks as below, which I suspect few people know about: application_id_ = db_.executeInt("PRAGMA application_id"); user_version_ = db_.executeInt("PRAGMA user_version"); page_size_ = db_.executeInt("PRAGMA page_size"); sqlite3_file* db_fp = nullptr; if (SQLITE_OK == sqlite3_file_control(db_._handle, nullptr, SQLITE_FCNTL_FILE_POINTER, &db_fp)) { if (const sqlite3_io_methods* io = db_fp->pMethods) { int val = 0; if (SQLITE_OK == io->xRead(db_fp, &val, 4, 24)) { change_counter_ = boost::endian::big_to_native(val); } if (SQLITE_OK == io->xRead(db_fp, &val, 4, 96)) { sqlite_version_ = boost::endian::big_to_native(val); } } else { // in-memory DB change_counter_ = 0; // TODO sqlite_version_ = SQLITE_VERSION_NUMBER; } } Or I needed to be able to open an SQLite DB that's truncated, because I partially read only the first 64KB of the DB from a TAR, So I could tell whether the large DBs inside that TAR are up-to-date or not, w/o untarring GBs of data. Which requires a sqlite3_db_config trick. bool DbInfo::isSQLiteDB( const fs::path& path, AllowTruncated allow_truncated, int* p_user_version ) { assert(fs::is_regular_file(path)); auto filename = path.string(); sqlite3* db = nullptr; int errc = sqlite3_open_v2(filename.c_str(), &db, SQLITE_OPEN_READONLY, nullptr); if (errc != SQLITE_OK) { sqlite3_close(db); // must close even on error return false; } if (bool(allow_truncated)) { // see https://sqlite.org/forum/forumpost/324ee8bd52 int new_val = 1; int actual_val = 0; sqlite3_db_config(db, SQLITE_DBCONFIG_WRITABLE_SCHEMA, new_val, &actual_val); //sqlite3_db_config(db, SQLITE_DBCONFIG_TRUNCATED_SCHEMA, 1); } sqlite3_stmt* stmt = nullptr; const char* sql_end = nullptr; constexpr char sql[] = "pragma user_version"; errc = sqlite3_prepare_v2(db, sql, sizeof(sql) - 1, &stmt, &sql_end); if ((errc == SQLITE_OK) && stmt) { assert((sql_end - sql) == sizeof(sql) - 1); if (SQLITE_ROW != sqlite3_step(stmt)) { errc = SQLITE_NOTADB; } else if (p_user_version) { *p_user_version = sqlite3_column_int(stmt, 0); } sqlite3_finalize(stmt); } else { errc = SQLITE_NOTADB; } sqlite3_close(db); return errc == SQLITE_OK; } That's the SQLite wrapper I have in mind. Different layers of increasing complexity, and higher level of abstractions. Collections of tricks and utilities around SQLite DBs. And possibly even competing with SQLite, on generating DBs directly, w/o going through the SQLite API, the SQL, bytecode VM, the Btree and page cache, but just the [file format][4], when you know in advance the cardinality and control the ordering, to precompute fan-out of the intermediate Btree pages, overflow pages, and can thus "stream" the database, possibly concurrently on several table, by reserving blocks of pages, and patching the root table in sqlite_master after the fact. That's my vision. But it's just that, a vision. --DD [1]: https://sqlite.org/c3ref/result_blob.html [2]: https://www.sqlite.org/carray.html [3] https://www.sqlite.org/json1.html [4] https://www.sqlite.org/fileformat.html

On Mon, Sep 8, 2025 at 7:08 PM Dominique Devienne via Boost < boost@lists.boost.org> wrote:
On Mon, Sep 8, 2025 at 12:13 PM Mohammad Nejati <ashtumashtum@gmail.com> wrote:
On Mon, Sep 8, 2025 at 12:35 PM Dominique Devienne via Boost <boost@lists.boost.org> wrote:
I think maybe people don't realize that SQLite is an *embedded* DB engine. The very term resultset is misleading. There's no resultset type in SQLite, like there is in PostgreSQL's LIBPQ for example. The term resultset give the impression the result of a query is "materialized" somehow. It's NOT!
Yes, I think the problem boils down to the fact that a resultset is tied to a statement, and trying to make it an owning type causes confusion around lifetime management and gives the false impression that multiple resultsets can exist for a single statement. It might be better to merge the functionality of resultset to statement and leave resultset as a reference type that is constructible from a statement (and possibly rename it to resultset_ref). This way, usages like the following will remain possible without having to pass a statement& around: void print(std::ostream &os, sqlite::resultset rw)
[sqlite3pp][1] has a statement class, that's the base abstract wrapper, adding RAII and typesafety (and overloading to drive higher abstractions). And separate command and query derived concrete classes, with "streaming" getters and "setters" (to bind) as syntax sugar.
i.e. there's no resultset at all. The "statement" remains the only object. There are more types on the query side, to support foreach loops, but not in a way that blur the real picture with a made-up resultset.
I think it's more a naming problem. If the resultset was named cursor or result_cursor or something a lot of it would be more obvious. I did separate this into multiple classes so that a lot of misuse cases can get caught at compile time. You are correct that the alternative is to put everything into one big class. I don't quite like it, because there's a subset of the stmt functions you can use depending on the context. Am I correct to guess that you would prefer a statement class that just is a RAII & error_code wrapper around sqlite3_stmt ?
Yes, it's an old wrapper that hasn't kept pace. It's more abstraction are closer to the reality of SQLite IMHO. FWIW.
PS: BTW, there's no such thing as named-binding in SQLite per-se. There's binding-by-index, and an API to get the index of a placeholder name. So basically name-binding is always more "overhead". I never used it... Smaller API surface too, unlike what sqlite3pp does, and I suspect Klemens'.
Correct, but I find it quite readable for large queries that are slow anyway.
[1]: https://github.com/iwongu/sqlite3pp/blob/master/src/sqlite3pp.h _______________________________________________ Boost mailing list -- boost@lists.boost.org To unsubscribe send an email to boost-leave@lists.boost.org https://lists.boost.org/mailman3/lists/boost.lists.boost.org/ Archived at: https://lists.boost.org/archives/list/boost@lists.boost.org/message/BI7RVLJE...

+ On Mon, Sep 8, 2025 at 10:07 PM Peter Dimov via Boost <boost@lists.boost.org> wrote:
Klemens Morgenstern wrote:
I think it's more a naming problem. If the resultset was named cursor or result_cursor or something a lot of it would be more obvious.
Probably not. "Cursor" still implies that you can have several.
I wrote a new design the statement/resultset to follow the sqlite API closer and implement C++ conventions here: https://github.com/klemens-morgenstern/sqlite/pull/16 new docs: http://klemens.dev/sqlite-statement This removes the resultset & static_resultset completely and makes iterating the statement explicit. Instead of for (auto res : conn.prepare("select 42").execute<std::tuple<int>>()); it is now: auto q = conn.prepare("select 42"); for (auto res : sqlite::statement_range<std::tuple<int>>(q)); // or for (auto itr = sqlite::statement_iterator<std::tuple<int>>(q); itr != sqlite::statement_iterator<std::tuple<int>>(q); itr ++); Where sqlite::statement_range behaves like std::ranges::istream and sqlite::statement_iterator like std::istream_iterator. By default, they return a `sqlite::row`, but as shown above, they can be modified to provide the same functionality as the previous static_resultset. By using input iterators the ownership should be completely obvious now and it should work as any experienced sqlite developer expects. I think this addresses the major issue (since the connection ownership design is trivial and has been discussed), but I'd like to ask for some feedback on this design.

On Thu, Sep 11, 2025 at 2:25 PM Klemens Morgenstern via Boost <boost@lists.boost.org> wrote:
auto q = conn.prepare("select 42"); for (auto res : sqlite::statement_range<std::tuple<int>>(q));
Would it be possible to have begin() and end() on the statement by default? That way, the following would work: for (auto row : conn.query("select * from ...")); Also, I see that the connection::query has been removed. What was the reason for that?

On Thu, Sep 11, 2025 at 9:22 PM Mohammad Nejati <ashtumashtum@gmail.com> wrote:
On Thu, Sep 11, 2025 at 2:25 PM Klemens Morgenstern via Boost <boost@lists.boost.org> wrote:
auto q = conn.prepare("select 42"); for (auto res : sqlite::statement_range<std::tuple<int>>(q));
Would it be possible to have begin() and end() on the statement by default? That way, the following would work: for (auto row : conn.query("select * from ..."));
Of course it would be, but now it might not be obvious that iterating actually modifies the statement. In the old API the .execute() communicated that, because you were iterating a resultset. Well, not clearly enough, but that was the idea.
Also, I see that the connection::query has been removed. What was the reason for that?
It doesn't query, it just creates a prepared statement. So it's just `prepare` everywhere now, because it's sqlite_prepare_v2 under the hood.

On Thu, Sep 11, 2025 at 4:57 PM Klemens Morgenstern <klemensdavidmorgenstern@gmail.com> wrote:
Of course it would be, but now it might not be obvious that iterating actually modifies the statement. In the old API the .execute() communicated that, because you were iterating a resultset. Well, not clearly enough, but that was the idea.
Do you mean writing: auto stmt = conn.prepare("select * from ..."); for (auto row : sqlite::statement_range(stmt)) instead of: for (auto row : conn.prepare("select * from ...")) makes a difference in terms of clarity about whether iterating over a statement modifies it or not?

czw., 11 wrz 2025 o 12:56 Klemens Morgenstern via Boost < boost@lists.boost.org> napisał(a):
+ On Mon, Sep 8, 2025 at 10:07 PM Peter Dimov via Boost < boost@lists.boost.org> wrote:
Klemens Morgenstern wrote:
I think it's more a naming problem. If the resultset was named cursor or result_cursor or something a lot of it would be more obvious.
Probably not. "Cursor" still implies that you can have several.
I wrote a new design the statement/resultset to follow the sqlite API closer and implement C++ conventions here: https://github.com/klemens-morgenstern/sqlite/pull/16 new docs: http://klemens.dev/sqlite-statement
This removes the resultset & static_resultset completely and makes iterating the statement explicit.
Instead of
for (auto res : conn.prepare("select 42").execute<std::tuple<int>>());
it is now:
auto q = conn.prepare("select 42"); for (auto res : sqlite::statement_range<std::tuple<int>>(q)); // or for (auto itr = sqlite::statement_iterator<std::tuple<int>>(q); itr != sqlite::statement_iterator<std::tuple<int>>(q); itr ++);
Where sqlite::statement_range behaves like std::ranges::istream and sqlite::statement_iterator like std::istream_iterator.
By default, they return a `sqlite::row`, but as shown above, they can be modified to provide the same functionality as the previous static_resultset.
By using input iterators the ownership should be completely obvious now and it should work as any experienced sqlite developer expects.
I think this addresses the major issue (since the connection ownership design is trivial and has been discussed), but I'd like to ask for some feedback on this design.
Thank you for a quick turnaround. My feedback is based on reading the docs rather than looking at the implementation. I am confused by the usage of the name "prepare". For me, it associates with the idea of a "prepared statement", and "prepared statement" for me means "not ready to execute yet: you need to provide values in place of all the placeholders first." Is my intuition wrong? Maybe use q.statement_range<type>() rather than sqlite::statement_range<type>(q) ? Or is statement_range applicable to anything other than statement? I also do not understand the idea behind the second true/false argument. How can type checks not be performed when I explicitly provide types to which to cast the values? Anyway, a naked true/false argument is difficult to understand when seen in the code. Maybe instead two functions: sqlite::statement_range<type>(q); sqlite::statement_range_strict<type>(q); ? Regards, &rzej;

On Thu, Sep 11, 2025 at 3:56 AM Klemens Morgenstern via Boost < boost@lists.boost.org> wrote:
I wrote a new design the statement/resultset to follow the sqlite API ... Instead of
for (auto res : conn.prepare("select 42").execute<std::tuple<int>>());
it is now:
On the one hand, I think that exploring the API design space to improve and polish the interface for end users is great, so kudos for that. However, this is exactly the sort of thing that could be done before the formal review by going out into the market and acquiring users with real-world use-cases and real-world problems who can provide the best possible feedback for changes. I think the model of writing a library direct-for-review works for certain authors (Joaquin with Bloom and Peter with Hash2 come to mind). Yet I do not think that it works in general. For example I always build up substantial users for my own libraries and the feedback ahead of the formal review has been pivotal in getting the libraries into shape. I would encourage this model going forward. Thanks

Le 2025-09-11 16:33, Vinnie Falco via Boost a écrit :
On the one hand, I think that exploring the API design space to improve and polish the interface for end users is great, so kudos for that.
However, this is exactly the sort of thing that could be done before the formal review by going out into the market and acquiring users with real-world use-cases and real-world problems who can provide the best possible feedback for changes. I think the model of writing a library direct-for-review works for certain authors (Joaquin with Bloom and Peter with Hash2 come to mind). Yet I do not think that it works in general. For example I always build up substantial users for my own libraries and the feedback ahead of the formal review has been pivotal in getting the libraries into shape. I would encourage this model going forward.
Isn't it exactly what the boost library incubator was designed for? Give visibility to a library, explicitly state it as experimental, so breakage and unstabilities are very likely to happen, and at some point when the author think it's ready, make a formal review. Why didn't it work like expected? Regards, Julien

On Thu, Sep 11, 2025 at 11:46 PM Julien Blanc via Boost < boost@lists.boost.org> wrote:
Isn't it exactly what the boost library incubator was designed for? ...Why didn't it work like expected?
Are we talking about Robert Ramey's incubator? Is that still up? Anyway.... it is a passive resource, not doing anything on its own. I think if you want to get users you have to promote the library on social media. Post to r/cpp. Maybe post to Hacker News. Search through GItHub for C++ projects that are using the raw SQLite C API, and open an issue pointing them to the proposed library with an offer of support. Invite them to the Official C++ Language Slack Workspace for support, to either a dedicated channel or a group channel like #boost. Answer their questions, and so on. Of course this is not an official requirement of libraries submitted for review. Yet it can't hurt. Thanks

pt., 12 wrz 2025 o 08:46 Julien Blanc via Boost <boost@lists.boost.org> napisał(a):
Le 2025-09-11 16:33, Vinnie Falco via Boost a écrit :
On the one hand, I think that exploring the API design space to improve and polish the interface for end users is great, so kudos for that.
However, this is exactly the sort of thing that could be done before the formal review by going out into the market and acquiring users with real-world use-cases and real-world problems who can provide the best possible feedback for changes. I think the model of writing a library direct-for-review works for certain authors (Joaquin with Bloom and Peter with Hash2 come to mind). Yet I do not think that it works in general. For example I always build up substantial users for my own libraries and the feedback ahead of the formal review has been pivotal in getting the libraries into shape. I would encourage this model going forward.
Isn't it exactly what the boost library incubator was designed for? Give visibility to a library, explicitly state it as experimental, so breakage and unstabilities are very likely to happen, and at some point when the author think it's ready, make a formal review. Why didn't it work like expected?
I think nowadays a GitHub repo is a good enough place to store a candidate library along with its documentation. The task for the author is to attract sufficient interest for their library. I have a small library that I consider proposing at some point at: https://github.com/akrzemi1/markable I would occasionally ask for feedback. But it is generally difficult to draw the expert's attention. The Boost Review process is generally a sufficiently strong incentive that experts get involved. For me this is like, "if I do not intervene at this point, the library will just get accepted", so I make sure that I drop other things in my life and find some time to contribute. In a way, I think that the Boost Review process when the rejection is interpreted as a way of iteratively improving the design is a good way forward. Regards, &rzej;

On Fri, Sep 12, 2025 at 7:48 AM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
I think nowadays a GitHub repo is a good enough place to store a candidate library along with its documentation. The task for the author is to attract sufficient interest for their library.
I have a small library that I consider proposing at some point at: https://github.com/akrzemi1/markable I would occasionally ask for feedback. But it is generally difficult to draw the expert's attention.
The incubator should be a vcpkg registry that we host up on github.com/boostorg too. This would make it easy for users to try out incubated libraries. - Christian

Andrzej Krzemienski wrote:
I think nowadays a GitHub repo is a good enough place to store a candidate library along with its documentation. The task for the author is to attract sufficient interest for their library.
I have a small library that I consider proposing at some point at: https://github.com/akrzemi1/markable I would occasionally ask for feedback. But it is generally difficult to draw the expert's attention.
One relative obstacle is that a library needs to have a different structure when used standalone and when it's part of the Boost superproject. When I prepare a new library for submission, I make it adhere to the latter structure, so that when accepted, it can go into Boost immediately and as-is. But this makes it inconvenient for others to try it out. We probably need to figure something out on that front because it's 2025. I suppose nowadays one can require C++17 or C++20 and avoid the need for depending on Boost libraries, which makes things much easier.

pon., 8 wrz 2025 o 11:05 Dominique Devienne via Boost <boost@lists.boost.org> napisał(a):
* I did not find any design documentation or rationale for the library's design decisions regarding ownership handling of sqlite3 resources.
Most reviewers have disclosed they don't have much experience with SQLite. This shows on this point. You often don't own the main connection object. Large and old code bases using SQLite have different subsystems, written by different people, each wanting to use its favorite flavor of SQLite. And that includes heavy weights like Qt and its Sql "module". We even have to use our own Qt SQLite Driver to be able to inject a connection owned by lower layers of our code.
So my point is essential to recognize that Boost.SQLite, if it arrives at all, arrives very late, and has to integrate with the messy reality of own grown and OSS wrappers, sometimes on the *same* connections. And as Klemens wrote, you don't have to have to duplicate APIs you can into, or templatize them, just because the connection is owning or not.
Whether the RAII or not the connection is the client-code's choice. And most code shouldn't care at all, and just use it.
This is a very important point. And somewhat incompatible with what Boost.SQLite docs say. The framework returning a handle to connection is not the only motivation for having a non-owing connection. Another, as you say, is the user already employing another library that does the resource management. This would mean that a `connection_ref` class (if Boost.SQLite takes that route) needs to have a constructor (apart from one taking `connection`) taking the `sqlite3*` parameter. Regards, &rzej;

niedz., 7 wrz 2025 o 22:59 Maximilian Riemensberger via Boost < boost@lists.boost.org> napisał(a):
* Transaction scoping rules: The scoping rules around transations, statements and resultset are suprising. The following example demonstrates that:
``` { sqlite::transaction t{conn}; // use a transaction to speed this up
auto st1 = conn.prepare(R"(insert into libitems (length, content_type) values ($length, $content_type) returning id)");
auto st2 = conn.prepare(R"(insert into libpaths (path, libitem) values ($path, $libitem))");
//{ // uncomment this scope to make it work auto res = st1.execute<rowid>({{"length", 123}, {"content_type", "application/octet-stream"}});
rowid id = res.current();
st2.execute({{"path", "lib/path/to/file"}, {"libitem", id.id}}); //}
t.commit(); } ```
The requirement of this extra scope around the resultset was quite surprising to me. Nowhere in the library documentation is this behavior explained. I would much rather prefer the scope of the transaction itself sort out the order of destruction to make sure it commits correctly without extra nested scopes. If that's not possible, at least the library documentation needs to make the user aware of those subtleties.
Would it make sense to offer an alternative interface for the transactions in this library? conn.transaction([&](sqlite::connection& conn) { auto st1 = conn.prepare(R"(insert into libitems (length, content_type) values ($length, $content_type) returning id)"); auto st2 = conn.prepare(R"(insert into libpaths (path, libitem) values ($path, $libitem))"); { auto res = st1.execute<rowid>({{"length", 123}, {"content_type", "application/octet-stream"}}); rowid id = res.current(); st2.execute({{"path", "lib/path/to/file"}, {"libitem", id.id}}); } }); This way, no explicit `commit` instruction is necessary: the implementation of member function transaction() can have it at the end of the scope. There is no way to forget the commit upon successful execution of the instructions. This would also correctly roll back a transaction upon any exception. Regards, &rzej;

On Mon, Sep 8, 2025 at 9:41 PM Andrzej Krzemienski via Boost < boost@lists.boost.org> wrote:
niedz., 7 wrz 2025 o 22:59 Maximilian Riemensberger via Boost < boost@lists.boost.org> napisał(a):
* Transaction scoping rules: The scoping rules around transations, statements and resultset are suprising. The following example demonstrates that:
``` { sqlite::transaction t{conn}; // use a transaction to speed this up
auto st1 = conn.prepare(R"(insert into libitems (length, content_type) values ($length, $content_type) returning id)");
auto st2 = conn.prepare(R"(insert into libpaths (path, libitem) values ($path, $libitem))");
//{ // uncomment this scope to make it work auto res = st1.execute<rowid>({{"length", 123}, {"content_type", "application/octet-stream"}});
rowid id = res.current();
st2.execute({{"path", "lib/path/to/file"}, {"libitem", id.id}}); //}
t.commit(); } ```
The requirement of this extra scope around the resultset was quite surprising to me. Nowhere in the library documentation is this behavior explained. I would much rather prefer the scope of the transaction itself sort out the order of destruction to make sure it commits correctly without extra nested scopes. If that's not possible, at least the library documentation needs to make the user aware of those subtleties.
Would it make sense to offer an alternative interface for the transactions in this library?
conn.transaction([&](sqlite::connection& conn) { auto st1 = conn.prepare(R"(insert into libitems (length, content_type) values ($length, $content_type) returning id)"); auto st2 = conn.prepare(R"(insert into libpaths (path, libitem) values ($path, $libitem))"); { auto res = st1.execute<rowid>({{"length", 123}, {"content_type", "application/octet-stream"}}); rowid id = res.current(); st2.execute({{"path", "lib/path/to/file"}, {"libitem", id.id}}); } });
This way, no explicit `commit` instruction is necessary: the implementation of member function transaction() can have it at the end of the scope. There is no way to forget the commit upon successful execution of the instructions. This would also correctly roll back a transaction upon any exception.
The explicit commit is on purpose. You never want to implicitly write to the database.

Am 07.09.25 um 22:57 schrieb Maximilian Riemensberger via Boost:
* Transaction scoping rules: The scoping rules around transations, statements and resultset are suprising. The following example demonstrates that:
``` { sqlite::transaction t{conn}; // use a transaction to speed this up
auto st1 = conn.prepare(R"(insert into libitems (length, content_type) values ($length, $content_type) returning id)");
auto st2 = conn.prepare(R"(insert into libpaths (path, libitem) values ($path, $libitem))");
//{ // uncomment this scope to make it work auto res = st1.execute<rowid>({{"length", 123}, {"content_type", "application/octet-stream"}});
rowid id = res.current();
st2.execute({{"path", "lib/path/to/file"}, {"libitem", id.id}}); //}
t.commit(); } ```
The requirement of this extra scope around the resultset was quite surprising to me. Nowhere in the library documentation is this behavior explained.
I fully agree: To the user this certainly is surprising and I don't see how this can be easily explained. Is there anything in the wrapper to make this usable in an intuitive way?
participants (17)
-
Alexander Grund
-
Andrzej Krzemienski
-
Christian Mazakas
-
Darryl Green
-
Dominique Devienne
-
Harold Bott
-
haroldjbott@gmail.com
-
Julien Blanc
-
Klemens Morgenstern
-
Maximilian Riemensberger
-
Mohammad Nejati
-
Mohammad Nejati [ashtum]
-
Mungo Gill
-
Peter Dimov
-
Ruben Perez
-
Scott Bailey
-
Vinnie Falco