feat: split initial social schema migration chain
replace monolithic migration with ordered scripts, include profiles/sessions in migration, and verify full downgrade/upgrade cycle for clean Supabase bootstrap
This commit is contained in:
@@ -20,9 +20,19 @@ from core.db.base import Base # noqa: E402
|
|||||||
from models import ( # noqa: F401,E402
|
from models import ( # noqa: F401,E402
|
||||||
AgentChatMessage,
|
AgentChatMessage,
|
||||||
AgentChatSession,
|
AgentChatSession,
|
||||||
|
AutomationJob,
|
||||||
|
Group,
|
||||||
|
GroupMember,
|
||||||
|
InboxMessage,
|
||||||
Llm,
|
Llm,
|
||||||
LlmFactory,
|
LlmFactory,
|
||||||
|
Memory,
|
||||||
Profile,
|
Profile,
|
||||||
|
ScheduleItem,
|
||||||
|
ScheduleSubscription,
|
||||||
|
Todo,
|
||||||
|
TodoSource,
|
||||||
|
UserAgent,
|
||||||
)
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
"""initial schema part 1: foundation tables
|
||||||
|
|
||||||
|
Revision ID: 202602260001
|
||||||
|
Revises:
|
||||||
|
Create Date: 2026-02-26 20:10:00
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
revision: str = "202602260001"
|
||||||
|
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(
|
||||||
|
"llm_factory",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=50), nullable=False),
|
||||||
|
sa.Column("request_url", sa.String(length=255), nullable=False),
|
||||||
|
sa.Column("avatar", sa.Text(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("name"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_llm_factory_name", "llm_factory", ["name"], unique=True)
|
||||||
|
_enable_rls("llm_factory")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"llms",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("factory_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("model_code", sa.String(length=50), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("model_code"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_llms_factory_id", "llms", ["factory_id"], unique=False)
|
||||||
|
op.create_index("ix_llms_model_code", "llms", ["model_code"], unique=True)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_llms_factory_id",
|
||||||
|
"llms",
|
||||||
|
"llm_factory",
|
||||||
|
["factory_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="RESTRICT",
|
||||||
|
)
|
||||||
|
_enable_rls("llms")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"profiles",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("username", sa.String(length=30), nullable=False),
|
||||||
|
sa.Column("avatar_url", sa.Text(), nullable=True),
|
||||||
|
sa.Column("bio", sa.String(length=200), nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"settings",
|
||||||
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
|
server_default="{}",
|
||||||
|
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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_profiles_username", "profiles", ["username"], unique=False)
|
||||||
|
op.create_index(
|
||||||
|
"ix_profiles_settings_gin",
|
||||||
|
"profiles",
|
||||||
|
["settings"],
|
||||||
|
unique=False,
|
||||||
|
postgresql_using="gin",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_profiles_id",
|
||||||
|
"profiles",
|
||||||
|
"users",
|
||||||
|
["id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
_enable_rls("profiles")
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE OR REPLACE FUNCTION public.create_profile_for_new_user()
|
||||||
|
RETURNS trigger
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
SECURITY DEFINER
|
||||||
|
SET search_path = public
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO public.profiles (id, username, avatar_url, bio, settings, created_at, updated_at)
|
||||||
|
VALUES (
|
||||||
|
NEW.id,
|
||||||
|
COALESCE(
|
||||||
|
NEW.raw_user_meta_data ->> 'username',
|
||||||
|
split_part(NEW.email, '@', 1),
|
||||||
|
'user_' || substring(NEW.id::text, 1, 8)
|
||||||
|
),
|
||||||
|
NULL,
|
||||||
|
NULL,
|
||||||
|
'{}'::jsonb,
|
||||||
|
now(),
|
||||||
|
now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO NOTHING;
|
||||||
|
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users")
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE TRIGGER on_auth_user_created
|
||||||
|
AFTER INSERT ON auth.users
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION public.create_profile_for_new_user();
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"automation_jobs",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("owner_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("title", sa.String(length=255), nullable=False),
|
||||||
|
sa.Column("prompt", sa.Text(), nullable=False),
|
||||||
|
sa.Column("schedule_type", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("run_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("next_run_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("timezone", sa.String(length=50), nullable=False),
|
||||||
|
sa.Column("last_run_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("status", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("created_by", sa.UUID(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("id", "owner_id", name="uq_automation_jobs_id_owner"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_automation_jobs_owner_status",
|
||||||
|
"automation_jobs",
|
||||||
|
["owner_id", "status"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_automation_jobs_status_next_run",
|
||||||
|
"automation_jobs",
|
||||||
|
["status", "next_run_at"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE automation_jobs ADD CONSTRAINT chk_automation_job_schedule_type CHECK (schedule_type IN ('daily', 'weekly'))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE automation_jobs ADD CONSTRAINT chk_automation_job_status CHECK (status IN ('active', 'disabled'))"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_automation_jobs_owner_id",
|
||||||
|
"automation_jobs",
|
||||||
|
"users",
|
||||||
|
["owner_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_automation_jobs_created_by",
|
||||||
|
"automation_jobs",
|
||||||
|
"users",
|
||||||
|
["created_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
_enable_rls("automation_jobs")
|
||||||
|
|
||||||
|
op.execute("REVOKE ALL ON TABLE public.alembic_version FROM anon")
|
||||||
|
op.execute("REVOKE ALL ON TABLE public.alembic_version FROM authenticated")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
_drop_rls("automation_jobs")
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_automation_jobs_created_by", "automation_jobs", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_automation_jobs_owner_id", "automation_jobs", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_table("automation_jobs")
|
||||||
|
|
||||||
|
_drop_rls("profiles")
|
||||||
|
op.execute("DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users")
|
||||||
|
op.execute("DROP FUNCTION IF EXISTS public.create_profile_for_new_user()")
|
||||||
|
op.drop_constraint("fk_profiles_id", "profiles", type_="foreignkey")
|
||||||
|
op.drop_table("profiles")
|
||||||
|
|
||||||
|
_drop_rls("llms")
|
||||||
|
op.drop_constraint("fk_llms_factory_id", "llms", type_="foreignkey")
|
||||||
|
op.drop_table("llms")
|
||||||
|
_drop_rls("llm_factory")
|
||||||
|
op.drop_table("llm_factory")
|
||||||
|
|
||||||
|
|
||||||
|
def _enable_rls(table_name: str) -> None:
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
for action in ["select", "insert", "update", "delete"]:
|
||||||
|
op.execute(
|
||||||
|
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||||
|
)
|
||||||
|
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_rls(table_name: str) -> None:
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
"""initial schema part 2: session and agent tables
|
||||||
|
|
||||||
|
Revision ID: 202602260002
|
||||||
|
Revises: 202602260001
|
||||||
|
Create Date: 2026-02-26 20:11:00
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
revision: str = "202602260002"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "202602260001"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"sessions",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("session_type", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("job_id", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("title", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("status", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"last_activity_at",
|
||||||
|
sa.DateTime(timezone=True),
|
||||||
|
server_default=sa.text("now()"),
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"message_count", sa.Integer(), server_default=sa.text("0"), nullable=False
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"total_tokens", sa.Integer(), server_default=sa.text("0"), nullable=False
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"total_cost",
|
||||||
|
sa.Numeric(precision=12, scale=6),
|
||||||
|
server_default=sa.text("0"),
|
||||||
|
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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_sessions_job_id", "sessions", ["job_id"], unique=False)
|
||||||
|
op.execute(
|
||||||
|
"CREATE INDEX ix_sessions_user_session_type_last_activity ON sessions (user_id, session_type, last_activity_at DESC)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE sessions ADD CONSTRAINT chk_session_type CHECK (session_type IN ('chat', 'automation'))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE sessions ADD CONSTRAINT chk_sessions_type_job_consistency CHECK ((session_type = 'chat' AND job_id IS NULL) OR (session_type = 'automation' AND job_id IS NOT NULL))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE sessions ADD CONSTRAINT chk_sessions_status CHECK (status IN ('pending', 'running', 'completed', 'failed'))"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_sessions_user_id",
|
||||||
|
"sessions",
|
||||||
|
"users",
|
||||||
|
["user_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_sessions_job_id_user_id",
|
||||||
|
"sessions",
|
||||||
|
"automation_jobs",
|
||||||
|
["job_id", "user_id"],
|
||||||
|
["id", "owner_id"],
|
||||||
|
ondelete="RESTRICT",
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_sessions_job_user",
|
||||||
|
"sessions",
|
||||||
|
["job_id", "user_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
_enable_rls("sessions")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"messages",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("session_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("seq", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("role", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("content", sa.Text(), nullable=False),
|
||||||
|
sa.Column("model_code", sa.String(length=50), nullable=True),
|
||||||
|
sa.Column("tool_name", sa.String(length=100), nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"input_tokens", sa.Integer(), nullable=False, server_default=sa.text("0")
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"output_tokens", sa.Integer(), nullable=False, server_default=sa.text("0")
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"cost",
|
||||||
|
sa.Numeric(precision=12, scale=6),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("0"),
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"currency",
|
||||||
|
sa.String(length=3),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("'USD'"),
|
||||||
|
),
|
||||||
|
sa.Column("latency_ms", sa.Integer(), nullable=True),
|
||||||
|
sa.Column("metadata", postgresql.JSONB(astext_type=sa.Text()), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("session_id", "seq", name="uq_messages_session_seq"),
|
||||||
|
)
|
||||||
|
op.create_index("ix_messages_session_id", "messages", ["session_id"], unique=False)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE messages ADD CONSTRAINT chk_message_role CHECK (role IN ('user', 'assistant', 'system', 'tool'))"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_messages_session_id",
|
||||||
|
"messages",
|
||||||
|
"sessions",
|
||||||
|
["session_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
_enable_rls("messages")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"user_agents",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("llm_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("agent_type", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"config",
|
||||||
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
|
server_default="{}",
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("status", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("created_by", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("updated_by", sa.UUID(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("user_id", name="uq_user_agents_user_id"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_user_agents_agent_type", "user_agents", ["agent_type"], unique=False
|
||||||
|
)
|
||||||
|
op.create_index("ix_user_agents_status", "user_agents", ["status"], unique=False)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE user_agents ADD CONSTRAINT chk_agent_type CHECK (agent_type IN ('INTENT_RECOGNITION', 'TASK_EXECUTION', 'RESULT_REPORTING'))"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_user_agents_user_id",
|
||||||
|
"user_agents",
|
||||||
|
"users",
|
||||||
|
["user_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_user_agents_llm_id",
|
||||||
|
"user_agents",
|
||||||
|
"llms",
|
||||||
|
["llm_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="RESTRICT",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_user_agents_created_by",
|
||||||
|
"user_agents",
|
||||||
|
"users",
|
||||||
|
["created_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_user_agents_updated_by",
|
||||||
|
"user_agents",
|
||||||
|
"users",
|
||||||
|
["updated_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
_enable_rls("user_agents")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"memories",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("owner_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("agent_id", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("memory_type", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("title", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("content", postgresql.JSONB(astext_type=sa.Text()), nullable=False),
|
||||||
|
sa.Column("source", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("status", sa.String(length=20), 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.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_memories_owner_type_status",
|
||||||
|
"memories",
|
||||||
|
["owner_id", "memory_type", "status"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_memories_agent_type_status",
|
||||||
|
"memories",
|
||||||
|
["agent_id", "memory_type", "status"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE memories ADD CONSTRAINT chk_memory_type_agent_id CHECK ((memory_type = 'work' AND agent_id IS NOT NULL) OR (memory_type = 'user' AND agent_id IS NULL))"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_memories_owner_id",
|
||||||
|
"memories",
|
||||||
|
"users",
|
||||||
|
["owner_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_memories_agent_id",
|
||||||
|
"memories",
|
||||||
|
"user_agents",
|
||||||
|
["agent_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
_enable_rls("memories")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
_drop_rls("memories")
|
||||||
|
op.drop_constraint("fk_memories_agent_id", "memories", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_memories_owner_id", "memories", type_="foreignkey")
|
||||||
|
op.drop_table("memories")
|
||||||
|
|
||||||
|
_drop_rls("user_agents")
|
||||||
|
op.drop_constraint("fk_user_agents_updated_by", "user_agents", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_user_agents_created_by", "user_agents", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_user_agents_llm_id", "user_agents", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_user_agents_user_id", "user_agents", type_="foreignkey")
|
||||||
|
op.drop_table("user_agents")
|
||||||
|
|
||||||
|
_drop_rls("messages")
|
||||||
|
op.drop_constraint("fk_messages_session_id", "messages", type_="foreignkey")
|
||||||
|
op.drop_table("messages")
|
||||||
|
|
||||||
|
_drop_rls("sessions")
|
||||||
|
op.drop_constraint("fk_sessions_job_id_user_id", "sessions", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_sessions_user_id", "sessions", type_="foreignkey")
|
||||||
|
op.drop_table("sessions")
|
||||||
|
|
||||||
|
|
||||||
|
def _enable_rls(table_name: str) -> None:
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
for action in ["select", "insert", "update", "delete"]:
|
||||||
|
op.execute(
|
||||||
|
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||||
|
)
|
||||||
|
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_rls(table_name: str) -> None:
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
"""initial schema part 3: social graph tables
|
||||||
|
|
||||||
|
Revision ID: 202602260003
|
||||||
|
Revises: 202602260002
|
||||||
|
Create Date: 2026-02-26 20:12:00
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
revision: str = "202602260003"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "202602260002"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"friendships",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("user_low_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("user_high_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("initiator_id", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("status", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("requested_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("accepted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("blocked_by", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("created_by", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("updated_by", sa.UUID(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("user_low_id", "user_high_id", name="uq_friendships_users"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_friendships_user_low_status",
|
||||||
|
"friendships",
|
||||||
|
["user_low_id", "status"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_friendships_user_high_status",
|
||||||
|
"friendships",
|
||||||
|
["user_high_id", "status"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"CREATE INDEX ix_friendships_pending ON friendships (status) WHERE status = 'pending'"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE friendships ADD CONSTRAINT chk_user_low_less_than_high CHECK (user_low_id < user_high_id)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE friendships ADD CONSTRAINT chk_initiator_id_valid CHECK (initiator_id IN (user_low_id, user_high_id))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE friendships ADD CONSTRAINT chk_user_ids_different CHECK (user_low_id <> user_high_id)"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_friendships_user_low_id",
|
||||||
|
"friendships",
|
||||||
|
"users",
|
||||||
|
["user_low_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_friendships_user_high_id",
|
||||||
|
"friendships",
|
||||||
|
"users",
|
||||||
|
["user_high_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_friendships_initiator_id",
|
||||||
|
"friendships",
|
||||||
|
"users",
|
||||||
|
["initiator_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_friendships_created_by",
|
||||||
|
"friendships",
|
||||||
|
"users",
|
||||||
|
["created_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_friendships_updated_by",
|
||||||
|
"friendships",
|
||||||
|
"users",
|
||||||
|
["updated_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
_enable_rls("friendships")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"groups",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("name", sa.String(length=100), nullable=False),
|
||||||
|
sa.Column("description", sa.Text(), nullable=True),
|
||||||
|
sa.Column("owner_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("status", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("created_by", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("updated_by", sa.UUID(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_groups_owner_status", "groups", ["owner_id", "status"], unique=False
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_groups_owner_id",
|
||||||
|
"groups",
|
||||||
|
"users",
|
||||||
|
["owner_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_groups_created_by",
|
||||||
|
"groups",
|
||||||
|
"users",
|
||||||
|
["created_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_groups_updated_by",
|
||||||
|
"groups",
|
||||||
|
"users",
|
||||||
|
["updated_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
_enable_rls("groups")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"group_members",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("group_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("user_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("role", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("join_source", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("invited_by", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("joined_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("removed_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("status", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("created_by", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("updated_by", sa.UUID(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint("group_id", "user_id", name="uq_group_members_group_user"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_group_members_group_role_status",
|
||||||
|
"group_members",
|
||||||
|
["group_id", "role", "status"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_group_members_user_status",
|
||||||
|
"group_members",
|
||||||
|
["user_id", "status"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE group_members ADD CONSTRAINT chk_group_member_role CHECK (role IN ('owner', 'admin', 'member'))"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_group_members_group_id",
|
||||||
|
"group_members",
|
||||||
|
"groups",
|
||||||
|
["group_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_group_members_user_id",
|
||||||
|
"group_members",
|
||||||
|
"users",
|
||||||
|
["user_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_group_members_invited_by",
|
||||||
|
"group_members",
|
||||||
|
"users",
|
||||||
|
["invited_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_group_members_created_by",
|
||||||
|
"group_members",
|
||||||
|
"users",
|
||||||
|
["created_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_group_members_updated_by",
|
||||||
|
"group_members",
|
||||||
|
"users",
|
||||||
|
["updated_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
_enable_rls("group_members")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
_drop_rls("group_members")
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_group_members_updated_by", "group_members", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_group_members_created_by", "group_members", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_group_members_invited_by", "group_members", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint("fk_group_members_user_id", "group_members", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_group_members_group_id", "group_members", type_="foreignkey")
|
||||||
|
op.drop_table("group_members")
|
||||||
|
|
||||||
|
_drop_rls("groups")
|
||||||
|
op.drop_constraint("fk_groups_updated_by", "groups", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_groups_created_by", "groups", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_groups_owner_id", "groups", type_="foreignkey")
|
||||||
|
op.drop_table("groups")
|
||||||
|
|
||||||
|
_drop_rls("friendships")
|
||||||
|
op.drop_constraint("fk_friendships_updated_by", "friendships", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_friendships_created_by", "friendships", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_friendships_initiator_id", "friendships", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_friendships_user_high_id", "friendships", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_friendships_user_low_id", "friendships", type_="foreignkey")
|
||||||
|
op.drop_table("friendships")
|
||||||
|
|
||||||
|
|
||||||
|
def _enable_rls(table_name: str) -> None:
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
for action in ["select", "insert", "update", "delete"]:
|
||||||
|
op.execute(
|
||||||
|
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||||
|
)
|
||||||
|
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_rls(table_name: str) -> None:
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||||
@@ -0,0 +1,488 @@
|
|||||||
|
"""initial schema part 4: collaboration tables
|
||||||
|
|
||||||
|
Revision ID: 202602260004
|
||||||
|
Revises: 202602260003
|
||||||
|
Create Date: 2026-02-26 20:13:00
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects import postgresql
|
||||||
|
|
||||||
|
revision: str = "202602260004"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "202602260003"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"schedule_items",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("owner_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("title", sa.String(length=255), nullable=False),
|
||||||
|
sa.Column("description", sa.Text(), nullable=True),
|
||||||
|
sa.Column("start_at", sa.DateTime(timezone=True), nullable=False),
|
||||||
|
sa.Column("end_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("timezone", sa.String(length=50), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"metadata",
|
||||||
|
postgresql.JSONB(astext_type=sa.Text()),
|
||||||
|
server_default="{}",
|
||||||
|
nullable=False,
|
||||||
|
),
|
||||||
|
sa.Column("recurrence_rule", sa.String(length=255), nullable=True),
|
||||||
|
sa.Column("source_type", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("status", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("created_by", sa.UUID(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_schedule_items_owner_start",
|
||||||
|
"schedule_items",
|
||||||
|
["owner_id", "start_at"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_schedule_items_status_start",
|
||||||
|
"schedule_items",
|
||||||
|
["status", "start_at"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE schedule_items ADD CONSTRAINT chk_schedule_item_source_type CHECK (source_type IN ('manual', 'imported', 'agent_generated'))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE schedule_items ADD CONSTRAINT chk_schedule_item_status CHECK (status IN ('active', 'completed', 'canceled', 'archived'))"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_schedule_items_owner_id",
|
||||||
|
"schedule_items",
|
||||||
|
"users",
|
||||||
|
["owner_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_schedule_items_created_by",
|
||||||
|
"schedule_items",
|
||||||
|
"users",
|
||||||
|
["created_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
_enable_rls("schedule_items")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"schedule_subscriptions",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("item_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("subscriber_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column(
|
||||||
|
"permission", sa.Integer(), nullable=False, server_default=sa.text("1")
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"notify_level",
|
||||||
|
sa.String(length=20),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("'all'"),
|
||||||
|
),
|
||||||
|
sa.Column(
|
||||||
|
"status",
|
||||||
|
sa.String(length=20),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.text("'active'"),
|
||||||
|
),
|
||||||
|
sa.Column("created_by", sa.UUID(), 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"),
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
"item_id", "subscriber_id", name="uq_schedule_subscriptions_item_subscriber"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_schedule_subscribers_subscriber_status",
|
||||||
|
"schedule_subscriptions",
|
||||||
|
["subscriber_id", "status"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_schedule_subscribers_item_status",
|
||||||
|
"schedule_subscriptions",
|
||||||
|
["item_id", "status"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE schedule_subscriptions ADD CONSTRAINT chk_schedule_subscription_permission CHECK (permission BETWEEN 0 AND 7)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE schedule_subscriptions ADD CONSTRAINT chk_schedule_subscription_notify_level CHECK (notify_level IN ('all', 'mentions', 'none'))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE schedule_subscriptions ADD CONSTRAINT chk_schedule_subscription_status CHECK (status IN ('active', 'paused', 'unsubscribed'))"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_schedule_subscriptions_item_id",
|
||||||
|
"schedule_subscriptions",
|
||||||
|
"schedule_items",
|
||||||
|
["item_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_schedule_subscriptions_subscriber_id",
|
||||||
|
"schedule_subscriptions",
|
||||||
|
"users",
|
||||||
|
["subscriber_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_schedule_subscriptions_created_by",
|
||||||
|
"schedule_subscriptions",
|
||||||
|
"users",
|
||||||
|
["created_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
_enable_rls("schedule_subscriptions")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"todos",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("owner_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("title", sa.String(length=255), nullable=False),
|
||||||
|
sa.Column("description", sa.String(length=1000), nullable=True),
|
||||||
|
sa.Column("due_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("priority", sa.Integer(), nullable=False),
|
||||||
|
sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.Column("status", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("created_by", sa.UUID(), 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.Column("deleted_at", sa.DateTime(timezone=True), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint("id"),
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_todos_owner_status_due",
|
||||||
|
"todos",
|
||||||
|
["owner_id", "status", "due_at"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_index(
|
||||||
|
"ix_todos_owner_created", "todos", ["owner_id", "created_at"], unique=False
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE todos ADD CONSTRAINT chk_todos_status CHECK (status IN ('pending', 'done', 'canceled'))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE todos ADD CONSTRAINT chk_todos_priority CHECK (priority BETWEEN 1 AND 4)"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_todos_owner_id",
|
||||||
|
"todos",
|
||||||
|
"users",
|
||||||
|
["owner_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_todos_created_by",
|
||||||
|
"todos",
|
||||||
|
"users",
|
||||||
|
["created_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
_enable_rls("todos")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"todo_sources",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("todo_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("schedule_item_id", sa.UUID(), 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.PrimaryKeyConstraint("id"),
|
||||||
|
sa.UniqueConstraint(
|
||||||
|
"todo_id", "schedule_item_id", name="uq_todo_sources_todo_schedule"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
op.create_index("ix_todo_sources_todo", "todo_sources", ["todo_id"], unique=False)
|
||||||
|
op.create_index(
|
||||||
|
"ix_todo_sources_schedule_item",
|
||||||
|
"todo_sources",
|
||||||
|
["schedule_item_id"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_todo_sources_todo_id",
|
||||||
|
"todo_sources",
|
||||||
|
"todos",
|
||||||
|
["todo_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_todo_sources_schedule_item_id",
|
||||||
|
"todo_sources",
|
||||||
|
"schedule_items",
|
||||||
|
["schedule_item_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
_enable_rls("todo_sources")
|
||||||
|
|
||||||
|
op.create_table(
|
||||||
|
"inbox_messages",
|
||||||
|
sa.Column("id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("recipient_id", sa.UUID(), nullable=False),
|
||||||
|
sa.Column("sender_id", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("message_type", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("friendship_id", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("schedule_item_id", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("group_id", sa.UUID(), nullable=True),
|
||||||
|
sa.Column("content", sa.Text(), nullable=True),
|
||||||
|
sa.Column(
|
||||||
|
"is_read", sa.Boolean(), nullable=False, server_default=sa.text("false")
|
||||||
|
),
|
||||||
|
sa.Column("status", sa.String(length=20), nullable=False),
|
||||||
|
sa.Column("created_by", sa.UUID(), 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(
|
||||||
|
"ix_inbox_messages_recipient_status_created",
|
||||||
|
"inbox_messages",
|
||||||
|
["recipient_id", "status", "created_at"],
|
||||||
|
unique=False,
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_type CHECK (message_type IN ('friend_request', 'calendar', 'system', 'group'))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_status CHECK (status IN ('pending', 'accepted', 'rejected', 'dismissed'))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_sender CHECK ((message_type = 'system' AND sender_id IS NULL) OR (message_type <> 'system' AND sender_id IS NOT NULL))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_friendship CHECK ((message_type = 'friend_request' AND friendship_id IS NOT NULL) OR (message_type <> 'friend_request' AND friendship_id IS NULL))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_schedule_item CHECK ((message_type = 'calendar' AND schedule_item_id IS NOT NULL) OR (message_type <> 'calendar' AND schedule_item_id IS NULL))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_group CHECK ((message_type = 'group' AND group_id IS NOT NULL) OR (message_type <> 'group' AND group_id IS NULL))"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"ALTER TABLE inbox_messages ADD CONSTRAINT chk_inbox_message_system_fields CHECK ((message_type = 'system' AND friendship_id IS NULL AND schedule_item_id IS NULL AND group_id IS NULL) OR (message_type <> 'system' AND (friendship_id IS NOT NULL OR schedule_item_id IS NOT NULL OR group_id IS NOT NULL)))"
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_inbox_messages_recipient_id",
|
||||||
|
"inbox_messages",
|
||||||
|
"users",
|
||||||
|
["recipient_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_inbox_messages_sender_id",
|
||||||
|
"inbox_messages",
|
||||||
|
"users",
|
||||||
|
["sender_id"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_inbox_messages_friendship_id",
|
||||||
|
"inbox_messages",
|
||||||
|
"friendships",
|
||||||
|
["friendship_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_inbox_messages_schedule_item_id",
|
||||||
|
"inbox_messages",
|
||||||
|
"schedule_items",
|
||||||
|
["schedule_item_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_inbox_messages_group_id",
|
||||||
|
"inbox_messages",
|
||||||
|
"groups",
|
||||||
|
["group_id"],
|
||||||
|
["id"],
|
||||||
|
ondelete="CASCADE",
|
||||||
|
)
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_inbox_messages_created_by",
|
||||||
|
"inbox_messages",
|
||||||
|
"users",
|
||||||
|
["created_by"],
|
||||||
|
["id"],
|
||||||
|
referent_schema="auth",
|
||||||
|
ondelete="SET NULL",
|
||||||
|
)
|
||||||
|
_enable_rls("inbox_messages")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
_drop_rls("inbox_messages")
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_inbox_messages_created_by", "inbox_messages", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_inbox_messages_group_id", "inbox_messages", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_inbox_messages_schedule_item_id", "inbox_messages", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_inbox_messages_friendship_id", "inbox_messages", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_inbox_messages_sender_id", "inbox_messages", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_inbox_messages_recipient_id", "inbox_messages", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_table("inbox_messages")
|
||||||
|
|
||||||
|
_drop_rls("todo_sources")
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_todo_sources_schedule_item_id", "todo_sources", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint("fk_todo_sources_todo_id", "todo_sources", type_="foreignkey")
|
||||||
|
op.drop_table("todo_sources")
|
||||||
|
|
||||||
|
_drop_rls("todos")
|
||||||
|
op.drop_constraint("fk_todos_created_by", "todos", type_="foreignkey")
|
||||||
|
op.drop_constraint("fk_todos_owner_id", "todos", type_="foreignkey")
|
||||||
|
op.drop_table("todos")
|
||||||
|
|
||||||
|
_drop_rls("schedule_subscriptions")
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_schedule_subscriptions_created_by",
|
||||||
|
"schedule_subscriptions",
|
||||||
|
type_="foreignkey",
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_schedule_subscriptions_subscriber_id",
|
||||||
|
"schedule_subscriptions",
|
||||||
|
type_="foreignkey",
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_schedule_subscriptions_item_id",
|
||||||
|
"schedule_subscriptions",
|
||||||
|
type_="foreignkey",
|
||||||
|
)
|
||||||
|
op.drop_table("schedule_subscriptions")
|
||||||
|
|
||||||
|
_drop_rls("schedule_items")
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_schedule_items_created_by", "schedule_items", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_constraint(
|
||||||
|
"fk_schedule_items_owner_id", "schedule_items", type_="foreignkey"
|
||||||
|
)
|
||||||
|
op.drop_table("schedule_items")
|
||||||
|
|
||||||
|
|
||||||
|
def _enable_rls(table_name: str) -> None:
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
for action in ["select", "insert", "update", "delete"]:
|
||||||
|
op.execute(
|
||||||
|
f"DROP POLICY IF EXISTS {role}_{action}_{table_name} ON {table_name}"
|
||||||
|
)
|
||||||
|
op.execute(f"ALTER TABLE {table_name} ENABLE ROW LEVEL SECURITY")
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_select_{table_name} ON {table_name} FOR SELECT TO {role} USING (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_insert_{table_name} ON {table_name} FOR INSERT TO {role} WITH CHECK (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_update_{table_name} ON {table_name} FOR UPDATE TO {role} USING (false) WITH CHECK (false)"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
f"CREATE POLICY {role}_delete_{table_name} ON {table_name} FOR DELETE TO {role} USING (false)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _drop_rls(table_name: str) -> None:
|
||||||
|
for role in ["anon", "authenticated"]:
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_delete_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_update_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_insert_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"DROP POLICY IF EXISTS {role}_select_{table_name} ON {table_name}")
|
||||||
|
op.execute(f"ALTER TABLE {table_name} DISABLE ROW LEVEL SECURITY")
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""initial schema part 5: hardening and partial indexes
|
||||||
|
|
||||||
|
Revision ID: 202602260005
|
||||||
|
Revises: 202602260004
|
||||||
|
Create Date: 2026-02-26 20:14:00
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "202602260005"
|
||||||
|
down_revision: Union[str, Sequence[str], None] = "202602260004"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute(
|
||||||
|
"CREATE INDEX ix_inbox_messages_pending_recent ON inbox_messages (recipient_id, created_at DESC) WHERE status = 'pending'"
|
||||||
|
)
|
||||||
|
op.execute(
|
||||||
|
"CREATE INDEX ix_todos_pending_due ON todos (owner_id, due_at) WHERE status = 'pending'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_todos_pending_due")
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_inbox_messages_pending_recent")
|
||||||
@@ -2,14 +2,34 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from models.agent_chat_message import AgentChatMessage
|
from models.agent_chat_message import AgentChatMessage
|
||||||
from models.agent_chat_session import AgentChatSession
|
from models.agent_chat_session import AgentChatSession
|
||||||
|
from models.automation_jobs import AutomationJob
|
||||||
|
from models.group_members import GroupMember
|
||||||
|
from models.groups import Group
|
||||||
|
from models.inbox_messages import InboxMessage
|
||||||
from models.llm import Llm
|
from models.llm import Llm
|
||||||
from models.llm_factory import LlmFactory
|
from models.llm_factory import LlmFactory
|
||||||
|
from models.memories import Memory
|
||||||
from models.profile import Profile
|
from models.profile import Profile
|
||||||
|
from models.schedule_items import ScheduleItem
|
||||||
|
from models.schedule_subscriptions import ScheduleSubscription
|
||||||
|
from models.todos import Todo
|
||||||
|
from models.todo_sources import TodoSource
|
||||||
|
from models.user_agents import UserAgent
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AgentChatMessage",
|
"AgentChatMessage",
|
||||||
"AgentChatSession",
|
"AgentChatSession",
|
||||||
|
"AutomationJob",
|
||||||
|
"GroupMember",
|
||||||
|
"Group",
|
||||||
|
"InboxMessage",
|
||||||
"Llm",
|
"Llm",
|
||||||
"LlmFactory",
|
"LlmFactory",
|
||||||
|
"Memory",
|
||||||
"Profile",
|
"Profile",
|
||||||
|
"ScheduleItem",
|
||||||
|
"ScheduleSubscription",
|
||||||
|
"Todo",
|
||||||
|
"TodoSource",
|
||||||
|
"UserAgent",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ from enum import Enum
|
|||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
DateTime,
|
DateTime,
|
||||||
Enum as SqlEnum,
|
Enum as SqlEnum,
|
||||||
ForeignKey,
|
|
||||||
Integer,
|
Integer,
|
||||||
Numeric,
|
Numeric,
|
||||||
String,
|
String,
|
||||||
@@ -28,18 +27,32 @@ class AgentChatSessionStatus(str, Enum):
|
|||||||
FAILED = "failed"
|
FAILED = "failed"
|
||||||
|
|
||||||
|
|
||||||
|
class SessionType(str, Enum):
|
||||||
|
CHAT = "chat"
|
||||||
|
AUTOMATION = "automation"
|
||||||
|
|
||||||
|
|
||||||
class AgentChatSession(TimestampMixin, SoftDeleteMixin, Base):
|
class AgentChatSession(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
__tablename__: str = "sessions"
|
__tablename__: str = "sessions"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
id: Mapped[uuid.UUID] = mapped_column(
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
)
|
)
|
||||||
user_id: Mapped[uuid.UUID] = mapped_column(
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
UUID(as_uuid=True),
|
UUID(as_uuid=True),
|
||||||
ForeignKey("auth.users.id", ondelete="CASCADE"),
|
|
||||||
nullable=False,
|
nullable=False,
|
||||||
index=True,
|
index=True,
|
||||||
)
|
)
|
||||||
|
session_type: Mapped[SessionType] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=SessionType.CHAT,
|
||||||
|
)
|
||||||
|
job_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
status: Mapped[AgentChatSessionStatus] = mapped_column(
|
status: Mapped[AgentChatSessionStatus] = mapped_column(
|
||||||
SqlEnum(
|
SqlEnum(
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, String, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationJobStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
DISABLED = "disabled"
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleType(str, Enum):
|
||||||
|
DAILY = "daily"
|
||||||
|
WEEKLY = "weekly"
|
||||||
|
|
||||||
|
|
||||||
|
class AutomationJob(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
|
__tablename__: str = "automation_jobs"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
owner_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
title: Mapped[str] = mapped_column(
|
||||||
|
String(255),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
prompt: Mapped[str] = mapped_column(
|
||||||
|
Text,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
schedule_type: Mapped[ScheduleType] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
run_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
next_run_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
timezone: Mapped[str] = mapped_column(
|
||||||
|
String(50),
|
||||||
|
nullable=False,
|
||||||
|
default="UTC",
|
||||||
|
)
|
||||||
|
last_run_at: Mapped[datetime | None] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
status: Mapped[AutomationJobStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=AutomationJobStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import String
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class FriendshipStatus(str, Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
ACCEPTED = "accepted"
|
||||||
|
BLOCKED = "blocked"
|
||||||
|
DECLINED = "declined"
|
||||||
|
CANCELED = "canceled"
|
||||||
|
|
||||||
|
|
||||||
|
class Friendship(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
|
__tablename__: str = "friendships"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
user_low_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
user_high_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
initiator_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
status: Mapped[FriendshipStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=FriendshipStatus.PENDING,
|
||||||
|
)
|
||||||
|
requested_at: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
accepted_at: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
blocked_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
updated_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import String
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class GroupMemberRole(str, Enum):
|
||||||
|
OWNER = "owner"
|
||||||
|
ADMIN = "admin"
|
||||||
|
MEMBER = "member"
|
||||||
|
|
||||||
|
|
||||||
|
class GroupMemberSource(str, Enum):
|
||||||
|
INVITED = "invited"
|
||||||
|
JOINED = "joined"
|
||||||
|
|
||||||
|
|
||||||
|
class GroupMemberStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
MUTED = "muted"
|
||||||
|
REMOVED = "removed"
|
||||||
|
|
||||||
|
|
||||||
|
class GroupMember(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
|
__tablename__: str = "group_members"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
group_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
role: Mapped[GroupMemberRole] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
join_source: Mapped[GroupMemberSource] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
invited_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
joined_at: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
removed_at: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
status: Mapped[GroupMemberStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=GroupMemberStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
updated_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import String, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class GroupStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
ARCHIVED = "archived"
|
||||||
|
|
||||||
|
|
||||||
|
class Group(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
|
__tablename__: str = "groups"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
name: Mapped[str] = mapped_column(
|
||||||
|
String(100),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
description: Mapped[str | None] = mapped_column(
|
||||||
|
Text,
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
owner_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
status: Mapped[GroupStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=GroupStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
updated_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import String, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class InboxMessageType(str, Enum):
|
||||||
|
FRIEND_REQUEST = "friend_request"
|
||||||
|
CALENDAR = "calendar"
|
||||||
|
SYSTEM = "system"
|
||||||
|
GROUP = "group"
|
||||||
|
|
||||||
|
|
||||||
|
class InboxMessageStatus(str, Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
ACCEPTED = "accepted"
|
||||||
|
REJECTED = "rejected"
|
||||||
|
DISMISSED = "dismissed"
|
||||||
|
|
||||||
|
|
||||||
|
class InboxMessage(TimestampMixin, Base):
|
||||||
|
__tablename__: str = "inbox_messages"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
recipient_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
sender_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
message_type: Mapped[InboxMessageType] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
friendship_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
schedule_item_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
group_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
content: Mapped[str | None] = mapped_column(
|
||||||
|
Text,
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
is_read: Mapped[bool] = mapped_column(
|
||||||
|
String(10),
|
||||||
|
nullable=False,
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
status: Mapped[InboxMessageStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=InboxMessageStatus.PENDING,
|
||||||
|
)
|
||||||
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import String
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryType(str, Enum):
|
||||||
|
USER = "user"
|
||||||
|
WORK = "work"
|
||||||
|
|
||||||
|
|
||||||
|
class MemorySource(str, Enum):
|
||||||
|
MANUAL = "manual"
|
||||||
|
AGENT = "agent"
|
||||||
|
IMPORTED = "imported"
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
DISABLED = "disabled"
|
||||||
|
|
||||||
|
|
||||||
|
class Memory(TimestampMixin, Base):
|
||||||
|
__tablename__: str = "memories"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
owner_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
agent_id: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
memory_type: Mapped[MemoryType] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
title: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||||
|
content: Mapped[dict] = mapped_column(
|
||||||
|
JSONB,
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
source: Mapped[MemorySource] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
status: Mapped[MemoryStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=MemoryStatus.ACTIVE,
|
||||||
|
)
|
||||||
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String, Text
|
from sqlalchemy import String, Text
|
||||||
from sqlalchemy.dialects.postgresql import UUID
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||||
from sqlalchemy.orm import Mapped, mapped_column
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
@@ -18,10 +18,10 @@ class Profile(TimestampMixin, SoftDeleteMixin, Base):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__tablename__: str = "profiles"
|
__tablename__: str = "profiles"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
id: Mapped[uuid.UUID] = mapped_column(
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
UUID(as_uuid=True),
|
UUID(as_uuid=True),
|
||||||
ForeignKey("auth.users.id", ondelete="CASCADE"),
|
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
)
|
)
|
||||||
username: Mapped[str] = mapped_column(
|
username: Mapped[str] = mapped_column(
|
||||||
@@ -37,3 +37,8 @@ class Profile(TimestampMixin, SoftDeleteMixin, Base):
|
|||||||
String(200),
|
String(200),
|
||||||
nullable=True,
|
nullable=True,
|
||||||
)
|
)
|
||||||
|
settings: Mapped[dict] = mapped_column(
|
||||||
|
JSONB,
|
||||||
|
nullable=False,
|
||||||
|
server_default="{}",
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, String, Text
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleItemStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
COMPLETED = "completed"
|
||||||
|
CANCELED = "canceled"
|
||||||
|
ARCHIVED = "archived"
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleItemSourceType(str, Enum):
|
||||||
|
MANUAL = "manual"
|
||||||
|
IMPORTED = "imported"
|
||||||
|
AGENT_GENERATED = "agent_generated"
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleItem(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
|
__tablename__: str = "schedule_items"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
owner_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
title: Mapped[str] = mapped_column(
|
||||||
|
String(255),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
description: Mapped[str | None] = mapped_column(
|
||||||
|
Text,
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
start_at: Mapped[datetime] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
end_at: Mapped[datetime | None] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
timezone: Mapped[str] = mapped_column(
|
||||||
|
String(50),
|
||||||
|
nullable=False,
|
||||||
|
default="UTC",
|
||||||
|
)
|
||||||
|
extra_metadata: Mapped[dict] = mapped_column(
|
||||||
|
"metadata",
|
||||||
|
JSONB,
|
||||||
|
nullable=False,
|
||||||
|
server_default="{}",
|
||||||
|
)
|
||||||
|
recurrence_rule: Mapped[str | None] = mapped_column(
|
||||||
|
String(255),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
source_type: Mapped[ScheduleItemSourceType] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=ScheduleItemSourceType.MANUAL,
|
||||||
|
)
|
||||||
|
status: Mapped[ScheduleItemStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=ScheduleItemStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import Integer, String
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
PAUSED = "paused"
|
||||||
|
UNSUBSCRIBED = "unsubscribed"
|
||||||
|
|
||||||
|
|
||||||
|
class NotifyLevel(str, Enum):
|
||||||
|
ALL = "all"
|
||||||
|
MENTIONS = "mentions"
|
||||||
|
NONE = "none"
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduleSubscription(TimestampMixin, Base):
|
||||||
|
__tablename__: str = "schedule_subscriptions"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
item_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
subscriber_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
permission: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
nullable=False,
|
||||||
|
default=1,
|
||||||
|
)
|
||||||
|
notify_level: Mapped[NotifyLevel] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=NotifyLevel.ALL,
|
||||||
|
)
|
||||||
|
status: Mapped[SubscriptionStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=SubscriptionStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TodoSource(TimestampMixin, Base):
|
||||||
|
__tablename__: str = "todo_sources"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
todo_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
schedule_item_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import DateTime, Integer, String
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TodoStatus(str, Enum):
|
||||||
|
PENDING = "pending"
|
||||||
|
DONE = "done"
|
||||||
|
CANCELED = "canceled"
|
||||||
|
|
||||||
|
|
||||||
|
class TodoPriority(int, Enum):
|
||||||
|
IMPORTANT_URGENT = 1
|
||||||
|
IMPORTANT_NOT_URGENT = 2
|
||||||
|
NOT_IMPORTANT_URGENT = 3
|
||||||
|
NOT_IMPORTANT_NOT_URGENT = 4
|
||||||
|
|
||||||
|
|
||||||
|
class Todo(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
|
__tablename__: str = "todos"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
owner_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
title: Mapped[str] = mapped_column(
|
||||||
|
String(255),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
description: Mapped[str | None] = mapped_column(
|
||||||
|
String(1000),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
due_at: Mapped[datetime | None] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
priority: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
nullable=False,
|
||||||
|
default=TodoPriority.NOT_IMPORTANT_NOT_URGENT,
|
||||||
|
)
|
||||||
|
completed_at: Mapped[datetime | None] = mapped_column(
|
||||||
|
DateTime(timezone=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
status: Mapped[TodoStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=TodoStatus.PENDING,
|
||||||
|
)
|
||||||
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
from sqlalchemy import String
|
||||||
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
|
||||||
|
|
||||||
|
|
||||||
|
class UserAgentStatus(str, Enum):
|
||||||
|
ACTIVE = "active"
|
||||||
|
PAUSED = "paused"
|
||||||
|
MIGRATING = "migrating"
|
||||||
|
|
||||||
|
|
||||||
|
class AgentType(str, Enum):
|
||||||
|
INTENT_RECOGNITION = "INTENT_RECOGNITION"
|
||||||
|
TASK_EXECUTION = "TASK_EXECUTION"
|
||||||
|
RESULT_REPORTING = "RESULT_REPORTING"
|
||||||
|
|
||||||
|
|
||||||
|
class UserAgent(TimestampMixin, SoftDeleteMixin, Base):
|
||||||
|
__tablename__: str = "user_agents"
|
||||||
|
__table_args__ = {"extend_existing": True}
|
||||||
|
|
||||||
|
id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||||
|
)
|
||||||
|
user_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
unique=True,
|
||||||
|
)
|
||||||
|
llm_id: Mapped[uuid.UUID] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
agent_type: Mapped[AgentType] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
)
|
||||||
|
config: Mapped[dict] = mapped_column(
|
||||||
|
JSONB,
|
||||||
|
nullable=False,
|
||||||
|
server_default="{}",
|
||||||
|
)
|
||||||
|
status: Mapped[UserAgentStatus] = mapped_column(
|
||||||
|
String(20),
|
||||||
|
nullable=False,
|
||||||
|
default=UserAgentStatus.ACTIVE,
|
||||||
|
)
|
||||||
|
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
|
updated_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||||
|
UUID(as_uuid=True),
|
||||||
|
nullable=True,
|
||||||
|
)
|
||||||
@@ -3,15 +3,25 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def test_agent_chat_migration_exists_and_creates_expected_tables() -> None:
|
def test_initial_migration_exists_and_creates_expected_tables() -> None:
|
||||||
versions_dir = Path(__file__).resolve().parents[3] / "alembic" / "versions"
|
versions_dir = Path(__file__).resolve().parents[3] / "alembic" / "versions"
|
||||||
migration = versions_dir / "20260226_create_agent_chat_core_tables.py"
|
migration_files = sorted(versions_dir.glob("20260226_*.py"))
|
||||||
|
assert len(migration_files) == 5, "split initial migrations should exist"
|
||||||
|
|
||||||
assert migration.exists()
|
content = "\n".join(m.read_text(encoding="utf-8") for m in migration_files)
|
||||||
|
|
||||||
content = migration.read_text(encoding="utf-8")
|
# New tables from social data model redesign
|
||||||
assert 'create_table(\n "llm_factory"' in content
|
assert "create_table(" in content and "automation_jobs" in content
|
||||||
assert 'create_table(\n "llms"' in content
|
assert "create_table(" in content and "user_agents" in content
|
||||||
assert 'create_table(\n "sessions"' in content
|
assert "create_table(" in content and "memories" in content
|
||||||
assert 'create_table(\n "messages"' in content
|
assert "create_table(" in content and "friendships" in content
|
||||||
assert "tool_calls" not in content
|
assert "create_table(" in content and "groups" in content
|
||||||
|
assert "create_table(" in content and "group_members" in content
|
||||||
|
assert "create_table(" in content and "schedule_items" in content
|
||||||
|
assert "create_table(" in content and "schedule_subscriptions" in content
|
||||||
|
assert "create_table(" in content and "inbox_messages" in content
|
||||||
|
assert "create_table(" in content and "todos" in content
|
||||||
|
assert "create_table(" in content and "todo_sources" in content
|
||||||
|
assert "create_table(" in content and "profiles" in content
|
||||||
|
assert "create_table(" in content and "sessions" in content
|
||||||
|
assert "create_table(" in content and "messages" in content
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ from __future__ import annotations
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def test_drop_display_name_migration_exists_and_uses_username_metadata() -> None:
|
def test_profiles_has_settings_column() -> None:
|
||||||
|
"""Verify profiles and auth trigger are defined in split migrations."""
|
||||||
versions_dir = Path(__file__).resolve().parents[3] / "alembic" / "versions"
|
versions_dir = Path(__file__).resolve().parents[3] / "alembic" / "versions"
|
||||||
migration = (
|
migration_files = sorted(versions_dir.glob("20260226_*.py"))
|
||||||
versions_dir / "20260224_drop_profile_display_name_and_trigger_username.py"
|
content = "\n".join(m.read_text(encoding="utf-8") for m in migration_files)
|
||||||
)
|
|
||||||
|
|
||||||
assert migration.exists()
|
assert 'create_table(\n "profiles"' in content
|
||||||
|
assert '"settings"' in content
|
||||||
content = migration.read_text(encoding="utf-8")
|
assert "create_profile_for_new_user" in content
|
||||||
assert "DROP COLUMN" in content and "display_name" in content
|
assert "on_auth_user_created" in content
|
||||||
assert "raw_user_meta_data->>'username'" in content
|
|
||||||
|
|||||||
Reference in New Issue
Block a user