"""per-project Building / Floor / Sector taxonomy (CR-005) The location taxonomy differs per project — on Micron, floors within B100 behave like separate buildings — so it is configured once per SOP instead of hard-coded. CR-018 rolls cost up by these values, which is why the table stores CODES (`code`, `path`) beside the display `name`: a rollup keyed on a label breaks the day somebody fixes a typo in it. `active` rather than a delete. Deactivating hides a value from new work packages while every package already referencing it still resolves its label, which is the same rule CR-002 and CR-016 apply to fields. Additive only: a new table, no change to any existing one, so nothing to backfill and nothing to migrate. Revision ID: e2a4c7d91b30 Revises: a7c31f9e5b02 Create Date: 2026-08-16 09:41:02.118307 """ from alembic import op import sqlalchemy as sa # revision identifiers, used by Alembic. revision = 'e2a4c7d91b30' down_revision = 'a7c31f9e5b02' branch_labels = None depends_on = None def upgrade() -> None: op.create_table( 'location_nodes', sa.Column('id', sa.String(length=40), nullable=False), sa.Column('project_id', sa.String(length=40), nullable=False), sa.Column('parent_id', sa.String(length=40), nullable=True), sa.Column('level', sa.String(length=20), nullable=False, server_default='building'), sa.Column('code', sa.String(length=60), nullable=False, server_default=''), sa.Column('path', sa.String(length=200), nullable=False, server_default=''), sa.Column('name', sa.String(length=200), nullable=False, server_default=''), sa.Column('active', sa.Boolean(), nullable=False, server_default=sa.true()), sa.Column('sort', sa.Integer(), nullable=False, server_default='0'), sa.Column('created_by', sa.String(length=200), nullable=False, server_default=''), sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), sa.ForeignKeyConstraint(['project_id'], ['projects.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id'), # One row per (project, path). This is what makes a re-import report a # duplicate instead of quietly creating a second B100/L2/1P. sa.UniqueConstraint('project_id', 'path', name='uq_location_path'), ) op.create_index(op.f('ix_location_nodes_project_id'), 'location_nodes', ['project_id'], unique=False) op.create_index(op.f('ix_location_nodes_parent_id'), 'location_nodes', ['parent_id'], unique=False) op.create_index(op.f('ix_location_nodes_path'), 'location_nodes', ['path'], unique=False) def downgrade() -> None: op.drop_index(op.f('ix_location_nodes_path'), table_name='location_nodes') op.drop_index(op.f('ix_location_nodes_parent_id'), table_name='location_nodes') op.drop_index(op.f('ix_location_nodes_project_id'), table_name='location_nodes') op.drop_table('location_nodes')