Repository navigation
Require SQLAlchemy-only database connections and Alembic-only schema changes - #852
Merged
Merged
Conversation
anth-volk
added this pull request to stack #755
October 20, 2026 13:46
anth-volk
force-pushed
the
sqlalchemy-database-rule
branch
2 times, most recently
from
October 20, 2026 16:38
ec05948 to
7f24696
Compare
Register sim.boundaries.sqlalchemy-and-alembic-only. Code connects to databases only through SQLAlchemy engines and connections, never by importing a database driver directly, and runtime code never changes the schema, which changes only through Alembic migrations. The guard in the entry project's tests parses every Python file under projects/*/src, libs/*/src and .github/scripts with ast. It rejects database driver imports, alembic imports outside a migrations directory, create_all and drop_all calls, create() and drop() calls on objects it can identify as tables, SQLAlchemy DDL statements, and DDL text in string literals other than docstrings. The DDL text check reuses the Stage 12 DDL_PATTERN, now in database_access_support.py, so it also covers the gateway and every library. test_stage12_schema_authority.py imports the pattern from there. The proof's bad fixture is the psycopg connection code of the Stage 12 adapter added in 08ebe1c, plus an illustrative metadata.create_all call. docs/engineering/skills/database-access.md explains the rule, which schema each migration chain owns, and that non-relational stores are out of scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
force-pushed
the
sqlalchemy-database-rule
branch
from
October 20, 2026 18:31
7f24696 to
07bc635
Compare
anth-volk
marked this pull request as ready for review
October 20, 2026 22:05
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #851
Summary
Registers
sim.boundaries.sqlalchemy-and-alembic-only, an enforced mechanical rule, with anastguard, a proof and a skill doc. Stacked on #807.Statement: Code connects to databases only through SQLAlchemy engines and connections, never by importing a database driver directly, and runtime code never changes the schema, which changes only through Alembic migrations.
The wording doesn't say which repository's migrations own the schema, so it holds both now and after #847:
sim.stage12.no-migration-chain, unchanged here).migrations/chain owns thesimulationschema.How the guard finds violations
projects/policyengine-simulation-entry/tests/test_database_access_boundaries.pyruns in the entry project's leg of theTestjob. It parses every Python file underprojects/*/src(including the executor'ssrc/modal),libs/*/srcand.github/scripts: 154 files on this branch. The checks live intests/database_access_support.py.It fails on:
psycopg,psycopg2,pymysql,MySQLdb,mysql.connector,sqlite3,asyncpg,pg8000,aiosqliteandaiomysql, including submodules andfrom mysql import connector. A driver named in a SQLAlchemy URL, such aspostgresql+psycopg, is not an import.alembicimports outside amigrationsdirectory. None exist today.create_allanddrop_all.create()anddrop()on an object the guard can identify as a table: aTable(...)call; a name or attribute bound to one, or annotated asTable, in any scanned file; a__table__attribute; or an item of.tables. Tables usually come from a shared module, so a table bound in one file is recognised in all of them. Acreate()ordrop()call that passescheckfirst=also counts, since only SQLAlchemy's methods take it. The pandasframe.drop(...)instage12_artifacts.pyis not flagged.CreateTable,DropTable,CreateSchema,DDLand the rest), imported fromsqlalchemyor used as an attribute. This goes beyond the issue's list: without it,connection.execute(CreateTable(table))would pass.DDL_PATTERN. Docstrings and comments don't count.migrationsdirectories: files under one may importalembicand change the schema, but driver imports are still rejected there.A second test fails if any of the named directories stops contributing files to the scan.
What it can't see: imports through
importlibor__import__, a table reached through some other local variable, SQL text assembled from separate literals, and shell scripts and SQL files. Today no shell script calls a database client, and the repository has no SQL files.Widened DDL and Alembic coverage
DDL_PATTERN)src(sim.stage12.no-schema-ddl, whole files)policyengine-fastapi,policyengine-simulation-contract,policyengine-simulation-observabilityand.github/scripts(string literals only)create_all/drop_allsim.stage12.persistence-typed-dml-only) andcreate_all(in the three directories aboveTable.create/Table.drop, SQLAlchemy DDL statementssim.stage12.no-migration-chain)import alembicin every source directory outsidemigrationsDDL_PATTERNmoved fromtest_stage12_schema_authority.pyintodatabase_access_support.py. The Stage 12 test imports it from there and still matches whole files, sosim.stage12.no-schema-ddlis unchanged. There is still one copy of the regex in this repository.Interaction with #847
#847 adds its own schema-change check:
SCHEMA_CHANGEandschema_change_violations()inlibs/policyengine-simulation-persistence/tests/ownership_checks.py. Its regex also matchesINDEX. That is a second regex covering the same ground, in a different uv project. The two should be combined when #847 is rebased onto this stack.git archivecopy ofapiv2/1a-report-tables(Add the simulation report tables #849,0391726b). It scanned 163 files, including 6 inlibs/policyengine-simulation-persistence/src, with 0 findings.alembicimports are in the root-levelmigrations/and in the persistence library'stests/, neither of which this guard scans. Under amigrationsdirectory they would be allowed anyway.test_stage12_schema_authority.py: Add the simulation schema's persistence library, migration chain and pre-deploy migration job #847 removes the migration-chain test; this PR changes the file's imports.rules/registry.toml,AGENTS.md,docs/generated/rules.mdandchangelog_entry.yaml. Rerunpython -m scripts.rules renderafter resolving them.Out of scope
Non-relational stores: Modal Dicts, object storage such as the Google Cloud Storage artifact and cache buckets, and in-process or on-disk caches.
libs/policyengine-fastapi/src/policyengine_fastapi/database.pybuilds an SQLite engine with SQLAlchemycreate_engine. Nothing in the repository imports it. It passes the rule and is unchanged.Proof
rules/fixtures/sim.boundaries.sqlalchemy-and-alembic-only/:policyengine-simulation-contractdidimport psycopgandfrom psycopg.rows import dict_row, and connected withpsycopg.connectand cursors, until 7e7fa00 replaced it with a SQLAlchemy engine. The fixture adds a runtimemetadata.create_all(engine). The fixture's docstring says thecreate_allhalf is illustrative: no commit has called it.create_enginewith thepostgresql+psycopgdriver name and a typedselect. It also has a docstring that mentionsCREATE TABLE, and a pandasframe.drop(columns=...).test_rule_proof_sqlalchemy_and_alembic_onlyasserts the exact findings: the two psycopg imports and thecreate_allcall for the bad fixture, and none for the good one.test_database_access_support.pycovers the scanner's edge cases (31 cases). Its file name doesn't match the registry's guard-test patterns, so the cases aren't rule nodes.Builds on
sim.stage12.no-schema-ddl: itsDDL_PATTERNis now shared.sim.stage12.no-migration-chain: cited for who migrates the Stage 12 tables today; not edited.sim.stage12.persistence-typed-dml-onlyandsim.stage12.deployment-uses-api-schema: the Stage 12 library's typed-DML rules.Docs
docs/engineering/skills/database-access.md, the rule's rationale. No existing skill doc covered database access, and the Stage 12 persistence README goes away with that library. It cites the rule and the related rules, says which chain owns which schema today and after Add the simulation schema's persistence library, migration chain and pre-deploy migration job #847, and says non-relational stores are out of scope.AGENTS.md's skills list and the skills README point to it.docs/generated/rules.mdand theAGENTS.mdRules block are regenerated.changelog_entry.yaml.Testing
uvx ruff@0.15.22 check --extend-select RUF100 .: all checks passed.uvx ruff@0.15.22 format --check projects/*/src libs/*/src(CI's scope): 153 files already formatted. The new tests and fixtures are formatted too.python -m scripts.rules validate: passed.GITHUB_BASE_REF=microdata-weight-rules python -m scripts.rules guard --base-ref origin/microdata-weight-rules: passed.python scripts/run_quality_guards.py:rules-registryandci-scriptspassed.python -m unittest discover -s scripts/rules/tests -t .: 115 tests OK.uv sync --frozen --extra test --python 3.13:uv run pytest tests/gave 652 passed. These include the 3 guard tests, the 31 scanner cases and the 4 Stage 12 schema-authority tests.apiv2/1a-report-tablesarchive: the guard and scanner tests gave 34 passed. Conftest loading was off, because that branch's conftest needs its own dependencies.🤖 Generated with Claude Code