Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Add source table-name accessors to Statement and Column (#594)
- Add `std::string_view` binding and column access, with tests and documentation (#518) (#607)
- Add `Column::getOptional<T>()` to preserve SQL NULL in typed column values (#614)

## [3.4.0] - 2026-09-21

Expand Down
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,19 @@ until the parameter is rebound or the statement is finalized. Calling `reset()`
does not release that binding. The view returned by `getStringView()` also
borrows SQLite-owned memory; copy it if it must outlive the current row.

### Owning and borrowing column values

`Column::getString()` and `getOptional<std::string>()` return owning copies.
For non-NULL columns, `getStringView()`, `getText(nullptr)`, and
`getOptional<std::string_view>()` return non-owning views or pointers into
SQLite-managed buffers. These avoid copies, but the data may be invalidated by
column type conversions or by stepping, resetting, or finalizing the statement.

`std::optional<T>` only preserves SQL NULL; it does not make `T` owning.
This is similar to Rust's borrowed `&str` versus owned `String`, except C++
does not enforce borrowing lifetimes at compile time. Use an owning type when
the value must outlive the current SQLite result.

### The first sample demonstrates how to query a database and get results:

```C++
Expand Down
38 changes: 38 additions & 0 deletions include/SQLiteCpp/Column.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@

#include <string>
#include <memory>
#include <optional>
#include <string_view>
#include <type_traits>

// Forward declarations to avoid inclusion of <sqlite3.h> in a header
struct sqlite3_stmt;
Expand Down Expand Up @@ -268,6 +270,42 @@ class SQLITECPP_API Column
return getStringView();
}

/**
* @brief Return the column value as an optional, preserving SQL NULL.
*
* Uses the string accessors for std::string and std::string_view, and the existing Column
* conversion operators for other types. NULL is distinct from an empty string or zero.
*
* @tparam T Destination value type, such as int, double, std::string, or std::string_view.
* @return std::nullopt for SQL NULL; otherwise the converted value.
* @note std::optional indicates whether the value is NULL; it does not imply ownership.
* @note Check for NULL before any other type conversions of the same column.
* @warning If T is a pointer or view, the result borrows SQLite-owned memory and may be
* invalidated by column conversions or stepping, resetting, or finalizing the statement.
* Use an owning T (e.g. std::string) to retain the value independently.
* @note A zero-length BLOB may produce an engaged optional<const void*> containing nullptr.
*/
template<typename T>
std::optional<T> getOptional() const
{
if (isNull())
{
return std::nullopt;
}
if constexpr (std::is_same_v<T, std::string>)
{
return getString();
}
else if constexpr (std::is_same_v<T, std::string_view>)
{
return getStringView();
}
else
{
return static_cast<T>(*this);
}
}

private:
Statement::TStatementPtr mStmtPtr; ///< Shared Pointer to the prepared SQLite Statement Object
int mIndex; ///< Index of the column in the row of result, starting at 0
Expand Down
67 changes: 67 additions & 0 deletions tests/Column_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,73 @@
#include <stdint.h>


TEST(Column, getOptional)
{
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
ASSERT_EQ(0, db.exec("CREATE TABLE test (number INTEGER, amount REAL, text TEXT, data BLOB)"));
ASSERT_EQ(1, db.exec("INSERT INTO test VALUES (NULL, NULL, NULL, NULL)"));
ASSERT_EQ(1, db.exec("INSERT INTO test VALUES (0, 0.0, '', X'')"));
ASSERT_EQ(1, db.exec("INSERT INTO test VALUES (42, 1.5, 'A' || char(0) || 'B', X'410042')"));

// Read each SQLite result column through only one conversion path.
SQLite::Statement query(db,
"SELECT number, number, amount, text, text, data, data FROM test ORDER BY rowid");

ASSERT_TRUE(query.executeStep());
EXPECT_FALSE(query.getColumn(0).getOptional<int>().has_value());
EXPECT_FALSE(query.getColumn(1).getOptional<int64_t>().has_value());
EXPECT_FALSE(query.getColumn(2).getOptional<double>().has_value());
EXPECT_FALSE(query.getColumn(3).getOptional<std::string>().has_value());
EXPECT_FALSE(query.getColumn(4).getOptional<std::string_view>().has_value());
EXPECT_FALSE(query.getColumn(5).getOptional<std::string>().has_value());
EXPECT_FALSE(query.getColumn(6).getOptional<const void*>().has_value());

ASSERT_TRUE(query.executeStep());
const auto zero = query.getColumn(0).getOptional<int>();
ASSERT_TRUE(zero.has_value());
EXPECT_EQ(0, *zero);
const auto zeroReal = query.getColumn(2).getOptional<double>();
ASSERT_TRUE(zeroReal.has_value());
EXPECT_DOUBLE_EQ(0.0, *zeroReal);
const auto emptyText = query.getColumn(3).getOptional<std::string>();
ASSERT_TRUE(emptyText.has_value());
EXPECT_TRUE(emptyText->empty());
const auto emptyPointer = query.getColumn(4).getOptional<const char*>();
ASSERT_TRUE(emptyPointer.has_value());
ASSERT_NE(nullptr, *emptyPointer);
EXPECT_STREQ("", *emptyPointer);
const auto emptyBlob = query.getColumn(5).getOptional<std::string_view>();
ASSERT_TRUE(emptyBlob.has_value());
EXPECT_TRUE(emptyBlob->empty());
const auto emptyBlobPointer = query.getColumn(6).getOptional<const void*>();
ASSERT_TRUE(emptyBlobPointer.has_value());
EXPECT_EQ(nullptr, *emptyBlobPointer);

ASSERT_TRUE(query.executeStep());
const auto number = query.getColumn(1).getOptional<int64_t>();
ASSERT_TRUE(number.has_value());
EXPECT_EQ(42, *number);
const auto amount = query.getColumn(2).getOptional<double>();
ASSERT_TRUE(amount.has_value());
EXPECT_DOUBLE_EQ(1.5, *amount);
const std::string embedded({'A', char(0), 'B'});
const auto text = query.getColumn(3).getOptional<std::string>();
ASSERT_TRUE(text.has_value());
EXPECT_EQ(embedded, *text);
const auto textView = query.getColumn(4).getOptional<std::string_view>();
ASSERT_TRUE(textView.has_value());
EXPECT_EQ(std::string_view(embedded), *textView);
const auto blob = query.getColumn(5).getOptional<std::string>();
ASSERT_TRUE(blob.has_value());
EXPECT_EQ(embedded, *blob);
const auto blobView = query.getColumn(6).getOptional<std::string_view>();
ASSERT_TRUE(blobView.has_value());
EXPECT_EQ(std::string_view(embedded), *blobView);

EXPECT_FALSE(query.executeStep());
}


TEST(Column, typeConstantsAsCaseLabels)
{
SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
Expand Down
Loading