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 diff --git a/include/SQLiteCpp/Column.h b/include/SQLiteCpp/Column.h index 64559a6c..2679232c 100644 --- a/include/SQLiteCpp/Column.h +++ b/include/SQLiteCpp/Column.h @@ -95,17 +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. * - * @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; /** @@ -221,13 +224,15 @@ class SQLITECPP_API Column return getDouble(); } /** - * @brief Inline cast operator to char* + * @brief Convert to a text pointer, returning nullptr for SQL NULL. * + * 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 { - 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..c876213e 100644 --- a/tests/Statement_test.cpp +++ b/tests/Statement_test.cpp @@ -1342,6 +1342,66 @@ TEST(Statement, getColumns) EXPECT_DOUBLE_EQ(0.0, testStruct2.real); } +TEST(Statement, getColumnsNullableText) +{ + struct NullableText + { + 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)")); + 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)); + 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()); + 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); + const auto emptyStringRow = query.getColumns(); + EXPECT_EQ("", emptyStringRow.text); + + 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); + const auto textStringRow = query.getColumns(); + EXPECT_EQ("hello", textStringRow.text); + + EXPECT_FALSE(query.executeStep()); +} + TEST(Statement, rowIteratorTraits) { using Iter = SQLite::Statement::RowIterator;