Source code for codegen_database.ext.cron.compare
"""Alembic comparator ops and diff logic for pg_cron jobs."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING
from alembic.operations.ops import MigrateOperation
from sqlalchemy import text
from codegen_database.ext.cron.base import CronJob
if TYPE_CHECKING:
from sqlalchemy import Connection
from codegen_database.ext.cron.base import CronJobs
_FETCH_JOBS_SQL = "SELECT jobname, schedule, command FROM cron.job"
[docs]
@dataclass
class ScheduleCronJobOp(MigrateOperation):
"""Alembic operation: create or update a pg_cron job."""
job: CronJob
[docs]
def to_sql(self) -> list[str]:
"""Return the SQL statement for this operation."""
return [self.job.to_sql_schedule()]
[docs]
def reverse(self) -> ScheduleCronJobOp:
"""Return a no-op downgrade: jobs are never dropped automatically.
``cron.schedule`` is an upsert so re-running it on downgrade
is always safe. Users who want to remove a job on downgrade
should add an explicit :class:`UnscheduleCronJobOp` call.
"""
return self
[docs]
@dataclass
class UnscheduleCronJobOp(MigrateOperation):
"""Alembic operation: remove a pg_cron job.
This op is available for manual migration authoring.
The autogenerate comparator never emits it automatically to
avoid accidentally removing externally-managed jobs.
"""
name: str
[docs]
def to_sql(self) -> list[str]:
"""Return the SQL statement for this operation."""
return [
CronJob(name=self.name, schedule="", command="").to_sql_unschedule()
]
def _pg_cron_installed(connection: Connection) -> bool:
"""Return ``True`` if pg_cron's schema exists in the database.
Args:
connection: Active database connection.
Returns:
``True`` when the ``cron`` schema is present.
"""
row = connection.execute(
text("SELECT 1 FROM pg_namespace WHERE nspname = 'cron'")
).scalar()
return row is not None
def _fetch_current_jobs(connection: Connection) -> dict[str, dict[str, str]]:
"""Return current pg_cron jobs keyed by job name.
Args:
connection: Active database connection.
Returns:
Mapping from ``jobname`` to a dict with ``schedule`` and
``command`` keys.
"""
rows = connection.execute(text(_FETCH_JOBS_SQL)).fetchall()
return {
row.jobname: {
"schedule": row.schedule,
"command": row.command,
}
for row in rows
}
[docs]
def compare_cron_jobs(
connection: Connection,
desired: CronJobs,
) -> list[ScheduleCronJobOp]:
"""Diff desired cron jobs against the current database state.
Only emits :class:`ScheduleCronJobOp` (create or update) — jobs
are never dropped automatically. Use :class:`UnscheduleCronJobOp`
in a hand-written migration to remove a job.
Args:
connection: Active database connection.
desired: The :class:`~codegen_database.ext.cron.base.CronJobs`
holder read from metadata.
Returns:
List of :class:`ScheduleCronJobOp` operations to bring
declared jobs into sync with the database.
"""
if not _pg_cron_installed(connection):
return []
current = _fetch_current_jobs(connection)
ops: list[ScheduleCronJobOp] = []
for job in desired.jobs:
cur = current.get(job.name)
if (
cur is None
or cur["schedule"] != job.schedule
or cur["command"] != job.command
):
ops.append(ScheduleCronJobOp(job=job))
return ops