"""backups plugin baseline (real create). Built after the ADR-008 ownership cutover, so unlike the ten cutover plugins whose 0001 is a stamp-only anchor, this baseline genuinely CREATES the table. The core chain never knew about backuprevisions, so this per-plugin chain is its sole authoritative creator. Emits explicit Alembic ops rather than using the create_plugin_tables helper: the helper builds a per-plugin MetaData filtered to the plugin's own tables, so the foreign key to the core assets table cannot resolve at CreateTable-compile time (NoReferencedTableError). Same shape autogenerate produces. Tables inherit the connection's default charset, matching how the core chain creates its own. """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'backups0001baseline' down_revision = None branch_labels = None depends_on = None def upgrade(): op.create_table( 'backuprevisions', sa.Column('backuprevisionid', sa.Integer(), nullable=False), sa.Column('assetid', sa.Integer(), nullable=False), sa.Column('backupkind', sa.String(length=50), nullable=False), sa.Column('storagebackend', sa.String(length=20), nullable=False, server_default='shopdb'), sa.Column('contenthash', sa.String(length=64), nullable=False), sa.Column('payloadjson', sa.Text(length=16777215), nullable=True), sa.Column('sharepath', sa.String(length=500), nullable=True), sa.Column('sourcefilename', sa.String(length=255), nullable=True), sa.Column('bytesize', sa.Integer(), nullable=True), sa.Column('sourcehostname', sa.String(length=255), nullable=True), sa.Column('collectedat', sa.DateTime(), nullable=True), sa.Column('createdat', sa.DateTime(), nullable=False), sa.ForeignKeyConstraint(['assetid'], ['assets.assetid'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('backuprevisionid'), ) op.create_index('ixbackuprevisionsassetid', 'backuprevisions', ['assetid']) op.create_index('ixbackuprevisionsbackupkind', 'backuprevisions', ['backupkind']) op.create_index('ixbackuprevisionscontenthash', 'backuprevisions', ['contenthash']) # The dedup read path is "latest revision for this asset+kind", so the # composite index is the one that actually gets used on every collector post. op.create_index('ixbackuprevisionsassetkind', 'backuprevisions', ['assetid', 'backupkind']) def downgrade(): op.drop_index('ixbackuprevisionsassetkind', table_name='backuprevisions') op.drop_index('ixbackuprevisionscontenthash', table_name='backuprevisions') op.drop_index('ixbackuprevisionsbackupkind', table_name='backuprevisions') op.drop_index('ixbackuprevisionsassetid', table_name='backuprevisions') op.drop_table('backuprevisions')