174 lines
7.6 KiB
Markdown
174 lines
7.6 KiB
Markdown
|
|
# 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)
|