Skip to content

Commit 3e4a74d

Browse files
corleymaMatthew Corleylidavidm
authored andcommitted
fix(c/driver/postgresql): honor GetObjects schema filter (apache#3855)
## Summary Fix the PostgreSQL driver’s `GetObjects` table enumeration so `db_schema` / `db_schema_filter` does not depend on `search_path`. ## Details The tables query used `pg_catalog.pg_table_is_visible(c.oid)`, which is `search_path`-dependent and can hide tables in non-current schemas even when a schema filter is provided. Remove that predicate and rely on the explicit schema predicate. ## Tests - `pre-commit run` - `ctest -L driver-postgresql` (against `postgres-test` from `docker compose`) Closes apache#3854 --------- Co-authored-by: Matthew Corley <matt.corley@freenome.com> Co-authored-by: David Li <li.davidm96@gmail.com>
1 parent dccb5aa commit 3e4a74d

File tree

3 files changed

+100
-1
lines changed

3 files changed

+100
-1
lines changed

c/driver/postgresql/connection.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,15 @@ static const char* kSchemaQueryAll =
7272
// Parameterized on schema_name, relkind
7373
// Note that when binding relkind as a string it must look like {"r", "v", ...}
7474
// (i.e., double quotes). Binding a binary list<string> element also works.
75+
// Don't use pg_table_is_visible(): it is search_path-dependent and would hide tables
76+
// in non-current schemas even when GetObjects is called with a schema filter.
7577
static const char* kTablesQueryAll =
7678
"SELECT c.relname, CASE c.relkind WHEN 'r' THEN 'table' WHEN 'v' THEN 'view' "
7779
"WHEN 'm' THEN 'materialized view' WHEN 't' THEN 'TOAST table' "
7880
"WHEN 'f' THEN 'foreign table' WHEN 'p' THEN 'partitioned table' END "
7981
"AS reltype FROM pg_catalog.pg_class c "
8082
"LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace "
81-
"WHERE pg_catalog.pg_table_is_visible(c.oid) AND n.nspname = $1 AND c.relkind = "
83+
"WHERE n.nspname = $1 AND c.relkind = "
8284
"ANY($2)";
8385

8486
// Parameterized on schema_name, table_name

c/driver/postgresql/postgresql_test.cc

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,61 @@ TEST_F(PostgresConnectionTest, GetObjectsGetDbSchemas) {
372372
ASSERT_NE(schema, nullptr) << "schema public not found";
373373
}
374374

375+
TEST_F(PostgresConnectionTest, GetObjectsSchemaFilterFindsTablesOutsideSearchPath) {
376+
ASSERT_THAT(AdbcConnectionNew(&connection, &error), IsOkStatus(&error));
377+
ASSERT_THAT(AdbcConnectionInit(&connection, &database, &error), IsOkStatus(&error));
378+
379+
const std::string schema_name = "adbc_get_objects_test";
380+
const std::string table_name = "schema_filter_table";
381+
382+
// Ensure the schema is not part of the current search_path.
383+
ASSERT_THAT(
384+
AdbcConnectionSetOption(&connection, ADBC_CONNECTION_OPTION_CURRENT_DB_SCHEMA,
385+
"public", &error),
386+
IsOkStatus(&error));
387+
388+
ASSERT_THAT(quirks()->EnsureDbSchema(&connection, schema_name, &error),
389+
IsOkStatus(&error));
390+
ASSERT_THAT(quirks()->DropTable(&connection, table_name, schema_name, &error),
391+
IsOkStatus(&error));
392+
393+
{
394+
adbc_validation::Handle<struct AdbcStatement> statement;
395+
ASSERT_THAT(AdbcStatementNew(&connection, &statement.value, &error),
396+
IsOkStatus(&error));
397+
398+
std::string create =
399+
"CREATE TABLE \"" + schema_name + "\".\"" + table_name + "\" (ints INT)";
400+
ASSERT_THAT(AdbcStatementSetSqlQuery(&statement.value, create.c_str(), &error),
401+
IsOkStatus(&error));
402+
ASSERT_THAT(AdbcStatementExecuteQuery(&statement.value, nullptr, nullptr, &error),
403+
IsOkStatus(&error));
404+
}
405+
406+
adbc_validation::StreamReader reader;
407+
ASSERT_THAT(AdbcConnectionGetObjects(&connection, ADBC_OBJECT_DEPTH_TABLES, nullptr,
408+
schema_name.c_str(), nullptr, nullptr, nullptr,
409+
&reader.stream.value, &error),
410+
IsOkStatus(&error));
411+
ASSERT_NO_FATAL_FAILURE(reader.GetSchema());
412+
ASSERT_NO_FATAL_FAILURE(reader.Next());
413+
ASSERT_NE(nullptr, reader.array->release);
414+
ASSERT_GT(reader.array->length, 0);
415+
416+
auto get_objects_data = adbc_validation::GetObjectsReader{&reader.array_view.value};
417+
ASSERT_NE(*get_objects_data, nullptr)
418+
<< "could not initialize the AdbcGetObjectsData object";
419+
420+
const auto catalog = adbc_validation::ConnectionGetOption(
421+
&connection, ADBC_CONNECTION_OPTION_CURRENT_CATALOG, &error);
422+
ASSERT_TRUE(catalog.has_value());
423+
424+
struct AdbcGetObjectsTable* table = InternalAdbcGetObjectsDataGetTableByName(
425+
*get_objects_data, catalog->c_str(), schema_name.c_str(), table_name.c_str());
426+
ASSERT_NE(table, nullptr) << "could not find " << schema_name << "." << table_name
427+
<< " via GetObjects";
428+
}
429+
375430
TEST_F(PostgresConnectionTest, GetObjectsGetAllFindsPrimaryKey) {
376431
ASSERT_THAT(AdbcConnectionNew(&connection, &error), IsOkStatus(&error));
377432
ASSERT_THAT(AdbcConnectionInit(&connection, &database, &error), IsOkStatus(&error));

python/adbc_driver_postgresql/tests/test_dbapi.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,48 @@ def test_conn_change_db_schema(postgres: dbapi.Connection) -> None:
5353
assert postgres.adbc_current_db_schema == "dbapischema"
5454

5555

56+
def test_get_objects_schema_filter_outside_search_path(
57+
postgres: dbapi.Connection,
58+
) -> None:
59+
schema_name = "dbapi_get_objects_test"
60+
table_name = "schema_filter_table"
61+
62+
# Regression test: adbc_get_objects(db_schema_filter=...) should not depend on the
63+
# connection's current schema/search_path.
64+
assert postgres.adbc_current_db_schema == "public"
65+
66+
with postgres.cursor() as cur:
67+
cur.execute(f'CREATE SCHEMA IF NOT EXISTS "{schema_name}"')
68+
cur.execute(f'DROP TABLE IF EXISTS "{schema_name}"."{table_name}"')
69+
cur.execute(f'CREATE TABLE "{schema_name}"."{table_name}" (ints INT)')
70+
postgres.commit()
71+
72+
assert postgres.adbc_current_db_schema == "public"
73+
74+
metadata = (
75+
postgres.adbc_get_objects(
76+
depth="tables",
77+
db_schema_filter=schema_name,
78+
table_name_filter=table_name,
79+
)
80+
.read_all()
81+
.to_pylist()
82+
)
83+
84+
catalog_name = postgres.adbc_current_catalog
85+
catalog = next(
86+
(row for row in metadata if row["catalog_name"] == catalog_name), None
87+
)
88+
assert catalog is not None
89+
90+
schemas = catalog["catalog_db_schemas"]
91+
assert len(schemas) == 1
92+
assert schemas[0]["db_schema_name"] == schema_name
93+
tables = schemas[0]["db_schema_tables"]
94+
assert len(tables) == 1
95+
assert tables[0]["table_name"] == table_name
96+
97+
5698
def test_conn_get_info(postgres: dbapi.Connection) -> None:
5799
info = postgres.adbc_get_info()
58100
assert info["driver_name"] == "ADBC PostgreSQL Driver"

0 commit comments

Comments
 (0)