Conversation
…erate
The six sqlalchemy_celery_beat schedule tables are declared on the library's
own declarative base, so they are absent from the SQLModel.metadata each of
the three Alembic tracks passes as target_metadata. On a store where beat has
run, autogenerate reads them as six tables that used to exist and should be
dropped, and `make makemigrations` writes that revision rather than merely
reporting it, folding op.drop_table('celery_periodictask') into whatever
migration the developer was actually authoring.
Add a shared include_object filter, defined once beside the Celery code and
passed to both context.configure calls in all three env.py files, mirroring
how compare_type is already wired. The excluded names are derived from the
library's own metadata rather than written out, so a schedule type the library
adds is covered with no edit here, and a future SEP table whose name merely
begins with celery_ is not swallowed. The filter stays narrow deliberately:
autogenerate must still report a genuinely removed SEP table.
`make migrate` now drives the library's own bootstrap after the alembic loop,
through the same entry point sidecar/supervisord.conf already uses, so a
database it has finished with is one every SEP entry point can start against
without waiting out SEP.API_READINESS_TIMEOUT.
The tables stay the library's: no revision is added to any track.
There was a problem hiding this comment.
🟡 Changes recommended
The new migration bootstrap can wait forever when the configured beat store remains unavailable.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Adds a metadata-derived Alembic filter preventing library-owned Celery beat tables from being proposed for removal across all migration tracks.
Changes:
- Applies the shared filter to SEP, Tasks, and Inventory Alembic environments.
- Bootstraps beat tables after
make migrate. - Adds shared helpers, fixtures, and integration coverage.
File summaries
| File | Description |
|---|---|
app/core/celery/migrations.py |
Defines the beat-table filter. |
app/sep/migrations/env.py |
Enables filtering for SEP migrations. |
app/tasks/migrations/env.py |
Enables filtering for Tasks migrations. |
app/inventory/migrations/env.py |
Enables filtering for Inventory migrations. |
Makefile |
Runs beat bootstrap after migrations. |
tests/app/beat_autogenerate.py |
Adds shared autogenerate helpers. |
tests/app/core/celery/test_migrations.py |
Tests filter scope and names. |
tests/app/core/celery/test_bootstrap.py |
Tests Makefile bootstrap wiring. |
tests/app/sep/migrations/test_alembic_integration.py |
Covers SEP filtering and bootstrap order. |
tests/app/tasks/migrations/test_beat_tables_ignored.py |
Covers Tasks filtering. |
tests/app/inventory/migrations/test_beat_tables_ignored.py |
Covers Inventory filtering. |
tests/app/inventory/migrations/conftest.py |
Centralizes Inventory migration setup. |
tests/app/inventory/migrations/test_mandatory_pmm_origin.py |
Removes the relocated fixture. |
Review details
- Files reviewed: 13/13 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The two positions the decision does not read were renamed _object and _compare_to to silence ARG001. compare_type, the other autogenerate hook the same three env.py files import, keeps the imposed names and suppresses instead; Alembic spells these object_ and compare_to and passes all five positionally.
…tion BEAT_TABLES and table_names() move from test_bootstrap into the beat_autogenerate helper, so the per-track tests reuse them instead of importing a constant out of a test_ module. Each per-track test now asserts the schedule tables reached the store before asserting the sweep stays quiet about them; without it the negative passes whether or not they were created.
|
Record when
The repo's ruff conventions already grant this shape for the unused-argument codes — "the signature is imposed from outside" — but the grant is written for Worth settling once, either as a documented condition alongside the unused-argument grant or as a |
Coverage reportClick to see where and how coverage changed
This report was generated by python-coverage-comment-action |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Summary
The six
sqlalchemy_celery_beatschedule tables are declared on the library's owndeclarative base, so they never appear in the
SQLModel.metadatathat all threeAlembic tracks pass as
target_metadata. On a database where Celery beat has run,autogenerate therefore reads them as six tables that used to exist and should be
dropped.
make makemigrationsdoes not merely report that — it writes the revision,folding
op.drop_table('celery_periodictask')into whatever migration the developerwas actually authoring.
Two coupled halves:
A shared
include_objectautogenerate filter in a new moduleapp/core/celery/migrations.py, passed alongside the existingcompare_typetoboth
context.configurecalls in each ofapp/sep/migrations/env.py,app/tasks/migrations/env.pyandapp/inventory/migrations/env.py. The excludednames are derived from the library's own metadata rather than written out, so a
schedule type the library adds later is covered with no edit here, and a future SEP
table whose name merely begins with
celery_is not swallowed.The filter reads each table object's
.name.ModelBase.metadata.tablesis keyedby the schema-qualified name (
celery_schema.celery_periodictask, because themodels carry
__table_args__ = {"schema": "celery_schema"}and the librarytranslates it at connect time), while autogenerate reflects the bare name — so a
filter built from those keys would match nothing and silently exclude nothing.
It stays deliberately narrow: only a reflected object of type
tablewhose nameis one of the six is excluded. A genuinely removed SEP table is still reported.
make migratedrives the library's own bootstrap after the alembic loop, viapython -m app.core.celery.bootstrap— the same entry pointsidecar/supervisord.confalready uses. Nothing outside a container previouslycreated those tables, so a freshly migrated local database left every SEP entry
point waiting on tables only beat itself would create.
The tables stay the library's: no revision is added to any of the three tracks.
The module lives beside the Celery code rather than in
app/core/db/utils.py(wherecompare_typelives) because its only input is the beat library's metadata: a filtermeaningful only to the beat feature does not belong in a generic core utility module,
and
app/core/celery/__init__.pycarries nothing but its licence header, so the leafmodule is cheap to reach.
Import cost points the same way, though not at the migration path. Putting the filter
in
utils.pywould give that module a module-scopefrom sqlalchemy_celery_beat.session import ModelBase, which pulls in celery andkombu — measured at +175 modules on top of what
app.core.db.utilsimports today,which does not include celery. That cost is per process rather than per importing
file, and it is not saved on the migration path: all three
env.pyfiles importapp.core.celery.migrationsat module scope, and Alembic loadsenv.pybefore anyversion file runs, so the side-car's three migration one-shots pay it either way. What
the placement spares is every process that imports
app.core.db.utilswithout runningAlembic — the API and the workers.
Why the integration tests assert what they assert
The three per-track integration tests assert that no beat table is named in the
proposed operations, rather than that
alembic checkcomes back empty. Underpytest,
target_metadatais the process-wideSQLModel.metadataand every track'smodels are imported into it, so each track's sweep reports the other two tracks'
tables as missing —
alert_backupshows up on the tasks track, for instance. Thatpollution is absent when
alembic --name <track>runs for real and is unrelated tothe beat tables either way, so the narrower assertion is the one that means
something. It still fails if the filter is removed from any single
env.py.Tested
Automated, run locally:
include_objectunit tests, and the bootstrap suite — green. Removing
include_objectfromboth
context.configurecalls inapp/inventory/migrations/env.pymakestests/app/inventory/migrations/test_beat_tables_ignored.pyfail, so theassertion is exercising the filter rather than passing vacuously.
tests/app/sep/migrations/test_alembic_integration.pyandtests/app/inventory/migrations/test_mandatory_pmm_origin.py, which togethercover the twelve tests that consume the fixture moved into the new conftest.
Manual, run by hand on this branch:
make migratecreates all six beat tables.Manual smoke tests still to run:
make migrateon a checkout whose databases started empty, thenpython3 -m app.main --start-celery: the API becomes ready and beat startswithout logging
Starting Celery beat without a ready HTTP API. This is theonly acceptance criterion with no automated coverage — the tests above prove
the bootstrap creates the tables in the
migrateorder, but nothing exercisesthe startup path that consumes them.
(
CELERY__BEAT_DBURI="sqlite:///sep.db" python3 -m app.core.celery.bootstrap),confirm
alembic --name sep checkreportsNo new upgrade operations detected.Before this change the same state reports six
remove_tableoperations.alembic --name tasks checkandalembic --name inventory checkare likewiseclean after
make migrate.make makemigrationson a database where beat has run offers no revisioncontaining
op.drop_table('celery_*').Bundled fix
inventory_alembic_configmoved fromtests/app/inventory/migrations/test_mandatory_pmm_origin.pyinto a new
tests/app/inventory/migrations/conftest.py. The new Inventory-track testneeds the same fixture, and duplicating it is what the scaffolding-duplication check
blocks; promoting it also brings the Inventory track in line with the Tasks track
(
tests/app/tasks/migrations/conftest.py) and the SEP track, which already keep thisfixture in a conftest. The body is unchanged, and all twelve tests that consumed it
pass.
BEAT_TABLESandtable_names()moved out oftests/app/core/celery/test_bootstrap.pyintotests/app/beat_autogenerate.pyforthe same reason: the per-track tests need both, and the alternative was importing a
constant out of a
test_*.pymodule while a shared helper for exactly this materialalready existed one directory up.
table_names()is unchanged;BEAT_TABLESstays awritten-out literal so that
BEAT_TABLE_NAMES == BEAT_TABLESstill tests thederivation instead of restating it.
Known limitations
make migraterecipe line is asserted by parsing the target's recipe text; noautomated test executes the target, so a shell-level wiring fault (the line landing
under the wrong target, a variable not expanding, a swallowed non-zero exit) would
pass the suite. A test driving the real target needs isolated databases for all
three services plus the beat store. The target was run by hand on this branch and
creates all six tables.
bootstrap_beat_schemawaits for the beat store without a bound, andmake migrateis a new caller of it (
checkmigrationsdepends onmigrate, so both migrationworkflows inherit it). An unreachable beat store therefore hangs the command,
logging once per second, instead of failing it. No shipped profile reaches that
state: the development profile uses a local SQLite file that connecting creates,
and the
production_dockerand side-car profiles resolve the beat store to the samedatabase the alembic loop just connected to. Neither CI workflow is exposed —
python.yamlandrelease.ymlboth invokemake checkmigrationswithout settingCELERY__BEAT_DBURIorFASTAPI_ENV, so both run the development profile against aSQLite file. What does reach it is a hand-set
CELERY__BEAT_DBURInaming a databasethat is not running, or one that rejects the credentials.
_wait_for_storeretrieson
OperationalError, which covers a refused connection and an unresolvable host —and also a rejected password, since psycopg2 raises
OperationalErroronFATAL: password authentication failed. That last case is the one that can neverclear, so the retry is unbounded against a failure that will not resolve itself.
Bounding the wait for this caller alone would mean changing a module this branch
otherwise only calls, which was weighed and declined.
make migrate's alembic loop reports only its last iteration's exit status, soa failed
tasksorinventoryupgrade does not fail the target. That ispre-existing, but the bootstrap step this branch appends now runs after it, against
a store that may be only partly migrated.
checkmigrationstwo rules below alreadyuses the
ret=0; … || ret=1idiom that closes it.Checklist
Database migrations generated if models changed ((N/A for this change — no model changes, and the point of the change is that no revision adopts these tables)make makemigrations)User-facing changes documented (README, inline help, UI text)(N/A for this change — developer tooling only; deployments already create these tables)Configuration changes documented with examples(N/A for this change — no new or changed settings)