diff --git a/CHANGES.md b/CHANGES.md index f336ce8..2f445cb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ # Version 2.0.5 Unreleased +- Add SQLAlchemy 2.1 row security DDL and reflection support. # Version 2.0.4 April 23, 2026 diff --git a/README.md b/README.md index 91c2650..25eaedc 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ Use `pip` to install the latest release of this dialect. pip install sqlalchemy-cockroachdb ``` -NOTE: This version of the dialect requires SQLAlchemy 2.0.x. To work with +NOTE: This version of the dialect requires SQLAlchemy 2.1.x. To work with earlier versions of SQLAlchemy you'll need to install an earlier version of this dialect. diff --git a/setup.py b/setup.py index 75a4944..02d569f 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ }, packages=find_packages(include=["sqlalchemy_cockroachdb"]), include_package_data=True, - install_requires=["SQLAlchemy>=2.0.47,<2.1"], + install_requires=["SQLAlchemy>=2.1.0b3,<2.2"], zip_safe=False, entry_points={ "sqlalchemy.dialects": [ diff --git a/sqlalchemy_cockroachdb/ddl_compiler.py b/sqlalchemy_cockroachdb/ddl_compiler.py index 69fcc2b..9353d1f 100644 --- a/sqlalchemy_cockroachdb/ddl_compiler.py +++ b/sqlalchemy_cockroachdb/ddl_compiler.py @@ -14,3 +14,8 @@ def visit_computed_column(self, generated, **kw): return "AS (%s) STORED" % self.sql_compiler.process( generated.sqltext, include_table=False, literal_binds=True ) + + def _format_policy_role(self, role): + if role == "CURRENT_ROLE": + raise exc.CompileError("CockroachDB row security does not support CURRENT_ROLE") + return super()._format_policy_role(role) diff --git a/test-requirements.in b/test-requirements.in index f2d04f8..1bddfec 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -12,4 +12,4 @@ more-itertools psycopg psycopg2 pytest -sqlalchemy>=2.0.47,<2.1 +sqlalchemy @ git+https://github.com/Pedrexus/sqlalchemy.git@1bbe0d64d6017b346a5e6f7bf784ba7a3294662f diff --git a/test-requirements.txt b/test-requirements.txt index 8a4a7d9..cf16c93 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -8,8 +8,6 @@ exceptiongroup==1.3.1 # via pytest futures==3.0.5 # via -r test-requirements.in -greenlet==3.5.0 - # via sqlalchemy iniconfig==2.3.0 # via pytest mako==1.3.12 @@ -32,7 +30,7 @@ pygments==2.20.0 # via pytest pytest==9.0.3 # via -r test-requirements.in -sqlalchemy==2.0.49 +sqlalchemy @ git+https://github.com/Pedrexus/sqlalchemy.git@1bbe0d64d6017b346a5e6f7bf784ba7a3294662f # via # -r test-requirements.in # alembic diff --git a/test/test_row_security.py b/test/test_row_security.py new file mode 100644 index 0000000..73b6910 --- /dev/null +++ b/test/test_row_security.py @@ -0,0 +1,77 @@ +from sqlalchemy import Column +from sqlalchemy import exc +from sqlalchemy import inspect +from sqlalchemy import Integer +from sqlalchemy import MetaData +from sqlalchemy import Table +from sqlalchemy import testing +from sqlalchemy.dialects.postgresql import CreatePolicy +from sqlalchemy.dialects.postgresql import EnableRowLevelSecurity +from sqlalchemy.dialects.postgresql import ForceRowLevelSecurity +from sqlalchemy.dialects.postgresql import Policy +from sqlalchemy.testing import fixtures +from sqlalchemy.testing.assertions import AssertsCompiledSQL +from sqlalchemy.testing.assertions import eq_ +from sqlalchemy.testing.assertions import expect_raises_message + + +class RowSecurityCompileTest(fixtures.TestBase, AssertsCompiledSQL): + __dialect__ = "cockroachdb" + + def test_rejects_unsupported_current_role(self): + table = Table("item", MetaData(), Column("owner_id", Integer)) + policy = Policy("read", table, roles=("CURRENT_ROLE",)) + + with expect_raises_message( + exc.CompileError, + "CockroachDB row security does not support CURRENT_ROLE", + ): + self.assert_compile(CreatePolicy(policy), "") + + +class RowSecurityReflectionTest(fixtures.TestBase): + __only_on__ = "cockroachdb" + __requires__ = ("sync_driver",) + + @testing.provide_metadata + def test_postgresql_compatible_reflection(self): + table = Table( + "row_security_reflection", + self.metadata, + Column("id", Integer, primary_key=True), + Column("owner_id", Integer), + ) + with testing.db.begin() as connection: + table.create(connection) + connection.execute(EnableRowLevelSecurity(table)) + connection.execute(ForceRowLevelSecurity(table)) + connection.execute( + CreatePolicy( + Policy( + "read", + table, + command="SELECT", + using=table.c.owner_id == 7, + ) + ) + ) + + state = inspect(connection).get_row_security(table.name) + + eq_( + state, + { + "enabled": True, + "forced": True, + "policies": [ + { + "name": "read", + "command": "SELECT", + "roles": ["public"], + "using": "owner_id = 7:::INT8", + "check": None, + "permissive": True, + } + ], + }, + )