From cb7cf3bc8c16b01d7b6912a722feb8744b64b0f8 Mon Sep 17 00:00:00 2001 From: Bernd Worsch Date: Thu, 26 Mar 2026 21:51:01 +0000 Subject: [PATCH] =?UTF-8?q?feat(db):=20ORM=20models=20+=20Alembic=20migrat?= =?UTF-8?q?ions=200001=E2=80=930003=20=E2=80=94=20T09/T10/T11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SQLAlchemy ORM (src/activity_core/orm.py): - ActivityDefinition, ActivityRun, TaskInstance mapped to Base.metadata - Wired into migrations/env.py for autogenerate support Migrations (chained 0001 → 0002 → 0003): - 0001: activity_definitions (id, name, enabled, trigger_type, trigger_config JSONB, context_sources JSONB, task_templates JSONB, dedupe_key_strategy, version, created_at, updated_at) - 0002: activity_runs (run_id, activity_id FK→activity_definitions, scheduled_for, fired_at, context_snapshot JSONB, tasks_spawned, version_used) + index on activity_id - 0003: task_instances (id, run_id FK→activity_runs CASCADE, type, params JSONB, status, created_at) + index on run_id Apply with: ACTCORE_DB_URL=... alembic upgrade head Co-Authored-By: Claude Sonnet 4.6 --- migrations/env.py | 3 +- .../0001_create_activity_definitions.py | 51 ++++++++++ .../versions/0002_create_activity_runs.py | 47 +++++++++ .../versions/0003_create_task_instances.py | 46 +++++++++ src/activity_core/orm.py | 95 +++++++++++++++++++ .../custodian-WP-0001-temporal-backbone.md | 6 +- 6 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 migrations/versions/0001_create_activity_definitions.py create mode 100644 migrations/versions/0002_create_activity_runs.py create mode 100644 migrations/versions/0003_create_task_instances.py create mode 100644 src/activity_core/orm.py diff --git a/migrations/env.py b/migrations/env.py index 934e2ef..73577a6 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -8,7 +8,8 @@ from sqlalchemy.ext.asyncio import async_engine_from_config from alembic import context -from activity_core.db import Base # noqa: F401 — imports all ORM models via submodules +from activity_core.db import Base # noqa: F401 +import activity_core.orm # noqa: F401 — registers all ORM tables into Base.metadata # Alembic Config object — access to values in alembic.ini config = context.config diff --git a/migrations/versions/0001_create_activity_definitions.py b/migrations/versions/0001_create_activity_definitions.py new file mode 100644 index 0000000..c7adfe8 --- /dev/null +++ b/migrations/versions/0001_create_activity_definitions.py @@ -0,0 +1,51 @@ +"""create_activity_definitions + +Revision ID: 0001 +Revises: +Create Date: 2026-03-26 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB + +revision: str = "0001" +down_revision: Union[str, Sequence[str], None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "activity_definitions", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("name", sa.Text(), nullable=False), + sa.Column("enabled", sa.Boolean(), nullable=False, server_default="true"), + sa.Column("trigger_type", sa.Text(), nullable=False), + sa.Column("trigger_config", JSONB(), nullable=False), + sa.Column("context_sources", JSONB(), nullable=False, server_default="[]"), + sa.Column("task_templates", JSONB(), nullable=False, server_default="[]"), + sa.Column( + "dedupe_key_strategy", sa.Text(), nullable=False, server_default="skip" + ), + sa.Column("version", sa.Integer(), nullable=False, server_default="1"), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column( + "updated_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.PrimaryKeyConstraint("id"), + ) + + +def downgrade() -> None: + op.drop_table("activity_definitions") diff --git a/migrations/versions/0002_create_activity_runs.py b/migrations/versions/0002_create_activity_runs.py new file mode 100644 index 0000000..295d45f --- /dev/null +++ b/migrations/versions/0002_create_activity_runs.py @@ -0,0 +1,47 @@ +"""create_activity_runs + +Revision ID: 0002 +Revises: 0001 +Create Date: 2026-03-26 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB + +revision: str = "0002" +down_revision: Union[str, Sequence[str], None] = "0001" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "activity_runs", + sa.Column("run_id", sa.UUID(), nullable=False), + sa.Column("activity_id", sa.UUID(), nullable=False), + sa.Column("scheduled_for", sa.DateTime(timezone=True), nullable=True), + sa.Column( + "fired_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.Column("context_snapshot", JSONB(), nullable=False, server_default="{}"), + sa.Column("tasks_spawned", sa.Integer(), nullable=False, server_default="0"), + sa.Column("version_used", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["activity_id"], + ["activity_definitions.id"], + ondelete="RESTRICT", + ), + sa.PrimaryKeyConstraint("run_id"), + ) + op.create_index("ix_activity_runs_activity_id", "activity_runs", ["activity_id"]) + + +def downgrade() -> None: + op.drop_index("ix_activity_runs_activity_id", table_name="activity_runs") + op.drop_table("activity_runs") diff --git a/migrations/versions/0003_create_task_instances.py b/migrations/versions/0003_create_task_instances.py new file mode 100644 index 0000000..ac76cfd --- /dev/null +++ b/migrations/versions/0003_create_task_instances.py @@ -0,0 +1,46 @@ +"""create_task_instances + +Revision ID: 0003 +Revises: 0002 +Create Date: 2026-03-26 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSONB + +revision: str = "0003" +down_revision: Union[str, Sequence[str], None] = "0002" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_table( + "task_instances", + sa.Column("id", sa.UUID(), nullable=False), + sa.Column("run_id", sa.UUID(), nullable=False), + sa.Column("type", sa.Text(), nullable=False), + sa.Column("params", JSONB(), nullable=False, server_default="{}"), + sa.Column("status", sa.Text(), nullable=False, server_default="pending"), + sa.Column( + "created_at", + sa.DateTime(timezone=True), + nullable=False, + server_default=sa.text("now()"), + ), + sa.ForeignKeyConstraint( + ["run_id"], + ["activity_runs.run_id"], + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index("ix_task_instances_run_id", "task_instances", ["run_id"]) + + +def downgrade() -> None: + op.drop_index("ix_task_instances_run_id", table_name="task_instances") + op.drop_table("task_instances") diff --git a/src/activity_core/orm.py b/src/activity_core/orm.py new file mode 100644 index 0000000..7ff85e9 --- /dev/null +++ b/src/activity_core/orm.py @@ -0,0 +1,95 @@ +"""SQLAlchemy ORM table definitions for activity-core. + +These are the persistence-layer counterparts to the Pydantic domain models in +models.py. Alembic reads Base.metadata (imported via db.py) for autogenerate. +""" + +from __future__ import annotations + +import uuid +from datetime import datetime + +from sqlalchemy import ( + Boolean, + DateTime, + ForeignKey, + Integer, + Text, + func, +) +from sqlalchemy.dialects.postgresql import JSONB, UUID +from sqlalchemy.orm import Mapped, mapped_column + +from activity_core.db import Base + + +class ActivityDefinition(Base): + __tablename__ = "activity_definitions" + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 + ) + name: Mapped[str] = mapped_column(Text, nullable=False) + enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True) + trigger_type: Mapped[str] = mapped_column(Text, nullable=False) + trigger_config: Mapped[dict] = mapped_column(JSONB, nullable=False) + context_sources: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) + task_templates: Mapped[list] = mapped_column(JSONB, nullable=False, default=list) + dedupe_key_strategy: Mapped[str] = mapped_column( + Text, nullable=False, default="skip" + ) + version: Mapped[int] = mapped_column(Integer, nullable=False, default=1) + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + nullable=False, + server_default=func.now(), + onupdate=func.now(), + ) + + +class ActivityRun(Base): + __tablename__ = "activity_runs" + + run_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 + ) + activity_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("activity_definitions.id", ondelete="RESTRICT"), + nullable=False, + index=True, + ) + scheduled_for: Mapped[datetime | None] = mapped_column( + DateTime(timezone=True), nullable=True + ) + fired_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now() + ) + context_snapshot: Mapped[dict] = mapped_column( + JSONB, nullable=False, default=dict + ) + tasks_spawned: Mapped[int] = mapped_column(Integer, nullable=False, default=0) + version_used: Mapped[int] = mapped_column(Integer, nullable=False) + + +class TaskInstance(Base): + __tablename__ = "task_instances" + + id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 + ) + run_id: Mapped[uuid.UUID] = mapped_column( + UUID(as_uuid=True), + ForeignKey("activity_runs.run_id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + type: Mapped[str] = mapped_column(Text, nullable=False) + params: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict) + status: Mapped[str] = mapped_column(Text, nullable=False, default="pending") + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), nullable=False, server_default=func.now() + ) diff --git a/workplans/custodian-WP-0001-temporal-backbone.md b/workplans/custodian-WP-0001-temporal-backbone.md index f21e267..3f91e77 100644 --- a/workplans/custodian-WP-0001-temporal-backbone.md +++ b/workplans/custodian-WP-0001-temporal-backbone.md @@ -40,15 +40,15 @@ tasks: state_hub_task_id: d9fe3e54-ec1a-4e23-aa76-d8869f4e024d - id: T09 title: Write activity_definitions migration - status: todo + status: done state_hub_task_id: 47774e01-1026-478e-9a46-7d676bfed45c - id: T10 title: Write activity_runs migration - status: todo + status: done state_hub_task_id: 0a74f29f-c07d-4338-be90-e5cf4087261b - id: T11 title: Write task_instances migration - status: todo + status: done state_hub_task_id: 491a6903-8189-43bb-958f-4d16abc84f8e - id: T12 title: Seed one example ActivityDefinition