"""endpoint installed-software (SBOM) snapshot table. Adds endpointapplications: one row per installed application discovered on an endpoint, reported by the agent as {displayname, displayversion, publisher}. Stored as a per-asset snapshot (replaced on each report), the same way the services/ports inventories are kept as the newest list rather than history. appid is groundwork for a future link to the core applications catalog (the "Managed Applications" concept): a nullable FK with ON DELETE SET NULL so retiring a managed Application does not delete discovered rows. No discovery-to-catalog matcher exists yet; the column is laid down now so the association can be added without a second migration. Idempotent: creation checks the live schema first so a db.create_all() site (which already built the table) re-runs clean on strict MySQL 8. """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'endpoints0004apps' down_revision = 'endpoints0003cmdtarget' branch_labels = None depends_on = None def _has_table(table_name): from sqlalchemy import inspect return inspect(op.get_bind()).has_table(table_name) def upgrade(): if _has_table('endpointapplications'): return op.create_table( 'endpointapplications', # BigInteger, not Integer. This is a SNAPSHOT table: every check-in # deletes the endpoint's rows and reinserts them, so the surrogate key # burns through values at (endpoints x installed apps) per report and # never reuses them. A 32-bit key exhausts on MySQL - a few hundred # endpoints reporting a few hundred apps every 15 minutes reaches 2.1 # billion inside a couple of years, and the failure is an insert that # stops working forever. SQLite has no such ceiling, so no test would # have caught it. Changed while the table is still unreleased. sa.Column('endpointapplicationid', sa.BigInteger().with_variant(sa.Integer, 'sqlite'), nullable=False), sa.Column('assetid', sa.Integer(), nullable=False), sa.Column('displayname', sa.String(length=255), nullable=False), sa.Column('displayversion', sa.String(length=128), nullable=True), sa.Column('publisher', sa.String(length=255), nullable=True), sa.Column('appid', sa.Integer(), nullable=True), sa.Column('recordedat', sa.DateTime(), nullable=False), sa.PrimaryKeyConstraint('endpointapplicationid'), sa.ForeignKeyConstraint( ['assetid'], ['assets.assetid'], name='fk_endpointapplications_assetid', ondelete='CASCADE'), sa.ForeignKeyConstraint( ['appid'], ['applications.appid'], name='fk_endpointapplications_appid', ondelete='SET NULL'), ) op.create_index( 'idx_endpointapplications_assetid', 'endpointapplications', ['assetid']) op.create_index( 'idx_endpointapplications_appid', 'endpointapplications', ['appid']) op.create_index( 'idx_endpointapplications_displayname', 'endpointapplications', ['displayname']) op.create_index( 'idx_endpointapplications_assetname', 'endpointapplications', ['assetid', 'displayname']) def downgrade(): if not _has_table('endpointapplications'): return op.drop_index('idx_endpointapplications_assetname', table_name='endpointapplications') op.drop_index('idx_endpointapplications_displayname', table_name='endpointapplications') op.drop_index('idx_endpointapplications_appid', table_name='endpointapplications') op.drop_index('idx_endpointapplications_assetid', table_name='endpointapplications') op.drop_table('endpointapplications')