Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Unreleased
- include identity column info (#297), and
- avoid parse error when reflecting ENUMs (#303).
(CRDB 26.3+ required for full compatibility.)
- `get_table_names()` and `has_table()` now use the upstream PostgreSQL
implementations. `get_table_names()` 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 (#310).

# Version 2.0.4
April 23, 2026
Expand Down
6 changes: 3 additions & 3 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ distlib==0.4.3
# via virtualenv
docutils==0.23
# via readme-renderer
filelock==4.0.1
filelock==4.0.4
# via
# python-discovery
# tox
Expand Down Expand Up @@ -56,7 +56,7 @@ packaging==26.3
# pyproject-api
# tox
# twine
platformdirs==4.11.12
platformdirs==4.12.0
# via
# tox
# virtualenv
Expand Down Expand Up @@ -95,7 +95,7 @@ urllib3==2.8.0
# id
# requests
# twine
virtualenv==21.11.1
virtualenv==21.13.0
# via tox
zipp==4.1.0
# via importlib-metadata
53 changes: 34 additions & 19 deletions sqlalchemy_cockroachdb/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,25 +120,15 @@ def _get_server_version_info(self, conn):
# used.
return (12, 0, 0)

def get_table_names(self, conn, schema=None, **kw):
# Upstream implementation needs correlated subqueries.

if not self._is_v2plus:
# v1.1 or earlier.
return [row.Table for row in conn.execute(text("SHOW TABLES"))]

# v2.0+ have a good information schema. Use it.
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},
)
]

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))
def get_table_names(self, connection, schema=None, **kw):
table_names = super().get_table_names(connection, schema=schema, **kw)
if schema is None:
for k in self.multi_entries_to_ignore:
try:
table_names.remove(k[1])
except ValueError:
pass
return table_names

def get_multi_columns(self, connection, schema, filter_names, scope, kind, **kw):
_include_hidden = kw.get("include_hidden", False)
Expand Down Expand Up @@ -259,6 +249,31 @@ def get_multi_columns(self, connection, schema, filter_names, scope, kind, **kw)
to_return.append((table, columns))
return to_return

def get_multi_foreign_keys(
self,
connection,
schema,
filter_names,
scope,
kind,
postgresql_ignore_search_path=False,
**kw,
):
result = super().get_multi_foreign_keys(
connection,
schema,
filter_names,
scope,
kind,
postgresql_ignore_search_path=postgresql_ignore_search_path,
**kw,
)
if schema is None:
result = dict(result)
for k in self.multi_entries_to_ignore:
result.pop(k, None)
return result

def get_indexes(self, conn, table_name, schema=None, **kw):
if self._is_v192plus:
indexes = super().get_indexes(conn, table_name, schema, **kw)
Expand Down
8 changes: 4 additions & 4 deletions sqlalchemy_cockroachdb/requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,6 @@ class Requirements(SuiteRequirementsSQLA, SuiteRequirementsAlembic):
emulated_lastrowid = exclusions.open()
dbapi_lastrowid = exclusions.open()
views = exclusions.open()
schemas = exclusions.skip_if(
lambda config: not config.db.dialect._is_v202plus,
"versions before 20.2 do not suport schemas",
)
implicit_default_schema = exclusions.skip_if(
lambda config: not config.db.dialect._is_v202plus,
"versions before 20.2 do not suport schemas",
Expand Down Expand Up @@ -160,6 +156,10 @@ class Requirements(SuiteRequirementsSQLA, SuiteRequirementsAlembic):
fk_onupdate = exclusions.closed()
fk_onupdate_restrict = exclusions.closed()

@property
def schemas(self):
return exclusions.open()

@property
def sync_driver(self):
return exclusions.only_if(
Expand Down
64 changes: 64 additions & 0 deletions test/test_introspection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
UniqueConstraint,
CheckConstraint,
text,
inspect,
)
from sqlalchemy.types import Integer, String, Boolean
import sqlalchemy.types as sqltypes
from sqlalchemy.testing import fixtures
from sqlalchemy.dialects.postgresql import INET
from sqlalchemy.dialects.postgresql import INTERVAL
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.dialects.postgresql.base import PGDialect
from unittest import mock

meta = MetaData()

Expand Down Expand Up @@ -166,3 +169,64 @@ 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"))
conn.execute(
text(
"CREATE TABLE names_ref (id INT PRIMARY KEY, "
"base_id INT REFERENCES names_base (id))"
)
)

def teardown_method(self, method):
with testing.db.begin() as conn:
conn.execute(text("DROP TABLE IF EXISTS names_ref"))
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")

@testing.requires.schemas
def test_get_table_names_uses_requested_schema(self):
schema = testing.config.test_schema
with testing.db.begin() as conn:
conn.execute(text(f"CREATE TABLE {schema}.names_other (id INT PRIMARY KEY)"))
try:
insp = inspect(testing.db)
table_names = insp.get_table_names(schema=schema)
assert "names_other" in table_names
assert "names_base" not in table_names
assert "names_other" not in insp.get_table_names()
finally:
with testing.db.begin() as conn:
conn.execute(text(f"DROP TABLE IF EXISTS {schema}.names_other"))

def test_get_foreign_keys_passes_ignore_search_path(self):
insp = inspect(testing.db)
(fk,) = insp.get_foreign_keys("names_ref")
assert fk["referred_table"] == "names_base"
with mock.patch.object(
PGDialect, "get_multi_foreign_keys", return_value={}
) as upstream:
testing.db.dialect.get_multi_foreign_keys(
None, None, None, None, None, postgresql_ignore_search_path=True
)
assert upstream.call_args.kwargs["postgresql_ignore_search_path"] is True
21 changes: 13 additions & 8 deletions test/test_suite_sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
from sqlalchemy.testing.suite import (
ComponentReflectionTest as _ComponentReflectionTest,
)
# (unused: entire class overwritten below)
# from sqlalchemy.testing.suite import (
# ComputedReflectionTest as _ComputedReflectionTest,
# )
from sqlalchemy.testing.suite import HasIndexTest as _HasIndexTest
from sqlalchemy.testing.suite import HasTableTest as _HasTableTest
from sqlalchemy.testing.suite import IntegerTest as _IntegerTest
from sqlalchemy.testing.suite import InsertBehaviorTest as _InsertBehaviorTest
from sqlalchemy.testing.suite import IsolationLevelTest as _IsolationLevelTest
Expand Down Expand Up @@ -205,7 +208,7 @@ def test_get_view_names(self):
# FWIW, insp.get_view_names() does still work IRL
pass

@testing.combinations(True, False, argnames="use_schema")
@testing.combinations(False, argnames="use_schema")
@testing.combinations((True, testing.requires.views), False, argnames="views")
def test_metadata(self, connection, use_schema, views):
if not (config.db.dialect.driver == "asyncpg" and not config.db.dialect._is_v231plus):
Expand All @@ -217,6 +220,14 @@ def test_not_existing_table(self):
pass


class ComputedReflectionTest():
# expected STORED COMPUTED COLUMN expression to have type int,
# but 'normal / 42' has type decimal
@skip("cockroachdb")
def test_everything(self):
pass


class HasIndexTest(_HasIndexTest):
@skip("cockroachdb")
def test_has_index(self):
Expand All @@ -226,12 +237,6 @@ def test_has_index(self):
pass


class HasTableTest(_HasTableTest):
@skip("cockroachdb")
def test_has_table_cache(self):
pass


class InsertBehaviorTest(_InsertBehaviorTest):
@skip("cockroachdb")
def test_no_results_for_non_returning_insert(self):
Expand Down
Loading