Files
signal-platform/alembic/versions/026_dolt_fundamentals_schema.py
T
dennisthiessenandClaude Opus 4.8 949cbbe7c0 feat(dolt): migration 026 + models — fundamentals/earnings schema (A1 schema)
First reviewable slice of workstream A: schema only, no importers, no data.

- data_import_runs: lean batch-import audit (source/revision/status,
  row_counts_json + validation_json as Text-holding-JSON per repo convention).
- fundamental_snapshots: CIK-keyed, one immutable row per accession; stores
  per-period raw facts (duration = cumulative YTD/FY, balance-sheet =
  period-end) plus period_start/period_end/fiscal_year/fiscal_period so
  discrete quarters, Q4, TTM and YoY are derived at read time.
- earnings_events: Dolt-sourced calendar + surprise history, unique
  (ticker_id, announce_date).
- tickers: nullable cik/sic/sic_description — the ticker<->issuer join point.

fundamental_data is left untouched (cutover gated separately at A5). Models
registered in app/models/__init__.py; Ticker gains an earnings_events
relationship. Verified: create_all builds the tables, mappers configure, and
migration 026 renders valid Postgres DDL up and down.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 00:07:17 +02:00

145 lines
5.9 KiB
Python

"""Dolt/SEC fundamentals schema — workstream A
Revision ID: 026
Revises: 025
Create Date: 2026-07-21 00:00:00.000000
Foundational schema for the Dolt bulk-data integration (workstream A): the
batch import-run audit table, the SEC-sourced immutable fundamental snapshots
(CIK-keyed, one row per accession), the Dolt earnings calendar/history, and the
SEC issuer identity columns on ``tickers``. No data is populated here — the
importers land in a later phase. ``fundamental_data`` is left untouched; its
cutover is gated separately (phase A5). ``data_import_runs`` is created first
because the other two tables carry an ``import_run_id`` FK to it.
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "026"
down_revision: Union[str, None] = "025"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
op.create_table(
"data_import_runs",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("source", sa.String(length=32), nullable=False),
sa.Column("revision", sa.String(length=64), nullable=True),
sa.Column("status", sa.String(length=16), nullable=False),
sa.Column("source_max_date", sa.Date(), nullable=True),
sa.Column("row_counts_json", sa.Text(), nullable=True),
sa.Column("validation_json", sa.Text(), nullable=True),
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
sa.Column("error_details", sa.Text(), nullable=True),
)
op.create_index(
"ix_data_import_runs_source_started", "data_import_runs", ["source", "started_at"]
)
op.create_table(
"fundamental_snapshots",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column("cik", sa.String(length=10), nullable=False),
sa.Column("accession", sa.String(length=25), nullable=False),
sa.Column("form", sa.String(length=12), nullable=False),
sa.Column("filed_date", sa.Date(), nullable=False),
sa.Column("accepted_at", sa.DateTime(timezone=True), nullable=False),
sa.Column("period_start", sa.Date(), nullable=True),
sa.Column("period_end", sa.Date(), nullable=False),
sa.Column("fiscal_year", sa.Integer(), nullable=False),
sa.Column("fiscal_period", sa.String(length=4), nullable=False),
# duration facts — cumulative YTD/FY
sa.Column("revenue", sa.Float(), nullable=True),
sa.Column("net_income", sa.Float(), nullable=True),
sa.Column("operating_income", sa.Float(), nullable=True),
sa.Column("diluted_eps", sa.Float(), nullable=True),
sa.Column("cfo", sa.Float(), nullable=True),
sa.Column("capex", sa.Float(), nullable=True),
sa.Column("depreciation_amortization", sa.Float(), nullable=True),
# balance-sheet facts — period-end
sa.Column("cash_and_st_investments", sa.Float(), nullable=True),
sa.Column("total_debt", sa.Float(), nullable=True),
sa.Column("shares_outstanding", sa.Float(), nullable=True),
sa.Column(
"import_run_id",
sa.Integer(),
sa.ForeignKey("data_import_runs.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.UniqueConstraint("accession", name="uq_fundamental_snapshots_accession"),
)
op.create_index(
"ix_fundamental_snapshots_cik_period",
"fundamental_snapshots",
["cik", "fiscal_year", "fiscal_period"],
)
op.create_index(
"ix_fundamental_snapshots_cik_period_end",
"fundamental_snapshots",
["cik", "period_end"],
)
op.create_table(
"earnings_events",
sa.Column("id", sa.Integer(), primary_key=True),
sa.Column(
"ticker_id",
sa.Integer(),
sa.ForeignKey("tickers.id", ondelete="CASCADE"),
nullable=False,
),
sa.Column("announce_date", sa.Date(), nullable=False),
sa.Column("session", sa.String(length=10), nullable=False),
sa.Column("period_end", sa.Date(), nullable=True),
sa.Column("eps_estimate", sa.Float(), nullable=True),
sa.Column("eps_actual", sa.Float(), nullable=True),
sa.Column("source", sa.String(length=32), nullable=False),
sa.Column(
"import_run_id",
sa.Integer(),
sa.ForeignKey("data_import_runs.id", ondelete="SET NULL"),
nullable=True,
),
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False),
sa.UniqueConstraint("ticker_id", "announce_date", name="uq_earnings_ticker_announce"),
)
op.create_index(
"ix_earnings_events_announce_date", "earnings_events", ["announce_date"]
)
# SEC issuer identity on tickers (nullable; the only ticker<->issuer join point).
op.add_column("tickers", sa.Column("cik", sa.String(length=10), nullable=True))
op.add_column("tickers", sa.Column("sic", sa.String(length=4), nullable=True))
op.add_column(
"tickers", sa.Column("sic_description", sa.String(length=160), nullable=True)
)
def downgrade() -> None:
op.drop_column("tickers", "sic_description")
op.drop_column("tickers", "sic")
op.drop_column("tickers", "cik")
op.drop_index("ix_earnings_events_announce_date", table_name="earnings_events")
op.drop_table("earnings_events")
op.drop_index(
"ix_fundamental_snapshots_cik_period_end", table_name="fundamental_snapshots"
)
op.drop_index(
"ix_fundamental_snapshots_cik_period", table_name="fundamental_snapshots"
)
op.drop_table("fundamental_snapshots")
op.drop_index(
"ix_data_import_runs_source_started", table_name="data_import_runs"
)
op.drop_table("data_import_runs")