diff --git a/c/driver/sqlite/sqlite.cc b/c/driver/sqlite/sqlite.cc index 62dec5ab25..06c0ba02e5 100644 --- a/c/driver/sqlite/sqlite.cc +++ b/c/driver/sqlite/sqlite.cc @@ -1250,6 +1250,16 @@ class SqliteStatement : public driver::Statement { } else if (key == kStatementOptionBindByName) { UNWRAP_RESULT(bind_by_name_, value.AsBool()); return status::Ok(); + } else if (key == ADBC_STATEMENT_OPTION_INCREMENTAL) { + // Incremental execution is not supported, but the option defaults to + // disabled, so accept an explicit "disabled" as a no-op. + bool incremental; + UNWRAP_RESULT(incremental, value.AsBool()); + if (incremental) { + return status::fmt::NotImplemented("{} Incremental execution is not supported", + kErrorPrefix); + } + return status::Ok(); } return Base::SetOptionImpl(key, std::move(value)); } diff --git a/c/driver/sqlite/statement_reader.c b/c/driver/sqlite/statement_reader.c index 7c2cae682c..137ed3611d 100644 --- a/c/driver/sqlite/statement_reader.c +++ b/c/driver/sqlite/statement_reader.c @@ -1213,9 +1213,12 @@ AdbcStatusCode InternalAdbcSqliteExportReader(sqlite3* db, sqlite3_stmt* stmt, } } - if (status == ADBC_STATUS_OK && !reader->done) { + if (status == ADBC_STATUS_OK) { + // Even if the bound parameter stream had zero rows (reader->done was set + // above without executing the query), still run InferFinalize below so + // that the reported schema has the correct number of columns. int64_t num_rows = 0; - while (((size_t)num_rows) < batch_size) { + while (!reader->done && ((size_t)num_rows) < batch_size) { int rc = sqlite3_step(stmt); if (rc == SQLITE_DONE) { if (!binder) { diff --git a/c/validation/adbc_validation.h b/c/validation/adbc_validation.h index 27f8f2543f..d72e674561 100644 --- a/c/validation/adbc_validation.h +++ b/c/validation/adbc_validation.h @@ -298,11 +298,13 @@ class ConnectionTest { void TestAutocommitDefault(); void TestAutocommitToggle(); + void TestAutocommitIntRoundTrip(); void TestMetadataCurrentCatalog(); void TestMetadataCurrentDbSchema(); void TestMetadataGetInfo(); + void TestMetadataGetInfoAllCodes(); void TestMetadataGetTableSchema(); void TestMetadataGetTableSchemaDbSchema(); void TestMetadataGetTableSchemaEscaping(); @@ -335,9 +337,11 @@ class ConnectionTest { TEST_F(FIXTURE, Concurrent) { TestConcurrent(); } \ TEST_F(FIXTURE, AutocommitDefault) { TestAutocommitDefault(); } \ TEST_F(FIXTURE, AutocommitToggle) { TestAutocommitToggle(); } \ + TEST_F(FIXTURE, AutocommitIntRoundTrip) { TestAutocommitIntRoundTrip(); } \ TEST_F(FIXTURE, MetadataCurrentCatalog) { TestMetadataCurrentCatalog(); } \ TEST_F(FIXTURE, MetadataCurrentDbSchema) { TestMetadataCurrentDbSchema(); } \ TEST_F(FIXTURE, MetadataGetInfo) { TestMetadataGetInfo(); } \ + TEST_F(FIXTURE, MetadataGetInfoAllCodes) { TestMetadataGetInfoAllCodes(); } \ TEST_F(FIXTURE, MetadataGetTableSchema) { TestMetadataGetTableSchema(); } \ TEST_F(FIXTURE, MetadataGetTableSchemaDbSchema) { \ TestMetadataGetTableSchemaDbSchema(); \ @@ -450,6 +454,8 @@ class StatementTest { void TestSqlPrepareErrorParamCountMismatch(); void TestSqlBind(); + void TestSqlBindZeroRows(); + void TestSqlBindNullType(); void TestSqlQueryEmpty(); void TestSqlQueryInts(); @@ -461,6 +467,7 @@ class StatementTest { void TestSqlQueryCancel(); void TestSqlQueryErrors(); void TestSqlQueryTrailingSemicolons(); + void TestSqlQueryIncrementalDefault(); void TestSqlQueryRowsAffectedDelete(); void TestSqlQueryRowsAffectedDeleteStream(); @@ -572,6 +579,8 @@ void StatementTest::TestSqlIngestType(ArrowType type, TestSqlPrepareErrorParamCountMismatch(); \ } \ TEST_F(FIXTURE, SqlBind) { TestSqlBind(); } \ + TEST_F(FIXTURE, SqlBindZeroRows) { TestSqlBindZeroRows(); } \ + TEST_F(FIXTURE, SqlBindNullType) { TestSqlBindNullType(); } \ TEST_F(FIXTURE, SqlQueryEmpty) { TestSqlQueryEmpty(); } \ TEST_F(FIXTURE, SqlQueryInts) { TestSqlQueryInts(); } \ TEST_F(FIXTURE, SqlQueryFloats) { TestSqlQueryFloats(); } \ @@ -580,6 +589,7 @@ void StatementTest::TestSqlIngestType(ArrowType type, TEST_F(FIXTURE, SqlQueryCancel) { TestSqlQueryCancel(); } \ TEST_F(FIXTURE, SqlQueryErrors) { TestSqlQueryErrors(); } \ TEST_F(FIXTURE, SqlQueryTrailingSemicolons) { TestSqlQueryTrailingSemicolons(); } \ + TEST_F(FIXTURE, SqlQueryIncrementalDefault) { TestSqlQueryIncrementalDefault(); } \ TEST_F(FIXTURE, SqlQueryRowsAffectedDelete) { TestSqlQueryRowsAffectedDelete(); } \ TEST_F(FIXTURE, SqlQueryRowsAffectedDeleteStream) { \ TestSqlQueryRowsAffectedDeleteStream(); \ diff --git a/c/validation/adbc_validation_connection.cc b/c/validation/adbc_validation_connection.cc index d7e61b918c..57c285fdaf 100644 --- a/c/validation/adbc_validation_connection.cc +++ b/c/validation/adbc_validation_connection.cc @@ -140,6 +140,32 @@ void ConnectionTest::TestAutocommitToggle() { IsOkStatus(&error)); } +void ConnectionTest::TestAutocommitIntRoundTrip() { + if (!quirks()->supports_get_option()) { + GTEST_SKIP(); + } + + ASSERT_THAT(AdbcConnectionNew(&connection, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcConnectionInit(&connection, &database, &error), IsOkStatus(&error)); + + // Drivers are not required to accept an integer value for the + // (string-typed) autocommit option, but a driver that accepts a value via + // SetOptionInt must also support reading it back via GetOptionInt. + AdbcStatusCode set_status = AdbcConnectionSetOptionInt( + &connection, ADBC_CONNECTION_OPTION_AUTOCOMMIT, 1, &error); + if (set_status != ADBC_STATUS_OK) { + if (error.release) error.release(&error); + GTEST_SKIP() << "Driver does not accept an integer value for " + << ADBC_CONNECTION_OPTION_AUTOCOMMIT; + } + + int64_t value = -1; + ASSERT_THAT(AdbcConnectionGetOptionInt(&connection, ADBC_CONNECTION_OPTION_AUTOCOMMIT, + &value, &error), + IsOkStatus(&error)); + ASSERT_EQ(1, value); +} + //------------------------------------------------------------ // Tests of metadata @@ -289,6 +315,67 @@ void ConnectionTest::TestMetadataGetInfo() { } } +void ConnectionTest::TestMetadataGetInfoAllCodes() { + ASSERT_THAT(AdbcConnectionNew(&connection, &error), IsOkStatus(&error)); + ASSERT_THAT(AdbcConnectionInit(&connection, &database, &error), IsOkStatus(&error)); + + if (!quirks()->supports_get_sql_info()) { + GTEST_SKIP(); + } + + // Passing NULL info_codes requests all info codes recognized by the driver. + std::vector all_codes; + { + StreamReader reader; + ASSERT_THAT( + AdbcConnectionGetInfo(&connection, /*info_codes=*/nullptr, + /*info_codes_length=*/0, &reader.stream.value, &error), + IsOkStatus(&error)); + ASSERT_NO_FATAL_FAILURE(reader.GetSchema()); + while (true) { + ASSERT_NO_FATAL_FAILURE(reader.Next()); + if (!reader.array->release) break; + for (int64_t row = 0; row < reader.array->length; row++) { + ASSERT_FALSE(ArrowArrayViewIsNull(reader.array_view->children[0], row)); + all_codes.push_back( + reader.array_view->children[0]->buffer_views[1].data.as_uint32[row]); + } + } + } + + // Any info code the driver returns for an explicit request is a code it + // recognizes, so it must also be present in the all-codes result. + for (uint32_t info_code : { + ADBC_INFO_VENDOR_NAME, + ADBC_INFO_VENDOR_VERSION, + ADBC_INFO_VENDOR_ARROW_VERSION, + ADBC_INFO_DRIVER_NAME, + ADBC_INFO_DRIVER_VERSION, + ADBC_INFO_DRIVER_ADBC_VERSION, + }) { + SCOPED_TRACE("info_code = " + std::to_string(info_code)); + uint32_t info[] = {info_code}; + + StreamReader reader; + ASSERT_THAT(AdbcConnectionGetInfo(&connection, info, 1, &reader.stream.value, &error), + IsOkStatus(&error)); + ASSERT_NO_FATAL_FAILURE(reader.GetSchema()); + while (true) { + ASSERT_NO_FATAL_FAILURE(reader.Next()); + if (!reader.array->release) break; + for (int64_t row = 0; row < reader.array->length; row++) { + ASSERT_FALSE(ArrowArrayViewIsNull(reader.array_view->children[0], row)); + const uint32_t code = + reader.array_view->children[0]->buffer_views[1].data.as_uint32[row]; + EXPECT_THAT(all_codes, ::testing::Contains(code)) + << "info code " << code + << " is returned for an explicit request but missing from the " + "all-codes GetInfo result"; + } + } + } +} + void ConnectionTest::TestMetadataGetTableSchema() { if (!quirks()->supports_bulk_ingest(ADBC_INGEST_OPTION_MODE_CREATE)) { GTEST_SKIP(); diff --git a/c/validation/adbc_validation_statement.cc b/c/validation/adbc_validation_statement.cc index a154b9cabc..34fc5513d5 100644 --- a/c/validation/adbc_validation_statement.cc +++ b/c/validation/adbc_validation_statement.cc @@ -2255,6 +2255,107 @@ void StatementTest::TestSqlBind() { } } +void StatementTest::TestSqlBindZeroRows() { + if (!quirks()->supports_dynamic_parameter_binding()) { + GTEST_SKIP(); + } + + ASSERT_THAT(AdbcStatementNew(&connection, &statement, &error), IsOkStatus(&error)); + std::string query = "SELECT " + quirks()->BindParameter(0); + ASSERT_THAT(AdbcStatementSetSqlQuery(&statement, query.c_str(), &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcStatementPrepare(&statement, &error), IsOkStatus(&error)); + + Handle schema; + Handle array; + struct ArrowError na_error; + + // Execute once with a single bound row to learn the result schema. + int64_t n_children = -1; + ASSERT_THAT(MakeSchema(&schema.value, {{"int64s", NANOARROW_TYPE_INT64}}), IsOkErrno()); + ASSERT_THAT(MakeBatch(&schema.value, &array.value, &na_error, {42}), + IsOkErrno()); + ASSERT_THAT(AdbcStatementBind(&statement, &array.value, &schema.value, &error), + IsOkStatus(&error)); + { + StreamReader reader; + ASSERT_THAT(AdbcStatementExecuteQuery(&statement, &reader.stream.value, + &reader.rows_affected, &error), + IsOkStatus(&error)); + ASSERT_NO_FATAL_FAILURE(reader.GetSchema()); + n_children = reader.schema->n_children; + ASSERT_GT(n_children, 0); + while (true) { + ASSERT_NO_FATAL_FAILURE(reader.Next()); + if (!reader.array->release) break; + } + } + + // Executing the same query with zero bound rows must report the same + // result schema, with no rows. + ASSERT_THAT(MakeSchema(&schema.value, {{"int64s", NANOARROW_TYPE_INT64}}), IsOkErrno()); + ASSERT_THAT(MakeBatch(&schema.value, &array.value, &na_error, {}), + IsOkErrno()); + ASSERT_THAT(AdbcStatementBind(&statement, &array.value, &schema.value, &error), + IsOkStatus(&error)); + { + StreamReader reader; + ASSERT_THAT(AdbcStatementExecuteQuery(&statement, &reader.stream.value, + &reader.rows_affected, &error), + IsOkStatus(&error)); + ASSERT_NO_FATAL_FAILURE(reader.GetSchema()); + EXPECT_EQ(n_children, reader.schema->n_children); + while (true) { + ASSERT_NO_FATAL_FAILURE(reader.Next()); + if (!reader.array->release) break; + EXPECT_EQ(0, reader.array->length); + } + } +} + +void StatementTest::TestSqlBindNullType() { + if (!quirks()->supports_dynamic_parameter_binding()) { + GTEST_SKIP(); + } + + ASSERT_THAT(AdbcStatementNew(&connection, &statement, &error), IsOkStatus(&error)); + std::string query = "SELECT " + quirks()->BindParameter(0); + ASSERT_THAT(AdbcStatementSetSqlQuery(&statement, query.c_str(), &error), + IsOkStatus(&error)); + ASSERT_THAT(AdbcStatementPrepare(&statement, &error), IsOkStatus(&error)); + + // GetParameterSchema reports a parameter whose type cannot be determined + // as NA (null type), so a driver must accept a bind batch built from that + // schema: a null-typed (all-NULL) parameter column. + Handle schema; + Handle array; + struct ArrowError na_error; + ASSERT_THAT(MakeSchema(&schema.value, {{"nulls", NANOARROW_TYPE_NA}}), IsOkErrno()); + ASSERT_THAT(MakeBatch(&schema.value, &array.value, &na_error, + {std::nullopt, std::nullopt}), + IsOkErrno()); + ASSERT_THAT(AdbcStatementBind(&statement, &array.value, &schema.value, &error), + IsOkStatus(&error)); + + StreamReader reader; + ASSERT_THAT(AdbcStatementExecuteQuery(&statement, &reader.stream.value, + &reader.rows_affected, &error), + IsOkStatus(&error)); + ASSERT_NO_FATAL_FAILURE(reader.GetSchema()); + ASSERT_EQ(1, reader.schema->n_children); + + int64_t nrows = 0; + while (true) { + ASSERT_NO_FATAL_FAILURE(reader.Next()); + if (!reader.array->release) break; + for (int64_t row = 0; row < reader.array->length; row++) { + EXPECT_TRUE(ArrowArrayViewIsNull(reader.array_view->children[0], row)); + } + nrows += reader.array->length; + } + ASSERT_EQ(2, nrows); +} + void StatementTest::TestSqlQueryEmpty() { ASSERT_THAT(AdbcStatementNew(&connection, &statement, &error), IsOkStatus(&error)); @@ -2539,6 +2640,47 @@ void StatementTest::TestSqlQueryTrailingSemicolons() { ASSERT_THAT(AdbcStatementRelease(&statement, &error), IsOkStatus(&error)); } +void StatementTest::TestSqlQueryIncrementalDefault() { + ASSERT_THAT(AdbcStatementNew(&connection, &statement, &error), IsOkStatus(&error)); + + // Incremental execution defaults to disabled, so explicitly setting the + // option to its default value must succeed as a no-op even in drivers + // that do not implement incremental execution (a generic client may + // write back the default unconditionally). + ASSERT_THAT(AdbcStatementSetOption(&statement, ADBC_STATEMENT_OPTION_INCREMENTAL, + ADBC_OPTION_VALUE_DISABLED, &error), + IsOkStatus(&error)); + + // Actually enabling incremental execution may legitimately be unsupported. + AdbcStatusCode enable_status = AdbcStatementSetOption( + &statement, ADBC_STATEMENT_OPTION_INCREMENTAL, ADBC_OPTION_VALUE_ENABLED, &error); + if (enable_status == ADBC_STATUS_OK) { + ASSERT_THAT(AdbcStatementSetOption(&statement, ADBC_STATEMENT_OPTION_INCREMENTAL, + ADBC_OPTION_VALUE_DISABLED, &error), + IsOkStatus(&error)); + } else { + ASSERT_THAT(enable_status, IsStatus(ADBC_STATUS_NOT_IMPLEMENTED, &error)); + if (error.release) error.release(&error); + } + + // The statement must still execute a plain query normally. + ASSERT_THAT(AdbcStatementSetSqlQuery(&statement, "SELECT 1", &error), + IsOkStatus(&error)); + StreamReader reader; + ASSERT_THAT(AdbcStatementExecuteQuery(&statement, &reader.stream.value, + &reader.rows_affected, &error), + IsOkStatus(&error)); + ASSERT_NO_FATAL_FAILURE(reader.GetSchema()); + ASSERT_EQ(1, reader.schema->n_children); + int64_t nrows = 0; + while (true) { + ASSERT_NO_FATAL_FAILURE(reader.Next()); + if (!reader.array->release) break; + nrows += reader.array->length; + } + ASSERT_EQ(1, nrows); +} + void StatementTest::TestSqlQueryRowsAffectedDelete() { ASSERT_THAT(quirks()->DropTable(&connection, "delete_test", &error), IsOkStatus(&error));