From 759933b0087fa794b2728e3bcfe831d5e0882410 Mon Sep 17 00:00:00 2001 From: Pedro CVLAB Date: Fri, 14 Aug 2026 19:28:37 +0900 Subject: [PATCH 1/2] Support SQLAlchemy row security --- CHANGES.md | 1 + README.md | 15 +++- setup.py | 2 +- sqlalchemy_cockroachdb/ddl_compiler.py | 5 ++ test-requirements.in | 2 +- test/test_row_security.py | 97 ++++++++++++++++++++++++++ 6 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 test/test_row_security.py 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..896a538 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. @@ -89,6 +89,19 @@ To connect using psycopg for async operation, see [README.psycopg.md](README.psycopg.md) +## Row level security + +SQLAlchemy 2.1 policy DDL and reflection work through the dialect's PostgreSQL +inheritance and CockroachDB's `pg_catalog` compatibility. Row level security +requires CockroachDB 25.2 or newer. + +Policy declaration, DDL execution, and inspection therefore use the same +SQLAlchemy API on PostgreSQL and CockroachDB. Database behavior still differs. +CockroachDB does not support subqueries in policy expressions, and applications +should review its documented `ON CONFLICT DO NOTHING` policy behavior before +using that statement for security-sensitive inserts. CockroachDB also does not +accept PostgreSQL's `CURRENT_ROLE` policy target. + ## Changelog See [CHANGES.md](CHANGES.md) diff --git a/setup.py b/setup.py index 75a4944..4ce19bf 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.0b4,<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..a2ef7e7 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>=2.1.0b4,<2.2 diff --git a/test/test_row_security.py b/test/test_row_security.py new file mode 100644 index 0000000..60199a7 --- /dev/null +++ b/test/test_row_security.py @@ -0,0 +1,97 @@ +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_inherits_postgresql_policy_compiler(self): + table = Table( + "item", + MetaData(), + Column("owner_id", Integer), + schema="app", + ) + policy = Policy( + "read", + table, + command="SELECT", + using=table.c.owner_id == 7, + ) + + self.assert_compile( + CreatePolicy(policy), + "CREATE POLICY read ON app.item FOR SELECT TO PUBLIC " "USING (owner_id = 7)", + literal_binds=True, + ) + + 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, + } + ], + }, + ) From ad8061cd0eaef3d36c9354b8e6916af937b0462f Mon Sep 17 00:00:00 2001 From: Pedro CVLAB Date: Mon, 17 Aug 2026 15:06:39 +0900 Subject: [PATCH 2/2] Tighten row security compatibility checks --- README.md | 13 ------------- setup.py | 2 +- test-requirements.in | 2 +- test-requirements.txt | 4 +--- test/test_row_security.py | 20 -------------------- 5 files changed, 3 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 896a538..25eaedc 100644 --- a/README.md +++ b/README.md @@ -89,19 +89,6 @@ To connect using psycopg for async operation, see [README.psycopg.md](README.psycopg.md) -## Row level security - -SQLAlchemy 2.1 policy DDL and reflection work through the dialect's PostgreSQL -inheritance and CockroachDB's `pg_catalog` compatibility. Row level security -requires CockroachDB 25.2 or newer. - -Policy declaration, DDL execution, and inspection therefore use the same -SQLAlchemy API on PostgreSQL and CockroachDB. Database behavior still differs. -CockroachDB does not support subqueries in policy expressions, and applications -should review its documented `ON CONFLICT DO NOTHING` policy behavior before -using that statement for security-sensitive inserts. CockroachDB also does not -accept PostgreSQL's `CURRENT_ROLE` policy target. - ## Changelog See [CHANGES.md](CHANGES.md) diff --git a/setup.py b/setup.py index 4ce19bf..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.1.0b4,<2.2"], + install_requires=["SQLAlchemy>=2.1.0b3,<2.2"], zip_safe=False, entry_points={ "sqlalchemy.dialects": [ diff --git a/test-requirements.in b/test-requirements.in index a2ef7e7..1bddfec 100644 --- a/test-requirements.in +++ b/test-requirements.in @@ -12,4 +12,4 @@ more-itertools psycopg psycopg2 pytest -sqlalchemy>=2.1.0b4,<2.2 +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 index 60199a7..73b6910 100644 --- a/test/test_row_security.py +++ b/test/test_row_security.py @@ -18,26 +18,6 @@ class RowSecurityCompileTest(fixtures.TestBase, AssertsCompiledSQL): __dialect__ = "cockroachdb" - def test_inherits_postgresql_policy_compiler(self): - table = Table( - "item", - MetaData(), - Column("owner_id", Integer), - schema="app", - ) - policy = Policy( - "read", - table, - command="SELECT", - using=table.c.owner_id == 7, - ) - - self.assert_compile( - CreatePolicy(policy), - "CREATE POLICY read ON app.item FOR SELECT TO PUBLIC " "USING (owner_id = 7)", - literal_binds=True, - ) - def test_rejects_unsupported_current_role(self): table = Table("item", MetaData(), Column("owner_id", Integer)) policy = Policy("read", table, roles=("CURRENT_ROLE",))