Skip to content
10 changes: 10 additions & 0 deletions c/driver/sqlite/sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,16 @@ class SqliteStatement : public driver::Statement<SqliteStatement> {
} 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));
}
Expand Down
7 changes: 5 additions & 2 deletions c/driver/sqlite/statement_reader.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 10 additions & 0 deletions c/validation/adbc_validation.h
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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(); \
Expand Down Expand Up @@ -450,6 +454,8 @@ class StatementTest {
void TestSqlPrepareErrorParamCountMismatch();

void TestSqlBind();
void TestSqlBindZeroRows();
void TestSqlBindNullType();

void TestSqlQueryEmpty();
void TestSqlQueryInts();
Expand All @@ -461,6 +467,7 @@ class StatementTest {
void TestSqlQueryCancel();
void TestSqlQueryErrors();
void TestSqlQueryTrailingSemicolons();
void TestSqlQueryIncrementalDefault();
void TestSqlQueryRowsAffectedDelete();
void TestSqlQueryRowsAffectedDeleteStream();

Expand Down Expand Up @@ -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(); } \
Expand All @@ -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(); \
Expand Down
87 changes: 87 additions & 0 deletions c/validation/adbc_validation_connection.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<uint32_t> 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();
Expand Down
142 changes: 142 additions & 0 deletions c/validation/adbc_validation_statement.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<struct ArrowSchema> schema;
Handle<struct ArrowArray> 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<int64_t>(&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<int64_t>(&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<struct ArrowSchema> schema;
Handle<struct ArrowArray> array;
struct ArrowError na_error;
ASSERT_THAT(MakeSchema(&schema.value, {{"nulls", NANOARROW_TYPE_NA}}), IsOkErrno());
ASSERT_THAT(MakeBatch<int64_t>(&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));

Expand Down Expand Up @@ -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));
Expand Down
Loading