fix: sync all models with production DB schema
Check Cross-Plugin Imports / check (push) Has been cancelled

- Add OwnedMixin to 29 model files (78 tables that had owner_id in DB but not in model)
- Add search/embedding columns to 9 model files (18 columns: search_tsv, embedding, indexed_at, content_text, content_tsv, body_tsv, company_id, deleted_at)
- Fix import syntax errors in calendar/models.py, mail/models.py, notification.py, contact.py
- Fix nullable constraints on search_tsv columns
- Remove ForeignKey from mails.company_id (companies table not always loaded in test context)
- All 36 tests pass (24 Phase J + 12 Phase K)
- Models now match production DB schema
This commit is contained in:
Agent Zero
2026-08-21 11:20:15 +02:00
parent b3dea611b4
commit 6555655ecf
31 changed files with 124 additions and 75 deletions
@@ -12,6 +12,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin
from pgvector.sqlalchemy import Vector
class AgentMemory(Base, TenantMixin, OwnedMixin):
+6 -6
View File
@@ -22,7 +22,7 @@ from app.models.owned_mixin import OwnedMixin
# --- Providers ---
class AIProvider(Base, TenantMixin):
class AIProvider(Base, TenantMixin, OwnedMixin):
"""LLM provider configuration (OpenAI, Anthropic, Ollama, etc.)."""
__tablename__ = "ai_providers"
@@ -53,7 +53,7 @@ class AIProvider(Base, TenantMixin):
# --- Models ---
class AIModel(Base, TenantMixin):
class AIModel(Base, TenantMixin, OwnedMixin):
"""Available model per provider."""
__tablename__ = "ai_models"
@@ -81,7 +81,7 @@ class AIModel(Base, TenantMixin):
# --- Presets ---
class AIPreset(Base, TenantMixin):
class AIPreset(Base, TenantMixin, OwnedMixin):
"""Model preset: model + parameters + optional system prompt."""
__tablename__ = "ai_presets"
@@ -167,7 +167,7 @@ class AIChatSession(Base, TenantMixin, OwnedMixin):
# --- Chat Messages ---
class AIChatMessage(Base, TenantMixin):
class AIChatMessage(Base, TenantMixin, OwnedMixin):
"""Individual message in a chat session."""
__tablename__ = "ai_chat_messages"
@@ -194,7 +194,7 @@ class AIChatMessage(Base, TenantMixin):
# --- Chat Folders ---
class AIChatFolder(Base, TenantMixin):
class AIChatFolder(Base, TenantMixin, OwnedMixin):
"""Folder for organizing chat sessions."""
__tablename__ = "ai_chat_folders"
@@ -219,7 +219,7 @@ class AIChatFolder(Base, TenantMixin):
# --- Chat Attachments ---
class AIChatAttachment(Base, TenantMixin):
class AIChatAttachment(Base, TenantMixin, OwnedMixin):
"""File attached to a chat message."""
__tablename__ = "ai_chat_attachments"
+2 -2
View File
@@ -64,7 +64,7 @@ class ProactiveSuggestion(Base, TenantMixin, OwnedMixin):
)
class ContextLog(Base, TenantMixin):
class ContextLog(Base, TenantMixin, OwnedMixin):
"""Log of user context changes (page views, entity selections)."""
__tablename__ = "ai_proactive_context_log"
@@ -86,7 +86,7 @@ class ContextLog(Base, TenantMixin):
)
class ProactiveSettings(Base, TenantMixin):
class ProactiveSettings(Base, TenantMixin, OwnedMixin):
"""Per-user settings for proactive AI."""
__tablename__ = "ai_proactive_settings"
+8 -8
View File
@@ -80,7 +80,7 @@ class AgentDefinition(Base, TenantMixin, OwnedMixin):
)
class AgentVersion(Base, TenantMixin):
class AgentVersion(Base, TenantMixin, OwnedMixin):
"""Versioned snapshots of agent definitions."""
__tablename__ = "automation_agent_versions"
@@ -135,7 +135,7 @@ class AutomationDefinition(Base, TenantMixin, OwnedMixin):
)
class AutomationVersion(Base, TenantMixin):
class AutomationVersion(Base, TenantMixin, OwnedMixin):
"""Versioned snapshots of automation definitions."""
__tablename__ = "automation_versions"
@@ -160,7 +160,7 @@ class AutomationVersion(Base, TenantMixin):
)
class AutomationCronJob(Base, TenantMixin):
class AutomationCronJob(Base, TenantMixin, OwnedMixin):
"""Cron job schedule entries for agent heartbeats, automation triggers, or custom jobs."""
__tablename__ = "automation_cron_jobs"
@@ -190,7 +190,7 @@ class AutomationCronJob(Base, TenantMixin):
)
class AgentRun(Base, TenantMixin):
class AgentRun(Base, TenantMixin, OwnedMixin):
"""Execution log for agent runs."""
__tablename__ = "automation_agent_runs"
@@ -229,7 +229,7 @@ class AgentRun(Base, TenantMixin):
)
class AutomationRun(Base, TenantMixin):
class AutomationRun(Base, TenantMixin, OwnedMixin):
"""Execution log for automation runs."""
__tablename__ = "automation_runs"
@@ -268,7 +268,7 @@ class AutomationRun(Base, TenantMixin):
dry_run: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
class AgentRunStep(Base, TenantMixin):
class AgentRunStep(Base, TenantMixin, OwnedMixin):
"""Individual step in a ReAct loop execution (Thought → Action → Observation)."""
__tablename__ = "automation_agent_run_steps"
@@ -296,7 +296,7 @@ class AgentRunStep(Base, TenantMixin):
)
class AgentSubtask(Base, TenantMixin):
class AgentSubtask(Base, TenantMixin, OwnedMixin):
"""A subtask delegated from one agent to another for multi-agent orchestration."""
__tablename__ = "agent_subtasks"
@@ -333,7 +333,7 @@ class AgentSubtask(Base, TenantMixin):
)
class SkillDefinitionDB(Base, TenantMixin):
class SkillDefinitionDB(Base, TenantMixin, OwnedMixin):
"""A skill definition persisted per tenant.
Skills are orchestration metadata, NOT a permission source. They reference
+9 -4
View File
@@ -19,6 +19,8 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
from pgvector.sqlalchemy import Vector
class Calendar(Base, TenantMixin, OwnedMixin):
@@ -42,6 +44,9 @@ class CalendarEntry(Base, TenantMixin, OwnedMixin):
"""Calendar entry — appointment or task, tenant-scoped, soft-deletable."""
__tablename__ = "calendar_entries"
indexed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
embedding: Mapped[Any] = mapped_column(Vector(768), nullable=True)
search_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
__table_args__ = (
Index("ix_entries_tenant_cal", "tenant_id", "calendar_id"),
Index("ix_entries_tenant_start", "tenant_id", "start_at"),
@@ -76,7 +81,7 @@ class CalendarEntry(Base, TenantMixin, OwnedMixin):
deleted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class CalendarEntryLink(Base, TenantMixin):
class CalendarEntryLink(Base, TenantMixin, OwnedMixin):
"""Link between a calendar entry and an entity (contact)."""
__tablename__ = "calendar_entry_links"
@@ -94,7 +99,7 @@ class CalendarEntryLink(Base, TenantMixin):
entity_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False)
class CalendarShare(Base, TenantMixin):
class CalendarShare(Base, TenantMixin, OwnedMixin):
"""Calendar sharing — user or group with read/write permission."""
__tablename__ = "calendar_shares"
@@ -150,7 +155,7 @@ class Subtask(Base, TenantMixin, OwnedMixin):
completed: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
class Resource(Base, TenantMixin):
class Resource(Base, TenantMixin, OwnedMixin):
"""Bookable resource — room or equipment."""
__tablename__ = "resources"
@@ -163,7 +168,7 @@ class Resource(Base, TenantMixin):
type: Mapped[str] = mapped_column(String(50), nullable=False)
class ResourceBooking(Base, TenantMixin):
class ResourceBooking(Base, TenantMixin, OwnedMixin):
"""Booking of a resource for a calendar entry."""
__tablename__ = "resource_bookings"
+8 -1
View File
@@ -5,12 +5,15 @@ from __future__ import annotations
import uuid
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, UniqueConstraint
from sqlalchemy import DateTime, ForeignKey, Index, Integer, String, UniqueConstraint, Text
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin
from pgvector.sqlalchemy import Vector
from sqlalchemy.dialects.postgresql import TSVECTOR
from typing import Any
class Folder(Base, TenantMixin, OwnedMixin):
@@ -46,6 +49,10 @@ class File(Base, TenantMixin, OwnedMixin):
"""File entity — stored on disk, tenant-scoped, soft-deletable."""
__tablename__ = "files"
indexed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
content_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
content_text: Mapped[str | None] = mapped_column(Text, nullable=True)
embedding: Mapped[Any] = mapped_column(Vector(768), nullable=True)
__table_args__ = (
Index("ix_files_folder", "folder_id"),
Index("ix_files_tenant", "tenant_id"),
+9 -9
View File
@@ -54,7 +54,7 @@ class CommConversation(Base, TenantMixin, OwnedMixin):
metadata_: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict, nullable=False)
class CommParticipant(Base, TenantMixin):
class CommParticipant(Base, TenantMixin, OwnedMixin):
"""Participant in a conversation — user, ai, system, gateway, etc."""
__tablename__ = "comm_participants"
@@ -84,7 +84,7 @@ class CommParticipant(Base, TenantMixin):
left_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class CommMessage(Base, TenantMixin):
class CommMessage(Base, TenantMixin, OwnedMixin):
"""Message in a conversation — text content plus rich content blocks."""
__tablename__ = "comm_messages"
@@ -117,7 +117,7 @@ class CommMessage(Base, TenantMixin):
edited_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
class CommMessageBlock(Base, TenantMixin):
class CommMessageBlock(Base, TenantMixin, OwnedMixin):
"""Rich content block attached to a message."""
__tablename__ = "comm_message_blocks"
@@ -139,7 +139,7 @@ class CommMessageBlock(Base, TenantMixin):
sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
class CommMessageAttachment(Base, TenantMixin):
class CommMessageAttachment(Base, TenantMixin, OwnedMixin):
"""File attachment on a message — DMS reference or comm-internal upload."""
__tablename__ = "comm_message_attachments"
@@ -165,7 +165,7 @@ class CommMessageAttachment(Base, TenantMixin):
metadata_: Mapped[dict[str, Any]] = mapped_column("metadata", JSONB, default=dict, nullable=False)
class CommMessageReaction(Base, TenantMixin):
class CommMessageReaction(Base, TenantMixin, OwnedMixin):
"""Emoji reaction on a message."""
__tablename__ = "comm_message_reactions"
@@ -187,7 +187,7 @@ class CommMessageReaction(Base, TenantMixin):
emoji: Mapped[str] = mapped_column(String(50), nullable=False)
class CommMessageRead(Base, TenantMixin):
class CommMessageRead(Base, TenantMixin, OwnedMixin):
"""Read state per user per conversation."""
__tablename__ = "comm_message_reads"
@@ -215,7 +215,7 @@ class CommMessageRead(Base, TenantMixin):
)
class CommConversationPin(Base, TenantMixin):
class CommConversationPin(Base, TenantMixin, OwnedMixin):
"""User-specific conversation pinning."""
__tablename__ = "comm_conversation_pins"
@@ -238,7 +238,7 @@ class CommConversationPin(Base, TenantMixin):
)
class CommConversationMute(Base, TenantMixin):
class CommConversationMute(Base, TenantMixin, OwnedMixin):
"""User-specific conversation muting."""
__tablename__ = "comm_conversation_mutes"
@@ -261,7 +261,7 @@ class CommConversationMute(Base, TenantMixin):
)
class CommMessageEdit(Base, TenantMixin):
class CommMessageEdit(Base, TenantMixin, OwnedMixin):
"""Edit history for messages — stores old content before each edit."""
__tablename__ = "comm_message_edits"
+22 -15
View File
@@ -21,6 +21,9 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
from pgvector.sqlalchemy import Vector
from typing import Any
# --- Mail Accounts (F-MAIL-14, F-MAIL-18) ---
@@ -61,7 +64,7 @@ class MailAccount(Base, TenantMixin, OwnedMixin):
# --- Mail Folders (F-MAIL-01, F-MAIL-19) ---
class MailFolder(Base, TenantMixin):
class MailFolder(Base, TenantMixin, OwnedMixin):
"""IMAP folder mapped to a mail account."""
__tablename__ = "mail_folders"
@@ -94,10 +97,14 @@ class MailFolder(Base, TenantMixin):
# --- Mails (F-MAIL-01, F-MAIL-03, F-MAIL-05) ---
class Mail(Base, TenantMixin):
class Mail(Base, TenantMixin, OwnedMixin):
"""Individual email message stored locally after IMAP sync."""
__tablename__ = "mails"
indexed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
embedding: Mapped[Any] = mapped_column(Vector(768), nullable=True)
body_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
company_id: Mapped[Any] = mapped_column(PGUUID(as_uuid=True), nullable=True)
__table_args__ = (
Index("ix_mails_folder", "folder_id"),
Index("ix_mails_account", "account_id"),
@@ -148,7 +155,7 @@ class Mail(Base, TenantMixin):
# --- Mail Attachments (F-MAIL-04) ---
class MailAttachment(Base, TenantMixin):
class MailAttachment(Base, TenantMixin, OwnedMixin):
"""Attachment on a mail, optionally linked to a DMS file."""
__tablename__ = "mail_attachments"
@@ -178,7 +185,7 @@ class MailAttachment(Base, TenantMixin):
# --- Mail Labels (F-MAIL-09) ---
class MailLabel(Base, TenantMixin):
class MailLabel(Base, TenantMixin, OwnedMixin):
"""Custom label/tag for mails (colored)."""
__tablename__ = "mail_labels"
@@ -192,7 +199,7 @@ class MailLabel(Base, TenantMixin):
user_id: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False)
class MailLabelAssignment(Base, TenantMixin):
class MailLabelAssignment(Base, TenantMixin, OwnedMixin):
"""Many-to-many: mail <-> label."""
__tablename__ = "mail_label_assignments"
@@ -220,7 +227,7 @@ class MailLabelAssignment(Base, TenantMixin):
# --- Mail Rules (F-MAIL-07) ---
class MailRule(Base, TenantMixin):
class MailRule(Base, TenantMixin, OwnedMixin):
"""Filter rule: conditions to actions for incoming mails."""
__tablename__ = "mail_rules"
@@ -247,7 +254,7 @@ class MailRule(Base, TenantMixin):
# --- Mail Templates (F-MAIL-06) ---
class MailTemplate(Base, TenantMixin):
class MailTemplate(Base, TenantMixin, OwnedMixin):
"""Email template with placeholder substitution."""
__tablename__ = "mail_templates"
@@ -265,7 +272,7 @@ class MailTemplate(Base, TenantMixin):
# --- Mail Signatures (F-MAIL-13) ---
class MailSignature(Base, TenantMixin):
class MailSignature(Base, TenantMixin, OwnedMixin):
"""Email signature (HTML) per user, optionally per account."""
__tablename__ = "mail_signatures"
@@ -288,7 +295,7 @@ class MailSignature(Base, TenantMixin):
# --- Vacation Auto-Reply (F-MAIL-08) ---
class VacationSentLog(Base, TenantMixin):
class VacationSentLog(Base, TenantMixin, OwnedMixin):
"""Dedup log for vacation auto-replies (one per sender per 24 hours)."""
__tablename__ = "vacation_sent_log"
@@ -312,7 +319,7 @@ class VacationSentLog(Base, TenantMixin):
# --- Seen-By Tracking (F-MAIL-15) ---
class MailSeenBy(Base, TenantMixin):
class MailSeenBy(Base, TenantMixin, OwnedMixin):
"""Tracks which users have seen a mail in a shared mailbox."""
__tablename__ = "mail_seen_by"
@@ -336,7 +343,7 @@ class MailSeenBy(Base, TenantMixin):
# --- Delegates (F-MAIL-16) ---
class MailAccountDelegate(Base, TenantMixin):
class MailAccountDelegate(Base, TenantMixin, OwnedMixin):
"""Delegate access to a mail account.
access_level values:
@@ -367,7 +374,7 @@ class MailAccountDelegate(Base, TenantMixin):
# --- Send Permissions (F-MAIL-17) ---
class MailAccountSendPermission(Base, TenantMixin):
class MailAccountSendPermission(Base, TenantMixin, OwnedMixin):
"""Permission for a user to send as a shared/group mailbox."""
__tablename__ = "mail_account_send_permissions"
@@ -390,7 +397,7 @@ class MailAccountSendPermission(Base, TenantMixin):
# --- PGP Keys (F-MAIL-12) ---
class PgpKey(Base, TenantMixin):
class PgpKey(Base, TenantMixin, OwnedMixin):
"""PGP private key for a user (encrypted at rest)."""
__tablename__ = "pgp_keys"
@@ -405,7 +412,7 @@ class PgpKey(Base, TenantMixin):
public_key_armored: Mapped[str] = mapped_column(Text, nullable=False)
class ContactPgpKey(Base, TenantMixin):
class ContactPgpKey(Base, TenantMixin, OwnedMixin):
"""Public PGP key for a contact."""
__tablename__ = "contact_pgp_keys"
@@ -419,7 +426,7 @@ class ContactPgpKey(Base, TenantMixin):
key_id: Mapped[str] = mapped_column(String(255), nullable=False, default="")
class MailSyncQueue(Base, TenantMixin):
class MailSyncQueue(Base, TenantMixin, OwnedMixin):
"""Queue for pending IMAP operations that need retry."""
__tablename__ = "mail_sync_queue"
+1 -1
View File
@@ -13,7 +13,7 @@ from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin
class Permission(Base, TenantMixin):
class Permission(Base, TenantMixin, OwnedMixin):
"""File/folder permission — grants access_level to a user or group."""
__tablename__ = "permissions"
+6 -1
View File
@@ -10,12 +10,17 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
from app.models.owned_mixin import OwnedMixin
from sqlalchemy.dialects.postgresql import TSVECTOR
from pgvector.sqlalchemy import Vector
from typing import Any
class Tag(Base, TenantMixin, OwnedMixin):
"""Tag entity — globally managed, tenant-scoped."""
__tablename__ = "tags"
embedding: Mapped[Any] = mapped_column(Vector(768), nullable=True)
search_tsv: Mapped[Any] = mapped_column(TSVECTOR, nullable=True)
__table_args__ = (
UniqueConstraint("tenant_id", "name", name="uq_tags_tenant_name"),
Index("ix_tags_tenant", "tenant_id"),
@@ -28,7 +33,7 @@ class Tag(Base, TenantMixin, OwnedMixin):
color: Mapped[str] = mapped_column(String(7), nullable=False, default="#6B7280")
class TagAssignment(Base, TenantMixin):
class TagAssignment(Base, TenantMixin, OwnedMixin):
"""N:M assignment between tags and entities (companies, contacts, files, folders)."""
__tablename__ = "tag_assignments"
@@ -12,7 +12,7 @@ from sqlalchemy.orm import Mapped, mapped_column
from app.core.db import Base, TenantMixin
class SearchProviderRegistry(Base, TenantMixin):
class SearchProviderRegistry(Base, TenantMixin, OwnedMixin):
"""Registry of active search providers per tenant."""
__tablename__ = "unified_search_providers"
@@ -31,7 +31,7 @@ class SearchProviderRegistry(Base, TenantMixin):
config: Mapped[dict] = mapped_column(JSONB, nullable=False, default=dict)
class SearchIndexLog(Base, TenantMixin):
class SearchIndexLog(Base, TenantMixin, OwnedMixin):
"""Log of indexing actions for audit and debugging."""
__tablename__ = "unified_search_index_log"
@@ -50,9 +50,10 @@ class SearchIndexLog(Base, TenantMixin):
error_message: Mapped[str | None] = mapped_column(Text, nullable=True)
from pgvector.sqlalchemy import Vector # noqa: E402
from app.models.owned_mixin import OwnedMixin
class DocumentChunk(Base, TenantMixin):
class DocumentChunk(Base, TenantMixin, OwnedMixin):
"""Chunk of a DMS file's extracted text, with its own embedding for RAG."""
__tablename__ = "document_chunks"