From 3b394f9c13569ccc6e3f81e685ad81b91abc2683 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Sat, 10 Oct 2026 11:44:38 +0300 Subject: [PATCH 01/19] fix(c/driver/sqlite): preserve double precision on string upcast Closes #4862 --- c/driver/sqlite/sqlite_test.cc | 43 ++++++++++++++++++++++++++++-- c/driver/sqlite/statement_reader.c | 25 +++-------------- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 22f7c8afe3..e87c0d2ded 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -1013,9 +1013,48 @@ TEST_F(SqliteReaderTest, IntsFloatsStrs) { ASSERT_EQ(NANOARROW_TYPE_STRING, reader.fields[0].type); ASSERT_NO_FATAL_FAILURE(reader.Next()); + ASSERT_NO_FATAL_FAILURE(CompareArray(reader.array_view->children[0], + {"1.0", "1.0", "", "Inf", "-Inf"})); +} + +TEST_F(SqliteReaderTest, InferFloatToStringPreservesSqliteText) { + 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.1, 3.141592653589793), " + "('end', 0.1)")); + + std::vector> expected_upcast; + std::vector> expected_append; + sqlite3_stmt* expected_stmt = nullptr; + ASSERT_EQ(SQLITE_OK, sqlite3_prepare_v2( + db, + "SELECT CAST(upcast_col AS TEXT), CAST(append_col AS TEXT) " + "FROM foo", + -1, &expected_stmt, nullptr)); + int rc = SQLITE_OK; + while ((rc = sqlite3_step(expected_stmt)) == SQLITE_ROW) { + expected_upcast.emplace_back( + std::string(reinterpret_cast(sqlite3_column_text(expected_stmt, 0)), + sqlite3_column_bytes(expected_stmt, 0))); + expected_append.emplace_back( + std::string(reinterpret_cast(sqlite3_column_text(expected_stmt, 1)), + sqlite3_column_bytes(expected_stmt, 1))); + } + EXPECT_EQ(SQLITE_DONE, rc); + sqlite3_finalize(expected_stmt); + + 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()); + ASSERT_NO_FATAL_FAILURE( + CompareArray(reader.array_view->children[0], expected_upcast)); ASSERT_NO_FATAL_FAILURE( - CompareArray(reader.array_view->children[0], - {"1.000000e+00", "1.000000e+00", "", "inf", "-inf"})); + CompareArray(reader.array_view->children[1], expected_append)); } TEST_F(SqliteReaderTest, InferIntReadInt) { diff --git a/c/driver/sqlite/statement_reader.c b/c/driver/sqlite/statement_reader.c index 0827a860ab..aca82ba511 100644 --- a/c/driver/sqlite/statement_reader.c +++ b/c/driver/sqlite/statement_reader.c @@ -953,28 +953,11 @@ 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; - } + // Match SQLite's own text conversion and retain enough precision to round-trip a + // double. + int written = (int)strlen(sqlite3_snprintf((int)kReserve, output, "%!.17g", value)); *offset += written; binary->size_bytes += written; ArrowBufferAppendUnsafe(offsets, offset, sizeof(int32_t)); From 91e9362972bbb92605d30b7fda2c30cd90ab96b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Sat, 10 Oct 2026 17:25:48 +0300 Subject: [PATCH 02/19] fix(c/driver/sqlite): preserve double round-trip precision --- c/driver/sqlite/sqlite_test.cc | 28 +++++++++++++++++++++++++--- c/driver/sqlite/statement_reader.c | 26 +++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 6 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index e87c0d2ded..c6855a10bb 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +#include #include #include // NOLINT [build/c++17] #include @@ -1017,7 +1018,7 @@ TEST_F(SqliteReaderTest, IntsFloatsStrs) { {"1.0", "1.0", "", "Inf", "-Inf"})); } -TEST_F(SqliteReaderTest, InferFloatToStringPreservesSqliteText) { +TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { ASSERT_NO_FATAL_FAILURE(Exec("CREATE TABLE foo (upcast_col, append_col)")); ASSERT_NO_FATAL_FAILURE( Exec("INSERT INTO foo VALUES " @@ -1051,10 +1052,31 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesSqliteText) { 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_round_trip = [](ArrowArrayView* actual, + const std::vector>& expected) { + ASSERT_EQ(actual->length, static_cast(expected.size())); + for (int64_t i = 0; i < actual->length; i++) { + ASSERT_TRUE(expected[i].has_value()); + const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); + const std::string actual_value(actual_view.data, actual_view.size_bytes); + + char* expected_end = nullptr; + const double expected_number = std::strtod(expected[i]->c_str(), &expected_end); + if (expected_end != expected[i]->c_str() && *expected_end == '\0') { + char* actual_end = nullptr; + const double actual_number = std::strtod(actual_value.c_str(), &actual_end); + ASSERT_NE(actual_end, actual_value.c_str()); + ASSERT_EQ(*actual_end, '\0'); + EXPECT_EQ(actual_number, expected_number); + } else { + EXPECT_EQ(actual_value, *expected[i]); + } + } + }; ASSERT_NO_FATAL_FAILURE( - CompareArray(reader.array_view->children[0], expected_upcast)); + expect_round_trip(reader.array_view->children[0], expected_upcast)); ASSERT_NO_FATAL_FAILURE( - CompareArray(reader.array_view->children[1], expected_append)); + expect_round_trip(reader.array_view->children[1], expected_append)); } TEST_F(SqliteReaderTest, InferIntReadInt) { diff --git a/c/driver/sqlite/statement_reader.c b/c/driver/sqlite/statement_reader.c index aca82ba511..a840aea0d8 100644 --- a/c/driver/sqlite/statement_reader.c +++ b/c/driver/sqlite/statement_reader.c @@ -955,9 +955,29 @@ AdbcStatusCode InternalSqliteStatementReaderAppendDoubleToBinary( static const size_t kReserve = 64; CHECK_NA(INTERNAL, ArrowBufferReserve(binary, kReserve), error); char* output = (char*)(binary->data + binary->size_bytes); - // Match SQLite's own text conversion and retain enough precision to round-trip a - // double. - int written = (int)strlen(sqlite3_snprintf((int)kReserve, output, "%!.17g", value)); + // Keep enough significant digits to round-trip the binary64 value. SQLite's + // printf precision can be capped by the linked SQLite build, so use libc for + // finite values and SQLite's formatter for its non-finite spellings. + int written; + if (isfinite(value)) { + written = snprintf(output, (int)kReserve, "%.17g", value); + if (written >= 0 && written < (int)kReserve - 2 && strchr(output, '.') == NULL) { + char* exponent = strchr(output, 'e'); + const size_t insert_at = + exponent == NULL ? (size_t)written : (size_t)(exponent - output); + memmove(output + insert_at + 2, output + insert_at, + (size_t)written - insert_at + 1); + output[insert_at] = '.'; + output[insert_at + 1] = '0'; + written += 2; + } + } else { + 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; ArrowBufferAppendUnsafe(offsets, offset, sizeof(int32_t)); From 5e757af08c34f4baaf3ab23242b3c5a5027130cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Sat, 10 Oct 2026 20:50:46 +0300 Subject: [PATCH 03/19] test(sqlite): compare precision against stored values --- c/driver/sqlite/sqlite_test.cc | 42 ++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index c6855a10bb..51a3b8aa0d 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -1027,22 +1027,33 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { "(0.1, 3.141592653589793), " "('end', 0.1)")); - std::vector> expected_upcast; - std::vector> expected_append; + struct ExpectedValue { + std::optional number; + std::string text; + }; + std::vector expected_upcast; + std::vector expected_append; sqlite3_stmt* expected_stmt = nullptr; ASSERT_EQ(SQLITE_OK, sqlite3_prepare_v2( db, - "SELECT CAST(upcast_col AS TEXT), CAST(append_col AS TEXT) " - "FROM foo", + "SELECT upcast_col, append_col FROM foo", -1, &expected_stmt, nullptr)); int rc = SQLITE_OK; while ((rc = sqlite3_step(expected_stmt)) == SQLITE_ROW) { - expected_upcast.emplace_back( - std::string(reinterpret_cast(sqlite3_column_text(expected_stmt, 0)), - sqlite3_column_bytes(expected_stmt, 0))); - expected_append.emplace_back( - std::string(reinterpret_cast(sqlite3_column_text(expected_stmt, 1)), - sqlite3_column_bytes(expected_stmt, 1))); + auto read_expected = [expected_stmt](int column) { + ExpectedValue value; + const int type = sqlite3_column_type(expected_stmt, column); + if (type == SQLITE_INTEGER || type == SQLITE_FLOAT) { + value.number = sqlite3_column_double(expected_stmt, column); + } else { + value.text = std::string( + reinterpret_cast(sqlite3_column_text(expected_stmt, column)), + sqlite3_column_bytes(expected_stmt, column)); + } + return value; + }; + expected_upcast.emplace_back(read_expected(0)); + expected_append.emplace_back(read_expected(1)); } EXPECT_EQ(SQLITE_DONE, rc); sqlite3_finalize(expected_stmt); @@ -1053,23 +1064,20 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { ASSERT_EQ(NANOARROW_TYPE_STRING, reader.fields[1].type); ASSERT_NO_FATAL_FAILURE(reader.Next()); auto expect_round_trip = [](ArrowArrayView* actual, - const std::vector>& expected) { + const std::vector& expected) { ASSERT_EQ(actual->length, static_cast(expected.size())); for (int64_t i = 0; i < actual->length; i++) { - ASSERT_TRUE(expected[i].has_value()); const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); const std::string actual_value(actual_view.data, actual_view.size_bytes); - char* expected_end = nullptr; - const double expected_number = std::strtod(expected[i]->c_str(), &expected_end); - if (expected_end != expected[i]->c_str() && *expected_end == '\0') { + if (expected[i].number.has_value()) { char* actual_end = nullptr; const double actual_number = std::strtod(actual_value.c_str(), &actual_end); ASSERT_NE(actual_end, actual_value.c_str()); ASSERT_EQ(*actual_end, '\0'); - EXPECT_EQ(actual_number, expected_number); + EXPECT_EQ(actual_number, *expected[i].number); } else { - EXPECT_EQ(actual_value, *expected[i]); + EXPECT_EQ(actual_value, expected[i].text); } } }; From 8df62c93260d8c44749c018ec5d31858bfc3a31b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Mon, 12 Oct 2026 09:27:05 +0300 Subject: [PATCH 04/19] fix sqlite float formatting in comma locales --- c/driver/sqlite/sqlite_test.cc | 41 +++++++++++++++++++++++++++--- c/driver/sqlite/statement_reader.c | 16 ++++++++++++ 2 files changed, 53 insertions(+), 4 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 51a3b8aa0d..577ac9b5b2 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +#include #include #include #include // NOLINT [build/c++17] @@ -1034,10 +1035,8 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { std::vector expected_upcast; std::vector expected_append; sqlite3_stmt* expected_stmt = nullptr; - ASSERT_EQ(SQLITE_OK, sqlite3_prepare_v2( - db, - "SELECT upcast_col, append_col FROM foo", - -1, &expected_stmt, nullptr)); + ASSERT_EQ(SQLITE_OK, sqlite3_prepare_v2(db, "SELECT upcast_col, append_col FROM foo", + -1, &expected_stmt, nullptr)); int rc = SQLITE_OK; while ((rc = sqlite3_step(expected_stmt)) == SQLITE_ROW) { auto read_expected = [expected_stmt](int column) { @@ -1087,6 +1086,40 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { expect_round_trip(reader.array_view->children[1], expected_append)); } +TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { + const char* current_locale = std::setlocale(LC_NUMERIC, nullptr); + ASSERT_NE(nullptr, current_locale); + const std::string original_locale(current_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) { + std::setlocale(LC_NUMERIC, original_locale.c_str()); + GTEST_SKIP() << "No decimal-comma locale is installed"; + } + + struct RestoreLocale { + std::string name; + ~RestoreLocale() { std::setlocale(LC_NUMERIC, name.c_str()); } + } restore{original_locale}; + + adbc_validation::StreamReader reader; + ASSERT_NO_FATAL_FAILURE(ExecSelect("(1.5), ('text')", kInferRows, &reader)); + 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) { adbc_validation::StreamReader reader; ASSERT_NO_FATAL_FAILURE( diff --git a/c/driver/sqlite/statement_reader.c b/c/driver/sqlite/statement_reader.c index a840aea0d8..ae1039952e 100644 --- a/c/driver/sqlite/statement_reader.c +++ b/c/driver/sqlite/statement_reader.c @@ -23,6 +23,7 @@ #include #include +#include #include #include #include @@ -961,6 +962,21 @@ AdbcStatusCode InternalSqliteStatementReaderAppendDoubleToBinary( int written; if (isfinite(value)) { written = snprintf(output, (int)kReserve, "%.17g", value); + if (written > 0 && written < (int)kReserve) { + const char* decimal_point = localeconv()->decimal_point; + if (decimal_point != NULL && decimal_point[0] != '\0' && + strcmp(decimal_point, ".") != 0) { + char* separator = strstr(output, decimal_point); + if (separator != NULL) { + size_t separator_length = strlen(decimal_point); + size_t separator_offset = (size_t)(separator - output); + size_t suffix_length = (size_t)written - separator_offset - separator_length; + memmove(separator + 1, separator + separator_length, suffix_length + 1); + *separator = '.'; + written -= (int)(separator_length - 1); + } + } + } if (written >= 0 && written < (int)kReserve - 2 && strchr(output, '.') == NULL) { char* exponent = strchr(output, 'e'); const size_t insert_at = From 4d1e3532717fd244c9c7153d84a26c52bebc05b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Mon, 12 Oct 2026 10:55:26 +0300 Subject: [PATCH 05/19] test(c/driver/sqlite): initialise locale test before locale change --- c/driver/sqlite/sqlite_test.cc | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 577ac9b5b2..180c62af4f 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -1087,6 +1087,11 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { } 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); @@ -1112,7 +1117,10 @@ TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { } restore{original_locale}; adbc_validation::StreamReader reader; - ASSERT_NO_FATAL_FAILURE(ExecSelect("(1.5), ('text')", kInferRows, &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 = From 2c18471fd3632ba5e422f4fe1502103664494cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Mon, 12 Oct 2026 18:53:53 +0300 Subject: [PATCH 06/19] test(c/driver/sqlite): restore locale before changing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Efe Gökdemir --- c/driver/sqlite/sqlite_test.cc | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 180c62af4f..f7a151fd72 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -1096,6 +1096,11 @@ TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { 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; @@ -1107,15 +1112,9 @@ TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { } } if (!comma_locale_available) { - std::setlocale(LC_NUMERIC, original_locale.c_str()); GTEST_SKIP() << "No decimal-comma locale is installed"; } - struct RestoreLocale { - std::string name; - ~RestoreLocale() { std::setlocale(LC_NUMERIC, name.c_str()); } - } restore{original_locale}; - adbc_validation::StreamReader reader; ASSERT_THAT(InternalAdbcSqliteExportReader(db, stmt, /*binder=*/nullptr, kInferRows, &reader.stream.value, &error), From a9768bbb79cc19cc85091de1c3ede535c31fdcc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Tue, 13 Oct 2026 09:55:33 +0300 Subject: [PATCH 07/19] fix(c/driver/sqlite): use SQLite float formatting --- c/driver/sqlite/statement_reader.c | 41 ++++-------------------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/c/driver/sqlite/statement_reader.c b/c/driver/sqlite/statement_reader.c index ae1039952e..fbd5a17748 100644 --- a/c/driver/sqlite/statement_reader.c +++ b/c/driver/sqlite/statement_reader.c @@ -23,8 +23,6 @@ #include #include -#include -#include #include #include #include @@ -956,40 +954,11 @@ AdbcStatusCode InternalSqliteStatementReaderAppendDoubleToBinary( static const size_t kReserve = 64; CHECK_NA(INTERNAL, ArrowBufferReserve(binary, kReserve), error); char* output = (char*)(binary->data + binary->size_bytes); - // Keep enough significant digits to round-trip the binary64 value. SQLite's - // printf precision can be capped by the linked SQLite build, so use libc for - // finite values and SQLite's formatter for its non-finite spellings. - int written; - if (isfinite(value)) { - written = snprintf(output, (int)kReserve, "%.17g", value); - if (written > 0 && written < (int)kReserve) { - const char* decimal_point = localeconv()->decimal_point; - if (decimal_point != NULL && decimal_point[0] != '\0' && - strcmp(decimal_point, ".") != 0) { - char* separator = strstr(output, decimal_point); - if (separator != NULL) { - size_t separator_length = strlen(decimal_point); - size_t separator_offset = (size_t)(separator - output); - size_t suffix_length = (size_t)written - separator_offset - separator_length; - memmove(separator + 1, separator + separator_length, suffix_length + 1); - *separator = '.'; - written -= (int)(separator_length - 1); - } - } - } - if (written >= 0 && written < (int)kReserve - 2 && strchr(output, '.') == NULL) { - char* exponent = strchr(output, 'e'); - const size_t insert_at = - exponent == NULL ? (size_t)written : (size_t)(exponent - output); - memmove(output + insert_at + 2, output + insert_at, - (size_t)written - insert_at + 1); - output[insert_at] = '.'; - output[insert_at + 1] = '0'; - written += 2; - } - } else { - written = (int)strlen(sqlite3_snprintf((int)kReserve, output, "%!.17g", value)); - } + // SQLite's alternate-form-2 formatter is locale-independent, keeps enough + // significant digits to round-trip binary64, and preserves REAL values as + // SQL-compatible text (including a decimal point for integral values). + 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; From da042157955987f5975fead42fac4756342e3f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Wed, 14 Oct 2026 06:41:47 +0300 Subject: [PATCH 08/19] test(sqlite): compare inferred strings with SQLite output --- c/driver/sqlite/sqlite_test.cc | 41 +++++++++------------------------- 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index f7a151fd72..ddb1814032 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -1028,28 +1028,17 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { "(0.1, 3.141592653589793), " "('end', 0.1)")); - struct ExpectedValue { - std::optional number; - std::string text; - }; - std::vector expected_upcast; - std::vector expected_append; + std::vector expected_upcast; + std::vector expected_append; sqlite3_stmt* expected_stmt = nullptr; ASSERT_EQ(SQLITE_OK, sqlite3_prepare_v2(db, "SELECT upcast_col, append_col FROM foo", -1, &expected_stmt, nullptr)); int rc = SQLITE_OK; while ((rc = sqlite3_step(expected_stmt)) == SQLITE_ROW) { auto read_expected = [expected_stmt](int column) { - ExpectedValue value; - const int type = sqlite3_column_type(expected_stmt, column); - if (type == SQLITE_INTEGER || type == SQLITE_FLOAT) { - value.number = sqlite3_column_double(expected_stmt, column); - } else { - value.text = std::string( - reinterpret_cast(sqlite3_column_text(expected_stmt, column)), - sqlite3_column_bytes(expected_stmt, column)); - } - return value; + const auto* text = sqlite3_column_text(expected_stmt, column); + return std::string(reinterpret_cast(text), + sqlite3_column_bytes(expected_stmt, column)); }; expected_upcast.emplace_back(read_expected(0)); expected_append.emplace_back(read_expected(1)); @@ -1062,28 +1051,18 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { 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_round_trip = [](ArrowArrayView* actual, - const std::vector& expected) { + auto expect_sqlite_text = [](ArrowArrayView* actual, + const std::vector& expected) { ASSERT_EQ(actual->length, static_cast(expected.size())); for (int64_t i = 0; i < actual->length; i++) { const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); const std::string actual_value(actual_view.data, actual_view.size_bytes); - if (expected[i].number.has_value()) { - char* actual_end = nullptr; - const double actual_number = std::strtod(actual_value.c_str(), &actual_end); - ASSERT_NE(actual_end, actual_value.c_str()); - ASSERT_EQ(*actual_end, '\0'); - EXPECT_EQ(actual_number, *expected[i].number); - } else { - EXPECT_EQ(actual_value, expected[i].text); - } + EXPECT_EQ(actual_value, expected[i]); } }; - ASSERT_NO_FATAL_FAILURE( - expect_round_trip(reader.array_view->children[0], expected_upcast)); - ASSERT_NO_FATAL_FAILURE( - expect_round_trip(reader.array_view->children[1], expected_append)); + ASSERT_NO_FATAL_FAILURE(expect_sqlite_text(reader.array_view->children[0], expected_upcast)); + ASSERT_NO_FATAL_FAILURE(expect_sqlite_text(reader.array_view->children[1], expected_append)); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From f54a456bbd74b45b98ed7953cab5840fbf85ad44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Wed, 14 Oct 2026 06:47:52 +0300 Subject: [PATCH 09/19] style(sqlite): wrap precision assertions --- c/driver/sqlite/sqlite_test.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index ddb1814032..e1b8492d74 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -1061,8 +1061,10 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { EXPECT_EQ(actual_value, expected[i]); } }; - ASSERT_NO_FATAL_FAILURE(expect_sqlite_text(reader.array_view->children[0], expected_upcast)); - ASSERT_NO_FATAL_FAILURE(expect_sqlite_text(reader.array_view->children[1], expected_append)); + ASSERT_NO_FATAL_FAILURE( + expect_sqlite_text(reader.array_view->children[0], expected_upcast)); + ASSERT_NO_FATAL_FAILURE( + expect_sqlite_text(reader.array_view->children[1], expected_append)); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From 8baea8d34ccd524306147150ef16e8c6cf5196b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Wed, 14 Oct 2026 07:59:27 +0300 Subject: [PATCH 10/19] test(sqlite): use explicit REAL string expectations --- c/driver/sqlite/sqlite_test.cc | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index e1b8492d74..5fedc0e728 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -1028,43 +1028,27 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { "(0.1, 3.141592653589793), " "('end', 0.1)")); - std::vector expected_upcast; - std::vector expected_append; - sqlite3_stmt* expected_stmt = nullptr; - ASSERT_EQ(SQLITE_OK, sqlite3_prepare_v2(db, "SELECT upcast_col, append_col FROM foo", - -1, &expected_stmt, nullptr)); - int rc = SQLITE_OK; - while ((rc = sqlite3_step(expected_stmt)) == SQLITE_ROW) { - auto read_expected = [expected_stmt](int column) { - const auto* text = sqlite3_column_text(expected_stmt, column); - return std::string(reinterpret_cast(text), - sqlite3_column_bytes(expected_stmt, column)); - }; - expected_upcast.emplace_back(read_expected(0)); - expected_append.emplace_back(read_expected(1)); - } - EXPECT_EQ(SQLITE_DONE, rc); - sqlite3_finalize(expected_stmt); + const std::vector expected_upcast = {"123456789.123", "3.1415926535897931", + "0.1", "end"}; + const std::vector expected_append = {"start", "123456789.123", + "3.1415926535897931", "0.1"}; 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_sqlite_text = [](ArrowArrayView* actual, - const std::vector& expected) { + auto expect_string = [](ArrowArrayView* actual, + const std::vector& expected) { ASSERT_EQ(actual->length, static_cast(expected.size())); for (int64_t i = 0; i < actual->length; i++) { const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); const std::string actual_value(actual_view.data, actual_view.size_bytes); - EXPECT_EQ(actual_value, expected[i]); } }; - ASSERT_NO_FATAL_FAILURE( - expect_sqlite_text(reader.array_view->children[0], expected_upcast)); - ASSERT_NO_FATAL_FAILURE( - expect_sqlite_text(reader.array_view->children[1], expected_append)); + ASSERT_NO_FATAL_FAILURE(expect_string(reader.array_view->children[0], expected_upcast)); + ASSERT_NO_FATAL_FAILURE(expect_string(reader.array_view->children[1], expected_append)); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From 9ed0d4fc87ab9bd816bb0b37e1eee987e9b9c002 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Wed, 14 Oct 2026 08:22:29 +0300 Subject: [PATCH 11/19] test(sqlite): assert float string round trips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Efe Gökdemir --- c/driver/sqlite/sqlite_test.cc | 38 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 5fedc0e728..2f9752a07c 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +#include #include #include #include @@ -1028,27 +1029,44 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { "(0.1, 3.141592653589793), " "('end', 0.1)")); - const std::vector expected_upcast = {"123456789.123", "3.1415926535897931", - "0.1", "end"}; - const std::vector expected_append = {"start", "123456789.123", - "3.1415926535897931", "0.1"}; + const std::vector> expected_upcast = { + 123456789.123, 3.141592653589793, 0.1, std::nullopt}; + const std::vector> expected_append = {std::nullopt, 123456789.123, + 3.141592653589793, 0.1}; + const std::vector expected_text_upcast = {"", "", "", "end"}; + const std::vector expected_text_append = {"start", "", "", ""}; 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_string = [](ArrowArrayView* actual, - const std::vector& expected) { - ASSERT_EQ(actual->length, static_cast(expected.size())); + auto expect_round_trip = [](ArrowArrayView* actual, + const std::vector>& expected_values, + const std::vector& expected_text) { + ASSERT_EQ(actual->length, static_cast(expected_values.size())); + ASSERT_EQ(expected_values.size(), expected_text.size()); for (int64_t i = 0; i < actual->length; i++) { const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); const std::string actual_value(actual_view.data, actual_view.size_bytes); - EXPECT_EQ(actual_value, expected[i]); + if (expected_values[i].has_value()) { + double parsed = 0.0; + const char* begin = actual_value.data(); + const char* end = begin + actual_value.size(); + const auto result = + std::from_chars(begin, end, parsed, std::chars_format::general); + ASSERT_EQ(result.ec, std::errc{}); + ASSERT_EQ(result.ptr, end); + EXPECT_EQ(parsed, expected_values[i].value()); + } else { + EXPECT_EQ(actual_value, expected_text[i]); + } } }; - ASSERT_NO_FATAL_FAILURE(expect_string(reader.array_view->children[0], expected_upcast)); - ASSERT_NO_FATAL_FAILURE(expect_string(reader.array_view->children[1], expected_append)); + ASSERT_NO_FATAL_FAILURE(expect_round_trip(reader.array_view->children[0], + expected_upcast, expected_text_upcast)); + ASSERT_NO_FATAL_FAILURE(expect_round_trip(reader.array_view->children[1], + expected_append, expected_text_append)); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From 28186b96ded457086dbcf3c24a6cf0842f7191a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Thu, 15 Oct 2026 05:40:57 +0300 Subject: [PATCH 12/19] docs(c/driver/sqlite): trim formatter comment --- c/driver/sqlite/statement_reader.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/c/driver/sqlite/statement_reader.c b/c/driver/sqlite/statement_reader.c index fbd5a17748..ed13536ee0 100644 --- a/c/driver/sqlite/statement_reader.c +++ b/c/driver/sqlite/statement_reader.c @@ -955,8 +955,7 @@ AdbcStatusCode InternalSqliteStatementReaderAppendDoubleToBinary( CHECK_NA(INTERNAL, ArrowBufferReserve(binary, kReserve), error); char* output = (char*)(binary->data + binary->size_bytes); // SQLite's alternate-form-2 formatter is locale-independent, keeps enough - // significant digits to round-trip binary64, and preserves REAL values as - // SQL-compatible text (including a decimal point for integral values). + // 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) { From 24a13cde302383e7a429c7746a8f6b9ae2e942a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Wed, 21 Oct 2026 15:25:21 +0300 Subject: [PATCH 13/19] test(c/driver/sqlite): compare inferred REAL strings directly --- c/driver/sqlite/sqlite_test.cc | 38 ++++++++++------------------------ 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 2f9752a07c..0727b2d306 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -#include #include #include #include @@ -1029,44 +1028,29 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { "(0.1, 3.141592653589793), " "('end', 0.1)")); - const std::vector> expected_upcast = { - 123456789.123, 3.141592653589793, 0.1, std::nullopt}; - const std::vector> expected_append = {std::nullopt, 123456789.123, - 3.141592653589793, 0.1}; - const std::vector expected_text_upcast = {"", "", "", "end"}; - const std::vector expected_text_append = {"start", "", "", ""}; + const std::vector expected_upcast = {"123456789.123", "3.1415926535897931", + "0.1", "end"}; + const std::vector expected_append = {"start", "123456789.123", + "3.1415926535897931", "0.1"}; 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_round_trip = [](ArrowArrayView* actual, - const std::vector>& expected_values, - const std::vector& expected_text) { + auto expect_strings = [](ArrowArrayView* actual, + const std::vector& expected_values) { ASSERT_EQ(actual->length, static_cast(expected_values.size())); - ASSERT_EQ(expected_values.size(), expected_text.size()); for (int64_t i = 0; i < actual->length; i++) { const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); const std::string actual_value(actual_view.data, actual_view.size_bytes); - if (expected_values[i].has_value()) { - double parsed = 0.0; - const char* begin = actual_value.data(); - const char* end = begin + actual_value.size(); - const auto result = - std::from_chars(begin, end, parsed, std::chars_format::general); - ASSERT_EQ(result.ec, std::errc{}); - ASSERT_EQ(result.ptr, end); - EXPECT_EQ(parsed, expected_values[i].value()); - } else { - EXPECT_EQ(actual_value, expected_text[i]); - } + EXPECT_EQ(actual_value, expected_values[i]); } }; - ASSERT_NO_FATAL_FAILURE(expect_round_trip(reader.array_view->children[0], - expected_upcast, expected_text_upcast)); - ASSERT_NO_FATAL_FAILURE(expect_round_trip(reader.array_view->children[1], - expected_append, expected_text_append)); + ASSERT_NO_FATAL_FAILURE( + expect_strings(reader.array_view->children[0], expected_upcast)); + ASSERT_NO_FATAL_FAILURE( + expect_strings(reader.array_view->children[1], expected_append)); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From 01e8e538935d11fe9f44a0d3f2684be39f6ad99f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Wed, 21 Oct 2026 16:07:28 +0300 Subject: [PATCH 14/19] test(c/driver/sqlite): assert floating-point round trips --- c/driver/sqlite/sqlite_test.cc | 38 ++++++++++++++++++++++++---------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 0727b2d306..2f9752a07c 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +#include #include #include #include @@ -1028,29 +1029,44 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { "(0.1, 3.141592653589793), " "('end', 0.1)")); - const std::vector expected_upcast = {"123456789.123", "3.1415926535897931", - "0.1", "end"}; - const std::vector expected_append = {"start", "123456789.123", - "3.1415926535897931", "0.1"}; + const std::vector> expected_upcast = { + 123456789.123, 3.141592653589793, 0.1, std::nullopt}; + const std::vector> expected_append = {std::nullopt, 123456789.123, + 3.141592653589793, 0.1}; + const std::vector expected_text_upcast = {"", "", "", "end"}; + const std::vector expected_text_append = {"start", "", "", ""}; 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& expected_values) { + auto expect_round_trip = [](ArrowArrayView* actual, + const std::vector>& expected_values, + const std::vector& expected_text) { ASSERT_EQ(actual->length, static_cast(expected_values.size())); + ASSERT_EQ(expected_values.size(), expected_text.size()); for (int64_t i = 0; i < actual->length; i++) { const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); const std::string actual_value(actual_view.data, actual_view.size_bytes); - EXPECT_EQ(actual_value, expected_values[i]); + if (expected_values[i].has_value()) { + double parsed = 0.0; + const char* begin = actual_value.data(); + const char* end = begin + actual_value.size(); + const auto result = + std::from_chars(begin, end, parsed, std::chars_format::general); + ASSERT_EQ(result.ec, std::errc{}); + ASSERT_EQ(result.ptr, end); + EXPECT_EQ(parsed, expected_values[i].value()); + } else { + EXPECT_EQ(actual_value, expected_text[i]); + } } }; - ASSERT_NO_FATAL_FAILURE( - expect_strings(reader.array_view->children[0], expected_upcast)); - ASSERT_NO_FATAL_FAILURE( - expect_strings(reader.array_view->children[1], expected_append)); + ASSERT_NO_FATAL_FAILURE(expect_round_trip(reader.array_view->children[0], + expected_upcast, expected_text_upcast)); + ASSERT_NO_FATAL_FAILURE(expect_round_trip(reader.array_view->children[1], + expected_append, expected_text_append)); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From c60b90fc7e0b9bca3eae0b314c4e7f4c8b3e0120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Thu, 22 Oct 2026 01:43:37 +0300 Subject: [PATCH 15/19] test(c/driver/sqlite): compare REAL output with SQLite text --- c/driver/sqlite/sqlite_test.cc | 59 +++++++++++++++------------------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 2f9752a07c..da6377f794 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -#include #include #include #include @@ -1029,44 +1028,38 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { "(0.1, 3.141592653589793), " "('end', 0.1)")); - const std::vector> expected_upcast = { - 123456789.123, 3.141592653589793, 0.1, std::nullopt}; - const std::vector> expected_append = {std::nullopt, 123456789.123, - 3.141592653589793, 0.1}; - const std::vector expected_text_upcast = {"", "", "", "end"}; - const std::vector expected_text_append = {"start", "", "", ""}; + auto read_strings = [](ArrowArrayView* actual) { + std::vector values; + for (int64_t i = 0; i < actual->length; i++) { + const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); + values.emplace_back(actual_view.data, actual_view.size_bytes); + } + return values; + }; + + // Use SQLite's own text conversion as the reference so the test does not + // depend on a particular SQLite release's floating-point spelling. + std::vector expected_upcast; + std::vector expected_append; + { + adbc_validation::StreamReader expected; + ASSERT_NO_FATAL_FAILURE( + Exec("SELECT CAST(upcast_col AS TEXT), CAST(append_col AS TEXT) FROM foo", + kInferRows, &expected)); + ASSERT_NO_FATAL_FAILURE(expected.Next()); + expected_upcast = read_strings(expected.array_view->children[0]); + expected_append = read_strings(expected.array_view->children[1]); + } + ASSERT_EQ(SQLITE_OK, sqlite3_finalize(stmt)); + stmt = nullptr; 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_round_trip = [](ArrowArrayView* actual, - const std::vector>& expected_values, - const std::vector& expected_text) { - ASSERT_EQ(actual->length, static_cast(expected_values.size())); - ASSERT_EQ(expected_values.size(), expected_text.size()); - for (int64_t i = 0; i < actual->length; i++) { - const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); - const std::string actual_value(actual_view.data, actual_view.size_bytes); - if (expected_values[i].has_value()) { - double parsed = 0.0; - const char* begin = actual_value.data(); - const char* end = begin + actual_value.size(); - const auto result = - std::from_chars(begin, end, parsed, std::chars_format::general); - ASSERT_EQ(result.ec, std::errc{}); - ASSERT_EQ(result.ptr, end); - EXPECT_EQ(parsed, expected_values[i].value()); - } else { - EXPECT_EQ(actual_value, expected_text[i]); - } - } - }; - ASSERT_NO_FATAL_FAILURE(expect_round_trip(reader.array_view->children[0], - expected_upcast, expected_text_upcast)); - ASSERT_NO_FATAL_FAILURE(expect_round_trip(reader.array_view->children[1], - expected_append, expected_text_append)); + EXPECT_EQ(read_strings(reader.array_view->children[0]), expected_upcast); + EXPECT_EQ(read_strings(reader.array_view->children[1]), expected_append); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From 0ec96120eb8a05f3ad5e584306bff01a4b1306dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Thu, 22 Oct 2026 02:03:17 +0300 Subject: [PATCH 16/19] test(c/driver/sqlite): use SQLite REAL formatting reference --- c/driver/sqlite/sqlite_test.cc | 56 ++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index da6377f794..0cc31e0aba 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -15,6 +15,7 @@ // specific language governing permissions and limitations // under the License. +#include #include #include #include @@ -1026,25 +1027,38 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { "(123456789.123, 'start'), " "(3.141592653589793, 123456789.123), " "(0.1, 3.141592653589793), " - "('end', 0.1)")); + "('end', 0.1), " + "(42, NULL), " + "(NULL, 7), " + "('original text', '')")); auto read_strings = [](ArrowArrayView* actual) { - std::vector values; + std::vector> values; for (int64_t i = 0; i < actual->length; i++) { + if (ArrowArrayViewIsNull(actual, i)) { + values.emplace_back(std::nullopt); + continue; + } const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); - values.emplace_back(actual_view.data, actual_view.size_bytes); + values.emplace_back(std::string(actual_view.data, actual_view.size_bytes)); } return values; }; - // Use SQLite's own text conversion as the reference so the test does not - // depend on a particular SQLite release's floating-point spelling. - std::vector expected_upcast; - std::vector expected_append; + // Use SQLite's 17-digit formatter for REAL values. Preserve other SQLite + // types in the reference query so TEXT, INTEGER, and NULL values are not + // routed through the double formatter. + std::vector> expected_upcast; + std::vector> expected_append; { adbc_validation::StreamReader expected; ASSERT_NO_FATAL_FAILURE( - Exec("SELECT CAST(upcast_col AS TEXT), CAST(append_col AS TEXT) FROM foo", + Exec("SELECT " + "CASE typeof(upcast_col) WHEN 'real' THEN printf('%!.17g', upcast_col) " + "WHEN 'integer' THEN CAST(upcast_col AS TEXT) ELSE upcast_col END, " + "CASE typeof(append_col) WHEN 'real' THEN printf('%!.17g', append_col) " + "WHEN 'integer' THEN CAST(append_col AS TEXT) ELSE append_col END " + "FROM foo", kInferRows, &expected)); ASSERT_NO_FATAL_FAILURE(expected.Next()); expected_upcast = read_strings(expected.array_view->children[0]); @@ -1058,8 +1072,30 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { ASSERT_EQ(NANOARROW_TYPE_STRING, reader.fields[0].type); ASSERT_EQ(NANOARROW_TYPE_STRING, reader.fields[1].type); ASSERT_NO_FATAL_FAILURE(reader.Next()); - EXPECT_EQ(read_strings(reader.array_view->children[0]), expected_upcast); - EXPECT_EQ(read_strings(reader.array_view->children[1]), expected_append); + const auto actual_upcast = read_strings(reader.array_view->children[0]); + const auto actual_append = read_strings(reader.array_view->children[1]); + EXPECT_EQ(actual_upcast, expected_upcast); + EXPECT_EQ(actual_append, expected_append); + + // Parsing independently against the inserted binary64 values ensures the + // reference comparison alone does not mask a loss of round-trip precision. + auto expect_round_trip = [](const std::vector>& values, + const std::vector>& expected) { + for (const auto& [index, expected_value] : expected) { + ASSERT_TRUE(values[index].has_value()); + const std::string& value = *values[index]; + double parsed = 0.0; + const auto result = std::from_chars(value.data(), value.data() + value.size(), + parsed, std::chars_format::general); + ASSERT_EQ(std::errc{}, result.ec); + ASSERT_EQ(value.data() + value.size(), result.ptr); + EXPECT_EQ(expected_value, parsed); + } + }; + ASSERT_NO_FATAL_FAILURE(expect_round_trip( + actual_upcast, {{0, 123456789.123}, {1, 3.141592653589793}, {2, 0.1}})); + ASSERT_NO_FATAL_FAILURE(expect_round_trip( + actual_append, {{1, 123456789.123}, {2, 3.141592653589793}, {3, 0.1}})); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From 2a20140a394fff5a47cc0ed71e0dd964f91eb547 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Thu, 22 Oct 2026 11:04:18 +0300 Subject: [PATCH 17/19] test(c/driver/sqlite): simplify precision regression --- c/driver/sqlite/sqlite_test.cc | 77 ++++++++-------------------------- 1 file changed, 18 insertions(+), 59 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 0cc31e0aba..cd90c28bf9 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -15,7 +15,6 @@ // specific language governing permissions and limitations // under the License. -#include #include #include #include @@ -1020,7 +1019,7 @@ TEST_F(SqliteReaderTest, IntsFloatsStrs) { {"1.0", "1.0", "", "Inf", "-Inf"})); } -TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { +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 " @@ -1032,70 +1031,30 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesRoundTripPrecision) { "(NULL, 7), " "('original text', '')")); - auto read_strings = [](ArrowArrayView* actual) { - std::vector> values; - for (int64_t i = 0; i < actual->length; i++) { - if (ArrowArrayViewIsNull(actual, i)) { - values.emplace_back(std::nullopt); - continue; - } - const ArrowStringView actual_view = ArrowArrayViewGetStringUnsafe(actual, i); - values.emplace_back(std::string(actual_view.data, actual_view.size_bytes)); - } - return values; - }; - - // Use SQLite's 17-digit formatter for REAL values. Preserve other SQLite - // types in the reference query so TEXT, INTEGER, and NULL values are not - // routed through the double formatter. - std::vector> expected_upcast; - std::vector> expected_append; - { - adbc_validation::StreamReader expected; - ASSERT_NO_FATAL_FAILURE( - Exec("SELECT " - "CASE typeof(upcast_col) WHEN 'real' THEN printf('%!.17g', upcast_col) " - "WHEN 'integer' THEN CAST(upcast_col AS TEXT) ELSE upcast_col END, " - "CASE typeof(append_col) WHEN 'real' THEN printf('%!.17g', append_col) " - "WHEN 'integer' THEN CAST(append_col AS TEXT) ELSE append_col END " - "FROM foo", - kInferRows, &expected)); - ASSERT_NO_FATAL_FAILURE(expected.Next()); - expected_upcast = read_strings(expected.array_view->children[0]); - expected_append = read_strings(expected.array_view->children[1]); - } - ASSERT_EQ(SQLITE_OK, sqlite3_finalize(stmt)); - stmt = nullptr; - 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()); - const auto actual_upcast = read_strings(reader.array_view->children[0]); - const auto actual_append = read_strings(reader.array_view->children[1]); - EXPECT_EQ(actual_upcast, expected_upcast); - EXPECT_EQ(actual_append, expected_append); - - // Parsing independently against the inserted binary64 values ensures the - // reference comparison alone does not mask a loss of round-trip precision. - auto expect_round_trip = [](const std::vector>& values, - const std::vector>& expected) { - for (const auto& [index, expected_value] : expected) { - ASSERT_TRUE(values[index].has_value()); - const std::string& value = *values[index]; - double parsed = 0.0; - const auto result = std::from_chars(value.data(), value.data() + value.size(), - parsed, std::chars_format::general); - ASSERT_EQ(std::errc{}, result.ec); - ASSERT_EQ(value.data() + value.size(), result.ptr); - EXPECT_EQ(expected_value, parsed); + auto expect_strings = [](ArrowArrayView* actual, + const std::vector>& expected) { + ASSERT_EQ(actual->length, static_cast(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(expect_round_trip( - actual_upcast, {{0, 123456789.123}, {1, 3.141592653589793}, {2, 0.1}})); - ASSERT_NO_FATAL_FAILURE(expect_round_trip( - actual_append, {{1, 123456789.123}, {2, 3.141592653589793}, {3, 0.1}})); + ASSERT_NO_FATAL_FAILURE(expect_strings(reader.array_view->children[0], + {"123456789.123", "3.1415926535897931", "0.1", + "end", "42", std::nullopt, "original text"})); + ASSERT_NO_FATAL_FAILURE(expect_strings( + reader.array_view->children[1], + {"start", "123456789.123", "3.1415926535897931", "0.1", std::nullopt, "7", ""})); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From 0c039e1761e3db00108d55098d7f286c8a9b1952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Thu, 22 Oct 2026 15:58:06 +0300 Subject: [PATCH 18/19] test(sqlite): assert round-trip precision across SQLite versions --- c/driver/sqlite/sqlite_test.cc | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index cd90c28bf9..c30eb52107 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -1025,8 +1025,8 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesPrecision) { Exec("INSERT INTO foo VALUES " "(123456789.123, 'start'), " "(3.141592653589793, 123456789.123), " - "(0.1, 3.141592653589793), " - "('end', 0.1), " + "(0.10000000000000002, 3.141592653589793), " + "('end', 0.10000000000000002), " "(42, NULL), " "(NULL, 7), " "('original text', '')")); @@ -1050,11 +1050,13 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesPrecision) { } }; ASSERT_NO_FATAL_FAILURE(expect_strings(reader.array_view->children[0], - {"123456789.123", "3.1415926535897931", "0.1", + {"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.1", std::nullopt, "7", ""})); + {"start", "123456789.123", "3.1415926535897931", "0.10000000000000002", + std::nullopt, "7", ""})); } TEST_F(SqliteReaderTest, InferFloatToStringUsesDotDecimalInCommaLocale) { From 67ed93bb74ef0aaba85a82f04823e0e747fa94f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Thu, 22 Oct 2026 16:31:46 +0300 Subject: [PATCH 19/19] style: apply clang-format to SQLite precision test --- c/driver/sqlite/sqlite_test.cc | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index c30eb52107..c7c37e7637 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -1049,14 +1049,13 @@ TEST_F(SqliteReaderTest, InferFloatToStringPreservesPrecision) { EXPECT_EQ(std::string(value.data, value.size_bytes), expected[i].value()); } }; - ASSERT_NO_FATAL_FAILURE(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", ""})); + ASSERT_NO_FATAL_FAILURE( + 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) {