234 lines
12 KiB
Python
234 lines
12 KiB
Python
"""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")
|