Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,686 changes: 1,686 additions & 0 deletions PRPs/PRP-2-data-platform-schema.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from app.core.config import get_settings
from app.core.database import Base

# Import all models for Alembic autogenerate detection
from app.features.data_platform import models as data_platform_models # noqa: F401

# Alembic Config object
config = context.config

Expand Down
187 changes: 187 additions & 0 deletions alembic/versions/e1165ebcef61_create_data_platform_tables.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
"""create_data_platform_tables

Revision ID: e1165ebcef61
Revises:
Create Date: 2026-01-26 09:57:38.704052

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'e1165ebcef61'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Apply migration."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('calendar',
sa.Column('date', sa.Date(), nullable=False),
sa.Column('day_of_week', sa.Integer(), nullable=False),
sa.Column('month', sa.Integer(), nullable=False),
sa.Column('quarter', sa.Integer(), nullable=False),
sa.Column('year', sa.Integer(), nullable=False),
sa.Column('is_holiday', sa.Boolean(), nullable=False),
sa.Column('holiday_name', sa.String(length=100), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint('day_of_week >= 0 AND day_of_week <= 6', name='ck_calendar_day_of_week'),
sa.CheckConstraint('month >= 1 AND month <= 12', name='ck_calendar_month'),
sa.CheckConstraint('quarter >= 1 AND quarter <= 4', name='ck_calendar_quarter'),
sa.PrimaryKeyConstraint('date')
)
op.create_index(op.f('ix_calendar_year'), 'calendar', ['year'], unique=False)
op.create_table('product',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('sku', sa.String(length=50), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('category', sa.String(length=100), nullable=True),
sa.Column('brand', sa.String(length=100), nullable=True),
sa.Column('base_price', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('base_cost', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_product_category'), 'product', ['category'], unique=False)
op.create_index(op.f('ix_product_sku'), 'product', ['sku'], unique=True)
op.create_table('store',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=20), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('region', sa.String(length=50), nullable=True),
sa.Column('city', sa.String(length=50), nullable=True),
sa.Column('store_type', sa.String(length=30), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_store_code'), 'store', ['code'], unique=True)
op.create_table('inventory_snapshot_daily',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('date', sa.Date(), nullable=False),
sa.Column('store_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('on_hand_qty', sa.Integer(), nullable=False),
sa.Column('on_order_qty', sa.Integer(), nullable=False),
sa.Column('is_stockout', sa.Boolean(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint('on_hand_qty >= 0', name='ck_inventory_on_hand_positive'),
sa.CheckConstraint('on_order_qty >= 0', name='ck_inventory_on_order_positive'),
sa.ForeignKeyConstraint(['date'], ['calendar.date'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['store_id'], ['store.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('date', 'store_id', 'product_id', name='uq_inventory_snapshot_daily_grain')
)
op.create_index(op.f('ix_inventory_snapshot_daily_date'), 'inventory_snapshot_daily', ['date'], unique=False)
op.create_index(op.f('ix_inventory_snapshot_daily_product_id'), 'inventory_snapshot_daily', ['product_id'], unique=False)
op.create_index(op.f('ix_inventory_snapshot_daily_store_id'), 'inventory_snapshot_daily', ['store_id'], unique=False)
op.create_index('ix_inventory_snapshot_date_store', 'inventory_snapshot_daily', ['date', 'store_id'], unique=False)
op.create_table('price_history',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('store_id', sa.Integer(), nullable=True),
sa.Column('price', sa.Numeric(precision=10, scale=2), nullable=False),
sa.Column('valid_from', sa.Date(), nullable=False),
sa.Column('valid_to', sa.Date(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint('price >= 0', name='ck_price_history_price_positive'),
sa.CheckConstraint('valid_to IS NULL OR valid_to >= valid_from', name='ck_price_history_valid_dates'),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['store_id'], ['store.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_price_history_product_id'), 'price_history', ['product_id'], unique=False)
op.create_index('ix_price_history_product_validity', 'price_history', ['product_id', 'valid_from', 'valid_to'], unique=False)
op.create_index(op.f('ix_price_history_store_id'), 'price_history', ['store_id'], unique=False)
op.create_index(op.f('ix_price_history_valid_from'), 'price_history', ['valid_from'], unique=False)
op.create_table('promotion',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('store_id', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('discount_pct', sa.Numeric(precision=5, scale=4), nullable=True),
sa.Column('discount_amount', sa.Numeric(precision=10, scale=2), nullable=True),
sa.Column('start_date', sa.Date(), nullable=False),
sa.Column('end_date', sa.Date(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint('discount_amount IS NULL OR discount_amount >= 0', name='ck_promotion_discount_amount_positive'),
sa.CheckConstraint('discount_pct IS NULL OR (discount_pct >= 0 AND discount_pct <= 1)', name='ck_promotion_discount_pct_range'),
sa.CheckConstraint('end_date >= start_date', name='ck_promotion_valid_dates'),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['store_id'], ['store.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index('ix_promotion_product_dates', 'promotion', ['product_id', 'start_date', 'end_date'], unique=False)
op.create_index(op.f('ix_promotion_product_id'), 'promotion', ['product_id'], unique=False)
op.create_index(op.f('ix_promotion_start_date'), 'promotion', ['start_date'], unique=False)
op.create_index(op.f('ix_promotion_store_id'), 'promotion', ['store_id'], unique=False)
op.create_table('sales_daily',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('date', sa.Date(), nullable=False),
sa.Column('store_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('unit_price', sa.Numeric(precision=10, scale=2), nullable=False),
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint('quantity >= 0', name='ck_sales_daily_quantity_positive'),
sa.CheckConstraint('total_amount >= 0', name='ck_sales_daily_amount_positive'),
sa.CheckConstraint('unit_price >= 0', name='ck_sales_daily_price_positive'),
sa.ForeignKeyConstraint(['date'], ['calendar.date'], ),
sa.ForeignKeyConstraint(['product_id'], ['product.id'], ),
sa.ForeignKeyConstraint(['store_id'], ['store.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('date', 'store_id', 'product_id', name='uq_sales_daily_grain')
)
op.create_index(op.f('ix_sales_daily_date'), 'sales_daily', ['date'], unique=False)
op.create_index('ix_sales_daily_date_product', 'sales_daily', ['date', 'product_id'], unique=False)
op.create_index('ix_sales_daily_date_store', 'sales_daily', ['date', 'store_id'], unique=False)
op.create_index(op.f('ix_sales_daily_product_id'), 'sales_daily', ['product_id'], unique=False)
op.create_index(op.f('ix_sales_daily_store_id'), 'sales_daily', ['store_id'], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
"""Revert migration."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_sales_daily_store_id'), table_name='sales_daily')
op.drop_index(op.f('ix_sales_daily_product_id'), table_name='sales_daily')
op.drop_index('ix_sales_daily_date_store', table_name='sales_daily')
op.drop_index('ix_sales_daily_date_product', table_name='sales_daily')
op.drop_index(op.f('ix_sales_daily_date'), table_name='sales_daily')
op.drop_table('sales_daily')
op.drop_index(op.f('ix_promotion_store_id'), table_name='promotion')
op.drop_index(op.f('ix_promotion_start_date'), table_name='promotion')
op.drop_index(op.f('ix_promotion_product_id'), table_name='promotion')
op.drop_index('ix_promotion_product_dates', table_name='promotion')
op.drop_table('promotion')
op.drop_index(op.f('ix_price_history_valid_from'), table_name='price_history')
op.drop_index(op.f('ix_price_history_store_id'), table_name='price_history')
op.drop_index('ix_price_history_product_validity', table_name='price_history')
op.drop_index(op.f('ix_price_history_product_id'), table_name='price_history')
op.drop_table('price_history')
op.drop_index('ix_inventory_snapshot_date_store', table_name='inventory_snapshot_daily')
op.drop_index(op.f('ix_inventory_snapshot_daily_store_id'), table_name='inventory_snapshot_daily')
op.drop_index(op.f('ix_inventory_snapshot_daily_product_id'), table_name='inventory_snapshot_daily')
op.drop_index(op.f('ix_inventory_snapshot_daily_date'), table_name='inventory_snapshot_daily')
op.drop_table('inventory_snapshot_daily')
op.drop_index(op.f('ix_store_code'), table_name='store')
op.drop_table('store')
op.drop_index(op.f('ix_product_sku'), table_name='product')
op.drop_index(op.f('ix_product_category'), table_name='product')
op.drop_table('product')
op.drop_index(op.f('ix_calendar_year'), table_name='calendar')
op.drop_table('calendar')
# ### end Alembic commands ###
26 changes: 26 additions & 0 deletions app/features/data_platform/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Data platform feature for retail forecasting mini-warehouse.

This module provides the core data models for the ForecastLabAI system:
- Dimension tables: Store, Product, Calendar
- Fact tables: SalesDaily, PriceHistory, Promotion, InventorySnapshotDaily
"""

from app.features.data_platform.models import (
Calendar,
InventorySnapshotDaily,
PriceHistory,
Product,
Promotion,
SalesDaily,
Store,
)

__all__ = [
"Calendar",
"InventorySnapshotDaily",
"PriceHistory",
"Product",
"Promotion",
"SalesDaily",
"Store",
]
Loading
Loading