feat: REQ-002/REQ-010 implementation — assets.tag_ids column + GET /v1/assets admin catalog (BAT-012)

- REQ-002 scope redefinition: portfolios.tag_ids → assets.tag_ids UUID[] column with GIN index
- REQ-010 augmentation: GET /v1/assets redesigned as admin catalog endpoint (admin-only, no token_type branching)
- Asset tagging infrastructure: tag_ids column, archived flag synthesis for inactive tags, list_assets N+1 optimization
- DB migration 20260521_0023: assets.tag_ids UUID[] NOT NULL DEFAULT '{}', idx_assets_tag_ids_gin
- Test suite: 17 new test cases for assets.tag_ids + GET /v1/assets AC-9~12 (362 tests total)
- Documentation: runtime flows, release baseline, batch verification all updated
- BAT-012 added to REL-001 Included Batch and marked release-candidate

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 23:37:08 +09:00
co-authored by Claude Opus 4.7
parent d01039e98f
commit 9038f41010
19 changed files with 867 additions and 47 deletions
@@ -0,0 +1,63 @@
"""Add assets.tag_ids UUID[] column and GIN index (REQ-002 CHG-20260521-02)
Revision ID: 20260521_0023
Revises: 20260520_0022
Create Date: 2026-05-21 00:00:00
"""
from __future__ import annotations
import sqlalchemy as sa
from alembic import op
from sqlalchemy import inspect
revision = "20260521_0023"
down_revision = "20260520_0022"
branch_labels = None
depends_on = None
def _column_exists(inspector: sa.engine.Inspector, table_name: str, column_name: str) -> bool:
return any(col["name"] == column_name for col in inspector.get_columns(table_name))
def _index_exists(inspector: sa.engine.Inspector, table_name: str, index_name: str) -> bool:
return any(idx["name"] == index_name for idx in inspector.get_indexes(table_name))
def upgrade() -> None:
bind = op.get_bind()
inspector = inspect(bind)
if not _column_exists(inspector, "assets", "tag_ids"):
op.add_column(
"assets",
sa.Column(
"tag_ids",
sa.ARRAY(sa.UUID()),
nullable=False,
server_default=sa.text("'{}'::uuid[]"),
),
)
inspector = inspect(bind)
if _column_exists(inspector, "assets", "tag_ids") and not _index_exists(
inspector, "assets", "idx_assets_tag_ids_gin"
):
op.create_index(
"idx_assets_tag_ids_gin",
"assets",
["tag_ids"],
postgresql_using="gin",
)
def downgrade() -> None:
bind = op.get_bind()
inspector = inspect(bind)
if _index_exists(inspector, "assets", "idx_assets_tag_ids_gin"):
op.drop_index("idx_assets_tag_ids_gin", table_name="assets")
if _column_exists(inspector, "assets", "tag_ids"):
op.drop_column("assets", "tag_ids")