"""Global product/price schema for v0.1. No user or workspace columns. Revision ID: 0002_domain_schema Revises: 0001_deploy_skeleton Create Date: 2026-10-03 """ from collections.abc import Sequence import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql revision: str = "0002_domain_schema" down_revision: str | None = "0001_deploy_skeleton" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: op.create_table( "attribute_definitions", sa.Column("key", sa.String(128), primary_key=True), sa.Column("value_type", sa.String(32), nullable=False), sa.Column("canonical_unit", sa.String(32)), sa.Column("minimum", sa.Numeric(12, 4)), sa.Column("maximum", sa.Numeric(12, 4)), sa.Column("description", sa.Text()), sa.CheckConstraint( "value_type IN ('number', 'string', 'boolean', 'string_list')", name="ck_attribute_definitions_value_type", ), ) op.create_table( "sources", sa.Column("id", sa.String(64), primary_key=True), sa.Column("name", sa.String(128), nullable=False), sa.Column("adapter_type", sa.String(64), nullable=False), ) op.create_table( "scoring_models", sa.Column("version", sa.String(64), primary_key=True), sa.Column("algorithm_id", sa.String(128), nullable=False), sa.Column("definition", postgresql.JSONB, nullable=False), sa.Column("definition_hash", sa.String(64), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), ) op.create_table( "product_profiles", sa.Column("id", sa.String(128), primary_key=True), sa.Column("version", sa.Integer(), primary_key=True), sa.Column("name", sa.String(256), nullable=False), sa.Column("category", sa.String(128), nullable=False), sa.Column("scoring_version", sa.String(64), sa.ForeignKey("scoring_models.version"), nullable=False), sa.Column("definition", postgresql.JSONB, nullable=False), sa.Column("definition_hash", sa.String(64), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.CheckConstraint("version > 0", name="ck_product_profiles_version"), ) op.create_table( "products", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("category", sa.String(128), nullable=False), sa.Column("brand", sa.String(128), nullable=False), sa.Column("name", sa.String(256), nullable=False), sa.Column("model", sa.String(128)), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), ) op.create_table( "product_variants", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("product_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("products.id"), nullable=False), sa.Column("name", sa.String(256), nullable=False), sa.Column("manufacturer_sku", sa.String(128)), sa.Column("identifiers", postgresql.JSONB, nullable=False, server_default=sa.text("'{}'::jsonb")), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), ) op.create_index("ix_product_variants_product_id", "product_variants", ["product_id"]) op.create_table( "variant_attributes", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("variant_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("product_variants.id"), nullable=False), sa.Column("attribute_key", sa.String(128), sa.ForeignKey("attribute_definitions.key"), nullable=False), sa.Column("source_id", sa.String(64), sa.ForeignKey("sources.id"), nullable=False), sa.Column("raw_value", postgresql.JSONB, nullable=False), sa.Column("normalized_value", postgresql.JSONB), sa.Column("unit", sa.String(32)), sa.Column("normalization_status", sa.String(32), nullable=False), sa.Column("normalizer_version", sa.String(64), nullable=False), sa.Column("confidence", sa.Numeric(4, 3), nullable=False), sa.Column("source_reference", sa.String(256), nullable=False), sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False), sa.Column("ingested_at", sa.DateTime(timezone=True), nullable=False), sa.Column("normalization_error", sa.Text()), sa.CheckConstraint("confidence >= 0 AND confidence <= 1", name="ck_variant_attributes_confidence"), sa.CheckConstraint( "normalization_status IN ('normalized', 'unknown')", name="ck_variant_attributes_status", ), ) op.execute( "CREATE INDEX ix_variant_attributes_lookup ON variant_attributes " "(variant_id, attribute_key, observed_at DESC, ingested_at DESC, id DESC)" ) op.create_table( "offers", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("variant_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("product_variants.id"), nullable=False), sa.Column("source_id", sa.String(64), sa.ForeignKey("sources.id"), nullable=False), sa.Column("external_id", sa.String(256), nullable=False), sa.Column("seller", sa.String(256), nullable=False), sa.Column("url", sa.Text()), sa.Column("market", sa.String(16), nullable=False), sa.Column("currency", sa.String(3), nullable=False), sa.Column("condition", sa.String(32), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.UniqueConstraint("source_id", "external_id", name="uq_offers_source_external"), sa.CheckConstraint("currency ~ '^[A-Z]{3}$'", name="ck_offers_currency"), sa.CheckConstraint("condition IN ('new', 'refurbished', 'used')", name="ck_offers_condition"), ) op.create_index("ix_offers_variant_id", "offers", ["variant_id"]) op.create_table( "price_observations", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("offer_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("offers.id"), nullable=False), sa.Column("source_id", sa.String(64), sa.ForeignKey("sources.id"), nullable=False), sa.Column("external_observation_id", sa.String(256), nullable=False), sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False), sa.Column("ingested_at", sa.DateTime(timezone=True), nullable=False), sa.Column("item_price", sa.Numeric(12, 2), nullable=False), sa.Column("shipping_price", sa.Numeric(12, 2)), sa.Column("currency", sa.String(3), nullable=False), sa.Column("availability", sa.String(32), nullable=False), sa.Column("raw_payload", postgresql.JSONB, nullable=False), sa.Column("confidence", sa.Numeric(4, 3), nullable=False), sa.UniqueConstraint("offer_id", "external_observation_id", name="uq_price_obs_offer_external"), sa.CheckConstraint("item_price > 0", name="ck_price_obs_item_price"), sa.CheckConstraint("shipping_price IS NULL OR shipping_price >= 0", name="ck_price_obs_shipping"), sa.CheckConstraint("confidence >= 0 AND confidence <= 1", name="ck_price_obs_confidence"), sa.CheckConstraint( "availability IN ('in_stock', 'out_of_stock', 'unknown')", name="ck_price_obs_availability", ), sa.CheckConstraint("currency ~ '^[A-Z]{3}$'", name="ck_price_obs_currency"), ) op.execute( "CREATE INDEX ix_price_observations_lookup ON price_observations " "(offer_id, observed_at DESC, ingested_at DESC, id DESC)" ) op.create_table( "evaluations", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("variant_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("product_variants.id"), nullable=False), sa.Column("offer_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("offers.id"), nullable=False), sa.Column("profile_id", sa.String(128), nullable=False), sa.Column("profile_version", sa.Integer(), nullable=False), sa.Column("scoring_version", sa.String(64), sa.ForeignKey("scoring_models.version"), nullable=False), sa.Column("as_of", sa.DateTime(timezone=True), nullable=False), sa.Column("snapshot_cutoff", sa.DateTime(timezone=True), nullable=False), sa.Column("scoring_hash", sa.String(64), nullable=False), sa.Column("engine_build", sa.String(64), nullable=False), sa.Column("product_eligibility", sa.String(16), nullable=False), sa.Column("offer_eligibility", sa.String(16), nullable=False), sa.Column("overall_eligibility", sa.String(16), nullable=False), sa.Column("product_score", sa.Numeric(6, 2)), sa.Column("price_score", sa.Numeric(6, 2)), sa.Column("deal_score", sa.Numeric(6, 2)), sa.Column("classification", sa.String(32), nullable=False), sa.Column("price_reference_scope", sa.String(32), nullable=False), sa.Column("input_snapshot", postgresql.JSONB, nullable=False), sa.Column("result_snapshot", postgresql.JSONB, nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), sa.ForeignKeyConstraint( ["profile_id", "profile_version"], ["product_profiles.id", "product_profiles.version"], name="fk_evaluations_profile", ), sa.CheckConstraint( "product_eligibility IN ('ELIGIBLE', 'INELIGIBLE', 'UNKNOWN')", name="ck_eval_product_eligibility", ), sa.CheckConstraint( "offer_eligibility IN ('ELIGIBLE', 'INELIGIBLE', 'UNKNOWN')", name="ck_eval_offer_eligibility", ), sa.CheckConstraint( "overall_eligibility IN ('ELIGIBLE', 'INELIGIBLE', 'UNKNOWN')", name="ck_eval_overall_eligibility", ), sa.CheckConstraint( "product_score IS NULL OR (product_score >= 0 AND product_score <= 100)", name="ck_eval_product_score", ), sa.CheckConstraint( "price_score IS NULL OR (price_score >= 0 AND price_score <= 100)", name="ck_eval_price_score", ), sa.CheckConstraint( "deal_score IS NULL OR (deal_score >= 0 AND deal_score <= 100)", name="ck_eval_deal_score", ), ) op.create_index("ix_evaluations_variant_id", "evaluations", ["variant_id"]) op.execute( "CREATE INDEX ix_evaluations_profile_created ON evaluations " "(profile_id, profile_version, created_at DESC)" ) op.create_table( "evaluation_components", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("evaluation_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("evaluations.id"), nullable=False), sa.Column("kind", sa.String(64), nullable=False), sa.Column("component_key", sa.String(128), nullable=False), sa.Column("raw_score", sa.Numeric(12, 6)), sa.Column("weight", sa.Numeric(12, 6)), sa.Column("contribution", sa.Numeric(12, 6)), sa.Column("details", postgresql.JSONB, nullable=False), sa.UniqueConstraint("evaluation_id", "kind", "component_key", name="uq_eval_component"), ) def downgrade() -> None: op.drop_table("evaluation_components") op.drop_table("evaluations") op.drop_table("price_observations") op.drop_table("offers") op.drop_table("variant_attributes") op.drop_table("product_variants") op.drop_table("products") op.drop_table("product_profiles") op.drop_table("scoring_models") op.drop_table("sources") op.drop_table("attribute_definitions")