diff --git a/c/driver/sqlite/sqlite_test.cc b/c/driver/sqlite/sqlite_test.cc index 22f7c8afe3..c7c37e7637 100644 --- a/c/driver/sqlite/sqlite_test.cc +++ b/c/driver/sqlite/sqlite_test.cc @@ -15,6 +15,8 @@ // specific language governing permissions and limitations // under the License. +#include +#include #include #include // NOLINT [build/c++17] #include @@ -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(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>& 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( - CompareArray(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) { diff --git a/c/driver/sqlite/statement_reader.c b/c/driver/sqlite/statement_reader.c index 0827a860ab..ed13536ee0 100644 --- a/c/driver/sqlite/statement_reader.c +++ b/c/driver/sqlite/statement_reader.c @@ -23,7 +23,6 @@ #include #include -#include #include #include #include @@ -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;