From 007d6583050ca752a7e81abd3333f4b0c84d8612 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Sat, 26 Sep 2026 04:32:03 +0000 Subject: [PATCH 1/2] Exclude views from get_table_names() get_table_names() read every row of information_schema.tables for the schema, so views were returned as tables as well as by get_view_names(). SQLAlchemy defines get_table_names() as base tables only, and callers that list tables and views separately showed each view twice. Filter on table_type = 'BASE TABLE'. has_table() previously relied on get_table_names(); it now checks all relations directly, so it keeps returning True for views as SQLAlchemy 2.0 specifies. --- CHANGES.md | 3 +++ sqlalchemy_cockroachdb/base.py | 19 +++++++++++++------ test/test_introspection.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index de366b0..f32ab25 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,9 @@ Unreleased - include identity column info (#297), and - avoid parse error when reflecting ENUMs (#303). (CRDB 26.3+ required for full compatibility.) +- `get_table_names()` now returns base tables only; views were also listed + and are reported by `get_view_names()`. `has_table()` still returns True for + views, as in SQLAlchemy 2.0. # Version 2.0.4 April 23, 2026 diff --git a/sqlalchemy_cockroachdb/base.py b/sqlalchemy_cockroachdb/base.py index 53cb532..94f21c9 100644 --- a/sqlalchemy_cockroachdb/base.py +++ b/sqlalchemy_cockroachdb/base.py @@ -127,18 +127,25 @@ def get_table_names(self, conn, schema=None, **kw): # v1.1 or earlier. return [row.Table for row in conn.execute(text("SHOW TABLES"))] - # v2.0+ have a good information schema. Use it. + # v2.0+ have a good information schema. Use it. Only base tables are + # returned; views are reported by get_view_names(). + return self._information_schema_relations(conn, schema, base_tables_only=True) + + def _information_schema_relations(self, conn, schema, base_tables_only): + query = "SELECT table_name FROM information_schema.tables WHERE table_schema=:schema" + if base_tables_only: + query += " AND table_type = 'BASE TABLE'" return [ row.table_name - for row in conn.execute( - text("SELECT table_name FROM information_schema.tables WHERE table_schema=:schema"), - {"schema": schema or self.default_schema_name}, - ) + for row in conn.execute(text(query), {"schema": schema or self.default_schema_name}) ] def has_table(self, conn, table, schema=None, info_cache=None): # Upstream implementation needs pg_table_is_visible(). - return any(t == table for t in self.get_table_names(conn, schema=schema)) + # As in SQLAlchemy 2.0, has_table() is also true for views. + if not self._is_v2plus: + return any(t == table for t in self.get_table_names(conn, schema=schema)) + return table in self._information_schema_relations(conn, schema, base_tables_only=False) def get_multi_columns(self, connection, schema, filter_names, scope, kind, **kw): _include_hidden = kw.get("include_hidden", False) diff --git a/test/test_introspection.py b/test/test_introspection.py index a45ac5f..d4eea0d 100644 --- a/test/test_introspection.py +++ b/test/test_introspection.py @@ -7,6 +7,7 @@ UniqueConstraint, CheckConstraint, text, + inspect, ) from sqlalchemy.types import Integer, String, Boolean import sqlalchemy.types as sqltypes @@ -166,3 +167,30 @@ def test_varchar(self): ] for t in types: self._test(t, sqltypes.VARCHAR) + + +class TableNamesTest(fixtures.TestBase): + __requires__ = ("sync_driver",) + + def setup_method(self): + with testing.db.begin() as conn: + conn.execute(text("CREATE TABLE names_base (id INT PRIMARY KEY)")) + conn.execute(text("CREATE VIEW names_view AS SELECT id FROM names_base")) + + def teardown_method(self, method): + with testing.db.begin() as conn: + conn.execute(text("DROP VIEW IF EXISTS names_view")) + conn.execute(text("DROP TABLE IF EXISTS names_base")) + + def test_get_table_names_excludes_views(self): + insp = inspect(testing.db) + table_names = insp.get_table_names() + assert "names_base" in table_names + assert "names_view" not in table_names + assert "names_view" in insp.get_view_names() + + def test_has_table_includes_views(self): + insp = inspect(testing.db) + assert insp.has_table("names_base") + assert insp.has_table("names_view") + assert not insp.has_table("names_absent") From 09a6e2cbb97fd5a3125fe2f847095d22b8658278 Mon Sep 17 00:00:00 2001 From: Amin Ghadersohi Date: Sat, 26 Sep 2026 23:31:17 +0000 Subject: [PATCH 2/2] Reference #310 in CHANGES.md --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f32ab25..97bff24 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,7 +7,7 @@ Unreleased (CRDB 26.3+ required for full compatibility.) - `get_table_names()` now returns base tables only; views were also listed and are reported by `get_view_names()`. `has_table()` still returns True for - views, as in SQLAlchemy 2.0. + views, as in SQLAlchemy 2.0 (#310). # Version 2.0.4 April 23, 2026