Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
3b394f9
fix(c/driver/sqlite): preserve double precision on string upcast
efegokdemir Oct 10, 2026
91e9362
fix(c/driver/sqlite): preserve double round-trip precision
efegokdemir Oct 10, 2026
5e757af
test(sqlite): compare precision against stored values
efegokdemir Oct 10, 2026
8df62c9
fix sqlite float formatting in comma locales
efegokdemir Oct 12, 2026
4d1e353
test(c/driver/sqlite): initialise locale test before locale change
efegokdemir Oct 12, 2026
2c18471
test(c/driver/sqlite): restore locale before changing it
efegokdemir Oct 12, 2026
a9768bb
fix(c/driver/sqlite): use SQLite float formatting
efegokdemir Oct 13, 2026
da04215
test(sqlite): compare inferred strings with SQLite output
efegokdemir Oct 14, 2026
f54a456
style(sqlite): wrap precision assertions
efegokdemir Oct 14, 2026
8baea8d
test(sqlite): use explicit REAL string expectations
efegokdemir Oct 14, 2026
9ed0d4f
test(sqlite): assert float string round trips
efegokdemir Oct 14, 2026
28186b9
docs(c/driver/sqlite): trim formatter comment
efegokdemir Oct 15, 2026
24a13cd
test(c/driver/sqlite): compare inferred REAL strings directly
efegokdemir Oct 21, 2026
01e8e53
test(c/driver/sqlite): assert floating-point round trips
efegokdemir Oct 21, 2026
c60b90f
test(c/driver/sqlite): compare REAL output with SQLite text
efegokdemir Oct 21, 2026
0ec9612
test(c/driver/sqlite): use SQLite REAL formatting reference
efegokdemir Oct 21, 2026
2a20140
test(c/driver/sqlite): simplify precision regression
efegokdemir Oct 22, 2026
0c039e1
test(sqlite): assert round-trip precision across SQLite versions
efegokdemir Oct 22, 2026
67ed93b
style: apply clang-format to SQLite precision test
efegokdemir Oct 22, 2026
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
85 changes: 83 additions & 2 deletions c/driver/sqlite/sqlite_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
// specific language governing permissions and limitations
// under the License.

#include <clocale>
#include <cstdlib>
#include <cstring>
#include <filesystem> // NOLINT [build/c++17]
#include <limits>
Expand Down Expand Up @@ -1013,9 +1015,88 @@ TEST_F(SqliteReaderTest, IntsFloatsStrs) {
ASSERT_EQ(NANOARROW_TYPE_STRING, reader.fields[0].type);

ASSERT_NO_FATAL_FAILURE(reader.Next());
ASSERT_NO_FATAL_FAILURE(CompareArray<std::string>(reader.array_view->children[0],
{"1.0", "1.0", "", "Inf", "-Inf"}));
}

TEST_F(SqliteReaderTest, InferFloatToStringPreservesPrecision) {
ASSERT_NO_FATAL_FAILURE(Exec("CREATE TABLE foo (upcast_col, append_col)"));
ASSERT_NO_FATAL_FAILURE(
Exec("INSERT INTO foo VALUES "
"(123456789.123, 'start'), "
"(3.141592653589793, 123456789.123), "
"(0.10000000000000002, 3.141592653589793), "
"('end', 0.10000000000000002), "
"(42, NULL), "
"(NULL, 7), "
"('original text', '')"));

adbc_validation::StreamReader reader;
ASSERT_NO_FATAL_FAILURE(Exec("SELECT * FROM foo", kInferRows, &reader));
ASSERT_EQ(NANOARROW_TYPE_STRING, reader.fields[0].type);
ASSERT_EQ(NANOARROW_TYPE_STRING, reader.fields[1].type);
ASSERT_NO_FATAL_FAILURE(reader.Next());
auto expect_strings = [](ArrowArrayView* actual,
const std::vector<std::optional<std::string>>& expected) {
ASSERT_EQ(actual->length, static_cast<int64_t>(expected.size()));
for (int64_t i = 0; i < actual->length; i++) {
if (!expected[i].has_value()) {
EXPECT_TRUE(ArrowArrayViewIsNull(actual, i));
continue;
}
ASSERT_FALSE(ArrowArrayViewIsNull(actual, i));
const ArrowStringView value = ArrowArrayViewGetStringUnsafe(actual, i);
EXPECT_EQ(std::string(value.data, value.size_bytes), expected[i].value());
}
};
ASSERT_NO_FATAL_FAILURE(
CompareArray<std::string>(reader.array_view->children[0],
{"1.000000e+00", "1.000000e+00", "", "inf", "-inf"}));
expect_strings(reader.array_view->children[0],
{"123456789.123", "3.1415926535897931", "0.10000000000000002", "end",
"42", std::nullopt, "original text"}));
ASSERT_NO_FATAL_FAILURE(expect_strings(reader.array_view->children[1],
{"start", "123456789.123", "3.1415926535897931",
"0.10000000000000002", std::nullopt, "7", ""}));
}

TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) {
ASSERT_NO_FATAL_FAILURE(Exec("CREATE TABLE foo (col)"));
ASSERT_NO_FATAL_FAILURE(Exec("INSERT INTO foo VALUES (1.5), ('text')"));
ASSERT_EQ(SQLITE_OK,
sqlite3_prepare_v2(db, "SELECT * FROM foo", -1, &stmt, /*pzTail=*/nullptr));

const char* current_locale = std::setlocale(LC_NUMERIC, nullptr);
ASSERT_NE(nullptr, current_locale);
const std::string original_locale(current_locale);

struct RestoreLocale {
std::string name;
~RestoreLocale() { std::setlocale(LC_NUMERIC, name.c_str()); }
} restore{original_locale};

const char* comma_locales[] = {"de_DE.UTF-8", "de_DE.utf8", "de_DE",
"German_Germany.1252"};
bool comma_locale_available = false;
for (const char* locale : comma_locales) {
if (std::setlocale(LC_NUMERIC, locale) != nullptr &&
std::strcmp(std::localeconv()->decimal_point, ",") == 0) {
comma_locale_available = true;
break;
}
}
if (!comma_locale_available) {
GTEST_SKIP() << "No decimal-comma locale is installed";
}

adbc_validation::StreamReader reader;
ASSERT_THAT(InternalAdbcSqliteExportReader(db, stmt, /*binder=*/nullptr, kInferRows,
&reader.stream.value, &error),
IsOkStatus(&error));
ASSERT_NO_FATAL_FAILURE(reader.GetSchema());
ASSERT_EQ(NANOARROW_TYPE_STRING, reader.fields[0].type);
ASSERT_NO_FATAL_FAILURE(reader.Next());
const ArrowStringView actual =
ArrowArrayViewGetStringUnsafe(reader.array_view->children[0], 0);
EXPECT_EQ(std::string_view(actual.data, actual.size_bytes), "1.5");
}

TEST_F(SqliteReaderTest, InferIntReadInt) {
Expand Down
29 changes: 8 additions & 21 deletions c/driver/sqlite/statement_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

#include <assert.h>
#include <inttypes.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <time.h>
Expand Down Expand Up @@ -953,27 +952,15 @@ AdbcStatusCode InternalSqliteStatementReaderAppendDoubleToBinary(
struct ArrowBuffer* offsets, struct ArrowBuffer* binary, double value,
int32_t* offset, struct AdbcError* error) {
static const size_t kReserve = 64;
size_t buffer_size = kReserve;
CHECK_NA(INTERNAL, ArrowBufferReserve(binary, buffer_size), error);
CHECK_NA(INTERNAL, ArrowBufferReserve(binary, kReserve), error);
char* output = (char*)(binary->data + binary->size_bytes);
int written = 0;
while (1) {
written = snprintf(output, buffer_size, "%e", value);
if (written < 0) {
InternalAdbcSetError(error, "Encoding error when upcasting double to string");
return ADBC_STATUS_INTERNAL;
} else if (((size_t)written) >= buffer_size) {
// Truncated, resize and try again
// Check for overflow - presumably this can never happen...?
if (UINT_MAX - buffer_size < buffer_size) {
InternalAdbcSetError(error, "Overflow when upcasting double to string");
return ADBC_STATUS_INTERNAL;
}
CHECK_NA(INTERNAL, ArrowBufferReserve(binary, buffer_size), error);
buffer_size += buffer_size;
continue;
}
break;
// SQLite's alternate-form-2 formatter is locale-independent, keeps enough
// significant digits to round-trip binary64.
const int written =
(int)strlen(sqlite3_snprintf((int)kReserve, output, "%!.17g", value));
if (written < 0 || (size_t)written >= kReserve) {
InternalAdbcSetError(error, "Encoding error when upcasting double to string");
return ADBC_STATUS_INTERNAL;
}
*offset += written;
binary->size_bytes += written;
Expand Down
Loading