# Schema Drift Analysis — LeoCRM **Date:** 2026-08-21 **Method:** `scripts/schema_drift_check.py` executed in production container `crm_app` against `crm_db` **Total Drifts:** 111 ## Summary | Issue Type | Count | Action | |---|---|---| | VARCHAR LENGTH MISMATCH | 3 | Migration 0135: ALTER COLUMN TYPE | | TABLE MISSING IN DB | 2 | Migration 0135: CREATE TABLE | | COLUMN IN DB NOT IN MODEL | 91 | Models need updating (columns already in DB via migrations) | | TABLE IN DB NOT IN MODEL | 15 | False positives (models exist but not loaded by drift script) | ## 1. VARCHAR LENGTH MISMATCH (3) These are the most critical drifts — the model defines a longer VARCHAR than the DB column, meaning writes can fail in production. | Table | Column | Model Type | DB Type | Fix | |---|---|---|---|---| | contacts | status | VARCHAR(30) | VARCHAR(20) | ALTER COLUMN TYPE VARCHAR(30) | | notifications | type | VARCHAR(100) | VARCHAR(20) | ALTER COLUMN TYPE VARCHAR(100) | | notification_preferences | type_key | VARCHAR(100) | VARCHAR(20) | ALTER COLUMN TYPE VARCHAR(100) | ## 2. TABLE MISSING IN DB (2) Models define these tables but they don't exist in the production database. | Table | Model Location | Fix | |---|---|---| | forgejo_reported_errors | `app/plugins/builtins/forgejo_error_reporter/models.py` | CREATE TABLE in migration 0135 | | pgp_keys | `app/plugins/builtins/mail/models.py` | CREATE TABLE in migration 0135 | ## 3. COLUMN IN DB NOT IN MODEL (91) These columns exist in the production database (added by Alembic migrations) but are NOT defined in the SQLAlchemy models. This means `Base.metadata.create_all()` (used by tests) creates tables WITHOUT these columns, while production has them. ### 3.1 owner_id Columns (78 tables) The `OwnedMixin` adds an `owner_id` column. Many models don't use `OwnedMixin` but migrations added `owner_id` to their tables. | Table | Column | DB Type | |---|---|---| | ai_conversations | owner_id | uuid | | ai_messages | owner_id | uuid | | audit_log | owner_id | uuid | | password_reset_tokens | owner_id | uuid | | api_tokens | owner_id | uuid | | backups | owner_id | uuid | | contactpersons | owner_id | uuid | | contact_merge_history | owner_id | uuid | | currencies | owner_id | uuid | | entity_permissions | owner_id | uuid | | entity_policies | owner_id | uuid | | groups | owner_id | uuid | | user_groups | owner_id | uuid | | notifications | owner_id | uuid | | notification_preferences | owner_id | uuid | | permission_delegations | owner_id | uuid | | permission_templates | owner_id | uuid | | roles | owner_id | uuid | | sessions | owner_id | uuid | | system_settings | owner_id | uuid | | tax_rates | owner_id | uuid | | user_tenants | owner_id | uuid | | workflow_instances | owner_id | uuid | | workflow_step_history | owner_id | uuid | | workspace_modules | owner_id | uuid | | workspace_users | owner_id | uuid | | workspace_widgets | owner_id | uuid | | ai_providers | owner_id | uuid | | ai_models | owner_id | uuid | | ai_presets | owner_id | uuid | | ai_chat_messages | owner_id | uuid | | ai_chat_folders | owner_id | uuid | | ai_chat_attachments | owner_id | uuid | | ai_proactive_context_log | owner_id | uuid | | ai_proactive_settings | owner_id | uuid | | automation_agent_versions | owner_id | uuid | | automation_versions | owner_id | uuid | | automation_cron_jobs | owner_id | uuid | | automation_agent_runs | owner_id | uuid | | automation_runs | owner_id | uuid | | agent_subtasks | owner_id | uuid | | calendar_entry_links | owner_id | uuid | | calendar_shares | owner_id | uuid | | user_calendar_visibility | owner_id | uuid | | resources | owner_id | uuid | | resource_bookings | owner_id | uuid | | comm_participants | owner_id | uuid | | comm_messages | owner_id | uuid | | comm_message_blocks | owner_id | uuid | | comm_message_attachments | owner_id | uuid | | comm_message_reactions | owner_id | uuid | | comm_message_reads | owner_id | uuid | | comm_conversation_pins | owner_id | uuid | | comm_conversation_mutes | owner_id | uuid | | comm_message_edits | owner_id | uuid | | mail_folders | owner_id | uuid | | mail_attachments | owner_id | uuid | | mail_labels | owner_id | uuid | | mail_label_assignments | owner_id | uuid | | mail_rules | owner_id | uuid | | mail_templates | owner_id | uuid | | mail_signatures | owner_id | uuid | | vacation_sent_log | owner_id | uuid | | mail_seen_by | owner_id | uuid | | mail_account_delegates | owner_id | uuid | | mail_account_send_permissions | owner_id | uuid | | contact_pgp_keys | owner_id | uuid | | mail_sync_queue | owner_id | uuid | | permissions | owner_id | uuid | | tag_assignments | owner_id | uuid | | unified_search_providers | owner_id | uuid | | unified_search_index_log | owner_id | uuid | ### 3.2 Search/Embedding/Index Columns (13) | Table | Column | DB Type | Purpose | |---|---|---|---| | audit_log | search_tsv | tsvector | Full-text search | | contacts | indexed_at | timestamp with time zone | Unified search index timestamp | | calendar_entries | search_tsv | tsvector | Full-text search | | calendar_entries | embedding | USER-DEFINED (vector) | Vector embedding | | calendar_entries | indexed_at | timestamp with time zone | Search index timestamp | | files | embedding | USER-DEFINED (vector) | Vector embedding | | files | content_text | text | Extracted text content | | files | content_tsv | tsvector | Full-text search | | files | indexed_at | timestamp with time zone | Search index timestamp | | mails | company_id | uuid | Company reference | | mails | body_tsv | tsvector | Full-text search | | mails | embedding | USER-DEFINED (vector) | Vector embedding | | mails | indexed_at | timestamp with time zone | Search index timestamp | | tags | search_tsv | tsvector | Full-text search | | tags | embedding | USER-DEFINED (vector) | Vector embedding | | agent_memories | embedding | USER-DEFINED (vector) | Vector embedding | | user_groups | deleted_at | timestamp with time zone | Soft delete | | notification_types | deleted_at | timestamp with time zone | Soft delete | ## 4. TABLE IN DB NOT IN MODEL (15) — False Positives These tables have models but the drift script doesn't import all model modules. They are NOT real drifts. | Table | Model Location | |---|---| | ai_decision_records | `app/ai/oversight.py` (DecisionRecordDB) | | approval_requests | `app/core/approval.py` | | companies_old | Legacy table (deprecated) | | company_contacts_old | Legacy table (deprecated) | | contacts_old | Legacy table (deprecated) | | event_outbox | `app/models/outbox.py` (EventOutbox) | | notifications_legacy | Legacy table (deprecated) | | outbox_deliveries | `app/models/outbox_delivery.py` (OutboxDelivery) | | plugin_allowlist | `app/models/plugin_allowlist.py` (PluginAllowlist) | | saved_filters | `app/models/saved_filter.py` (SavedFilter) | | tenant_plugin_activation | Plugin activation table | | user_preferences | `app/models/user_preference.py` (UserPreference) | | wiki_article_versions | `app/plugins/builtins/wiki/models.py` | | wiki_articles | `app/plugins/builtins/wiki/models.py` | | wiki_categories | `app/plugins/builtins/wiki/models.py` | ## Root Cause - **Tests** use `Base.metadata.create_all()` which creates tables from SQLAlchemy model definitions - **Production** uses Alembic migrations which may add columns not in models (e.g., `owner_id`, `search_tsv`, `embedding`) - This causes schema drift: tests pass but production may fail on missing columns or wrong VARCHAR lengths ## Fix 1. **Migration 0135**: Fix VARCHAR lengths + create missing tables 2. **conftest.py**: Switch from `Base.metadata.create_all()` to `alembic upgrade head` so tests use the same schema as production 3. **Models**: Should be updated to include `OwnedMixin` and search/embedding columns (separate task)