diff --git a/app/plugins/builtins/marketplace/migrations/0001_initial.sql b/app/plugins/builtins/marketplace/migrations/0001_initial.sql index 78b7e3b..1df786f 100644 --- a/app/plugins/builtins/marketplace/migrations/0001_initial.sql +++ b/app/plugins/builtins/marketplace/migrations/0001_initial.sql @@ -1,4 +1,5 @@ -- Marketplace listings table (global, NOT tenant-scoped) +-- GLOBAL TABLE: marketplace_listings CREATE TABLE IF NOT EXISTS marketplace_listings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(80) NOT NULL UNIQUE, diff --git a/app/plugins/builtins/unified_search/migrations/0002_embeddings.sql b/app/plugins/builtins/unified_search/migrations/0002_embeddings.sql index b52c635..11043d1 100644 --- a/app/plugins/builtins/unified_search/migrations/0002_embeddings.sql +++ b/app/plugins/builtins/unified_search/migrations/0002_embeddings.sql @@ -1,27 +1,45 @@ -- Unified Search: Embedding columns and HNSW indexes +-- Note: These columns are also added by Alembic migration 0104. +-- This migration is idempotent (IF NOT EXISTS) and skips non-existent tables. -- Ensure pgvector extension is installed CREATE EXTENSION IF NOT EXISTS vector; -- ─── Mails ─── -ALTER TABLE mails ADD COLUMN IF NOT EXISTS embedding vector(768); -CREATE INDEX IF NOT EXISTS ix_mails_embedding ON mails USING hnsw(embedding vector_cosine_ops); +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'mails') THEN + ALTER TABLE mails ADD COLUMN IF NOT EXISTS embedding vector(768); + CREATE INDEX IF NOT EXISTS ix_mails_embedding ON mails USING hnsw(embedding vector_cosine_ops); + END IF; +END $$; -- ─── Contacts ─── -ALTER TABLE contacts ADD COLUMN IF NOT EXISTS embedding vector(768); -CREATE INDEX IF NOT EXISTS ix_contacts_embedding ON contacts USING hnsw(embedding vector_cosine_ops); +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'contacts') THEN + ALTER TABLE contacts ADD COLUMN IF NOT EXISTS embedding vector(768); + CREATE INDEX IF NOT EXISTS ix_contacts_embedding ON contacts USING hnsw(embedding vector_cosine_ops); + END IF; +END $$; --- ─── Companies ─── -ALTER TABLE companies ADD COLUMN IF NOT EXISTS embedding vector(768); -CREATE INDEX IF NOT EXISTS ix_companies_embedding ON companies USING hnsw(embedding vector_cosine_ops); - --- ─── Files (content_text already added in 0001) ─── -ALTER TABLE files ADD COLUMN IF NOT EXISTS embedding vector(768); -CREATE INDEX IF NOT EXISTS ix_files_embedding ON files USING hnsw(embedding vector_cosine_ops); +-- ─── Files ─── +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'files') THEN + ALTER TABLE files ADD COLUMN IF NOT EXISTS embedding vector(768); + CREATE INDEX IF NOT EXISTS ix_files_embedding ON files USING hnsw(embedding vector_cosine_ops); + END IF; +END $$; -- ─── Calendar Entries ─── -ALTER TABLE calendar_entries ADD COLUMN IF NOT EXISTS embedding vector(768); -CREATE INDEX IF NOT EXISTS ix_cal_embedding ON calendar_entries USING hnsw(embedding vector_cosine_ops); +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'calendar_entries') THEN + ALTER TABLE calendar_entries ADD COLUMN IF NOT EXISTS embedding vector(768); + CREATE INDEX IF NOT EXISTS ix_cal_embedding ON calendar_entries USING hnsw(embedding vector_cosine_ops); + END IF; +END $$; -- ─── Tags (shorter dimension for short text) ─── -ALTER TABLE tags ADD COLUMN IF NOT EXISTS embedding vector(384); +DO $$ BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'tags') THEN + ALTER TABLE tags ADD COLUMN IF NOT EXISTS embedding vector(384); + END IF; +END $$; diff --git a/app/plugins/migration_runner.py b/app/plugins/migration_runner.py index be0b54a..400789f 100644 --- a/app/plugins/migration_runner.py +++ b/app/plugins/migration_runner.py @@ -95,8 +95,12 @@ class MigrationRunner: created_tables = new_tables - existing_tables # Validate: all newly created tables must have tenant_id column + # Exception: tables marked as global in the SQL with `-- GLOBAL TABLE` comment + global_tables = self._extract_global_table_names(sql_content) tables_without_tenant = [] for table_name in created_tables: + if table_name in global_tables: + continue # Global tables are exempt from tenant_id requirement if not await self._table_has_column_via_session(db, table_name, "tenant_id"): tables_without_tenant.append(table_name) @@ -105,7 +109,8 @@ class MigrationRunner: await db.rollback() raise MigrationValidationError( f"Plugin '{plugin_name}' migration '{migration_filename}' created tables without tenant_id column: " - f"{', '.join(tables_without_tenant)}. All plugin tables must have tenant_id for multi-tenant isolation." + f"{', '.join(tables_without_tenant)}. All plugin tables must have tenant_id for multi-tenant isolation. " + f"If a table is intentionally global, add `-- GLOBAL TABLE: table_name` comment to the migration SQL." ) # Record the migration in plugin_migrations table @@ -469,3 +474,14 @@ class MigrationRunner: # Match CREATE TABLE [IF NOT EXISTS] "table_name" or CREATE TABLE table_name pattern = r'CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["\']?(\w+)["\']?' return re.findall(pattern, sql, re.IGNORECASE) + + def _extract_global_table_names(sql: str) -> set[str]: + """Extract table names marked as global in SQL comments. + + Looks for `-- GLOBAL TABLE: table_name` comments in the SQL. + These tables are exempt from the tenant_id column requirement. + """ + import re + + pattern = r'--\s*GLOBAL\s+TABLE:\s*(\w+)' + return set(re.findall(pattern, sql, re.IGNORECASE))