From 9f52a2bd58a3f68a4df9e9039746d3a56470e9d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rombauts?= Date: Sat, 24 Oct 2026 14:33:42 +0200 Subject: [PATCH 1/3] Preserve SQL NULL in const char* conversions Return nullptr for implicit text-pointer conversions, including getColumns() mappings, while retaining getText()'s safe empty-string default. Test NULL, empty, and nonempty text values. --- include/SQLiteCpp/Column.h | 7 ++++-- tests/Statement_test.cpp | 44 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/include/SQLiteCpp/Column.h b/include/SQLiteCpp/Column.h index 64559a6c..87103e6f 100644 --- a/include/SQLiteCpp/Column.h +++ b/include/SQLiteCpp/Column.h @@ -97,6 +97,7 @@ class SQLITECPP_API Column /** * @brief Return a pointer to the text value (NULL terminated string) of the column. * + * @param[in] apDefaultValue Value to return for SQL NULL (empty string by default, or nullptr if requested). * @warning The value pointed at is only valid while the statement is valid (ie. not finalized), * thus you must copy it before using it beyond its scope (to a std::string for instance). */ @@ -221,13 +222,15 @@ class SQLITECPP_API Column return getDouble(); } /** - * @brief Inline cast operator to char* + * @brief Convert to a text pointer, preserving SQL NULL as nullptr. * + * Unlike getText() without arguments, this does not replace NULL with an empty string. + * @warning The returned pointer is only valid while the statement data is valid. * @see getText */ operator const char*() const { - return getText(); + return getText(nullptr); } /** * @brief Inline cast operator to void* diff --git a/tests/Statement_test.cpp b/tests/Statement_test.cpp index 836c0922..779592ef 100644 --- a/tests/Statement_test.cpp +++ b/tests/Statement_test.cpp @@ -1342,6 +1342,50 @@ TEST(Statement, getColumns) EXPECT_DOUBLE_EQ(0.0, testStruct2.real); } +TEST(Statement, getColumnsNullableText) +{ + struct NullableText + { + const char* text; + }; + + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + ASSERT_EQ(0, db.exec("CREATE TABLE test (value TEXT)")); + ASSERT_EQ(1, db.exec("INSERT INTO test VALUES (NULL)")); + ASSERT_EQ(1, db.exec("INSERT INTO test VALUES ('')")); + ASSERT_EQ(1, db.exec("INSERT INTO test VALUES ('hello')")); + + SQLite::Statement query(db, "SELECT value FROM test ORDER BY rowid"); + + ASSERT_TRUE(query.executeStep()); + EXPECT_TRUE(query.getColumn(0).isNull()); + const auto nullRow = query.getColumns(); + EXPECT_EQ(nullptr, nullRow.text); + const char* nullText = query.getColumn(0); + EXPECT_EQ(nullptr, nullText); + EXPECT_STREQ("", query.getColumn(0).getText()); // Keep the safe default. + EXPECT_EQ(nullptr, query.getColumn(0).getText(nullptr)); + + ASSERT_TRUE(query.executeStep()); + EXPECT_FALSE(query.getColumn(0).isNull()); + const auto emptyRow = query.getColumns(); + ASSERT_NE(nullptr, emptyRow.text); + EXPECT_STREQ("", emptyRow.text); + const char* emptyText = query.getColumn(0); + ASSERT_NE(nullptr, emptyText); + EXPECT_STREQ("", emptyText); + + ASSERT_TRUE(query.executeStep()); + const auto textRow = query.getColumns(); + ASSERT_NE(nullptr, textRow.text); + EXPECT_STREQ("hello", textRow.text); + const char* text = query.getColumn(0); + ASSERT_NE(nullptr, text); + EXPECT_STREQ("hello", text); + + EXPECT_FALSE(query.executeStep()); +} + TEST(Statement, rowIteratorTraits) { using Iter = SQLite::Statement::RowIterator; From 750dbcefc54376a9024b082932dfbd40b2b288b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rombauts?= Date: Sat, 24 Oct 2026 16:32:01 +0200 Subject: [PATCH 2/3] Test nullable text and document pointer lifetime Cover string and string-view conversions alongside nullable text pointers. Clarify invalidation and allocation-failure behavior. --- include/SQLiteCpp/Column.h | 20 +++++++++++--------- tests/Statement_test.cpp | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/include/SQLiteCpp/Column.h b/include/SQLiteCpp/Column.h index 87103e6f..2679232c 100644 --- a/include/SQLiteCpp/Column.h +++ b/include/SQLiteCpp/Column.h @@ -95,18 +95,20 @@ class SQLITECPP_API Column /// Return the double (64bits float) value of the column double getDouble() const noexcept; /** - * @brief Return a pointer to the text value (NULL terminated string) of the column. + * @brief Return a pointer to the UTF-8 text value (NULL-terminated string) of the column. * - * @param[in] apDefaultValue Value to return for SQL NULL (empty string by default, or nullptr if requested). - * @warning The value pointed at is only valid while the statement is valid (ie. not finalized), - * thus you must copy it before using it beyond its scope (to a std::string for instance). + * @param[in] apDefaultValue Value returned if SQLite returns nullptr (empty string by default). + * @return Text pointer, or apDefaultValue if SQLite provides no text pointer. + * @note SQLite can return nullptr for SQL NULL or an out-of-memory conversion error. + * @warning Type conversions or stepping, resetting, or finalizing the statement may invalidate the + * returned pointer. Copy the text if it must be retained. */ const char* getText(const char* apDefaultValue = "") const noexcept; /** * @brief Return a pointer to the binary blob value of the column. * - * @warning The value pointed at is only valid while the statement is valid (ie. not finalized), - * thus you must copy it before using it beyond its scope (to a std::string for instance). + * @warning Type conversions or stepping, resetting, or finalizing the statement may invalidate the + * returned pointer. Copy the data if it must be retained. */ const void* getBlob() const noexcept; /** @@ -222,10 +224,10 @@ class SQLITECPP_API Column return getDouble(); } /** - * @brief Convert to a text pointer, preserving SQL NULL as nullptr. + * @brief Convert to a text pointer, returning nullptr for SQL NULL. * - * Unlike getText() without arguments, this does not replace NULL with an empty string. - * @warning The returned pointer is only valid while the statement data is valid. + * Unlike getText() without arguments, this does not replace a null pointer with an empty string. + * @warning Type conversions or stepping, resetting, or finalizing the statement may invalidate the pointer. * @see getText */ operator const char*() const diff --git a/tests/Statement_test.cpp b/tests/Statement_test.cpp index 779592ef..c876213e 100644 --- a/tests/Statement_test.cpp +++ b/tests/Statement_test.cpp @@ -1349,6 +1349,11 @@ TEST(Statement, getColumnsNullableText) const char* text; }; + struct StringText + { + std::string text; + }; + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); ASSERT_EQ(0, db.exec("CREATE TABLE test (value TEXT)")); ASSERT_EQ(1, db.exec("INSERT INTO test VALUES (NULL)")); @@ -1365,6 +1370,13 @@ TEST(Statement, getColumnsNullableText) EXPECT_EQ(nullptr, nullText); EXPECT_STREQ("", query.getColumn(0).getText()); // Keep the safe default. EXPECT_EQ(nullptr, query.getColumn(0).getText(nullptr)); + EXPECT_STREQ("[null]", query.getColumn(0).getText("[null]")); + const std::string nullString = query.getColumn(0); + EXPECT_TRUE(nullString.empty()); + const std::string_view nullView = query.getColumn(0); + EXPECT_TRUE(nullView.empty()); + const auto nullStringRow = query.getColumns(); + EXPECT_TRUE(nullStringRow.text.empty()); ASSERT_TRUE(query.executeStep()); EXPECT_FALSE(query.getColumn(0).isNull()); @@ -1374,6 +1386,8 @@ TEST(Statement, getColumnsNullableText) const char* emptyText = query.getColumn(0); ASSERT_NE(nullptr, emptyText); EXPECT_STREQ("", emptyText); + const auto emptyStringRow = query.getColumns(); + EXPECT_EQ("", emptyStringRow.text); ASSERT_TRUE(query.executeStep()); const auto textRow = query.getColumns(); @@ -1382,6 +1396,8 @@ TEST(Statement, getColumnsNullableText) const char* text = query.getColumn(0); ASSERT_NE(nullptr, text); EXPECT_STREQ("hello", text); + const auto textStringRow = query.getColumns(); + EXPECT_EQ("hello", textStringRow.text); EXPECT_FALSE(query.executeStep()); } From be385a06c6d5c2b219bb175ad16dfbe7d967753d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Rombauts?= Date: Sat, 24 Oct 2026 16:32:04 +0200 Subject: [PATCH 3/3] docs: clarify the nullable text compatibility change Point users relying on the old implicit empty-string fallback to getText(), which retains that behavior. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 655a1d6e..fd30b507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix link errors in supplied assertion handlers and add handler-enabled CI coverage (#587) - Make column type constants usable in constant expressions (ABI change) (#609) - Restrict CMake package compatibility to the same major version (#610) +- Preserve SQL NULL as nullptr in implicit `const char*` conversions; use `getText()` for the old fallback (#612) ### Removed