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