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:
qzl
2026-02-26 17:58:37 +08:00
parent 2994cc708c
commit 6641eba9df
22 changed files with 2242 additions and 23 deletions
+20
View File
@@ -2,14 +2,34 @@ from __future__ import annotations
from models.agent_chat_message import AgentChatMessage
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_factory import LlmFactory
from models.memories import Memory
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__ = [
"AgentChatMessage",
"AgentChatSession",
"AutomationJob",
"GroupMember",
"Group",
"InboxMessage",
"Llm",
"LlmFactory",
"Memory",
"Profile",
"ScheduleItem",
"ScheduleSubscription",
"Todo",
"TodoSource",
"UserAgent",
]
+15 -2
View File
@@ -8,7 +8,6 @@ from enum import Enum
from sqlalchemy import (
DateTime,
Enum as SqlEnum,
ForeignKey,
Integer,
Numeric,
String,
@@ -28,18 +27,32 @@ class AgentChatSessionStatus(str, Enum):
FAILED = "failed"
class SessionType(str, Enum):
CHAT = "chat"
AUTOMATION = "automation"
class AgentChatSession(TimestampMixin, SoftDeleteMixin, Base):
__tablename__: str = "sessions"
__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),
ForeignKey("auth.users.id", ondelete="CASCADE"),
nullable=False,
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)
status: Mapped[AgentChatSessionStatus] = mapped_column(
SqlEnum(
+72
View File
@@ -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,
)
+64
View File
@@ -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,
)
+77
View File
@@ -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,
)
+49
View File
@@ -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,
)
+75
View File
@@ -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,
)
+61
View File
@@ -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,
)
+8 -3
View File
@@ -2,8 +2,8 @@ from __future__ import annotations
import uuid
from sqlalchemy import ForeignKey, String, Text
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy import String, Text
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.orm import Mapped, mapped_column
from core.db.base import Base, SoftDeleteMixin, TimestampMixin
@@ -18,10 +18,10 @@ class Profile(TimestampMixin, SoftDeleteMixin, Base):
"""
__tablename__: str = "profiles"
__table_args__ = {"extend_existing": True}
id: Mapped[uuid.UUID] = mapped_column(
UUID(as_uuid=True),
ForeignKey("auth.users.id", ondelete="CASCADE"),
primary_key=True,
)
username: Mapped[str] = mapped_column(
@@ -37,3 +37,8 @@ class Profile(TimestampMixin, SoftDeleteMixin, Base):
String(200),
nullable=True,
)
settings: Mapped[dict] = mapped_column(
JSONB,
nullable=False,
server_default="{}",
)
+82
View File
@@ -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,
)
+25
View File
@@ -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,
)
+67
View File
@@ -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,
)
+62
View File
@@ -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,
)