fix(migrations): make runner robust for all tenant DBs

- Register all missing migrations in runner.py
- Make v4.3 idempotent (rename xml_unsigned only if exists)
- Make v3.3 idempotent (skip warehouse_inventory/purchase_order_items ops when tables/columns missing)
- Mark v3.3.1 and v3.9 as master-only (SKIP)
- Mark v3.5.1 as optional (skip if whatsapp tables missing)
- Runner skips files marked with '-- : SKIP'
This commit is contained in:
2026-06-14 10:08:16 +00:00
parent 7d21d21200
commit 6aff32f93b
5 changed files with 194 additions and 163 deletions

View File

@@ -77,11 +77,19 @@ def apply_migration(db_name, version):
print(f" ERROR: Migration file not found: {filepath}") print(f" ERROR: Migration file not found: {filepath}")
return False return False
with open(filepath) as f:
sql = f.read()
# Skip migrations marked for manual/non-tenant execution
first_line = sql.splitlines()[0].strip() if sql.strip() else ''
if first_line.startswith(': SKIP') or first_line.startswith('-- : SKIP'):
print(f" SKIP (manual/non-tenant migration)")
return True
conn = get_tenant_conn_by_dbname(db_name) conn = get_tenant_conn_by_dbname(db_name)
cur = conn.cursor() cur = conn.cursor()
try: try:
with open(filepath) as f: cur.execute(sql)
cur.execute(f.read())
conn.commit() conn.commit()
return True return True
except Exception as e: except Exception as e:

View File

@@ -1,6 +1,6 @@
-- ═══════════════════════════════════════════════════════════════════════ -- ═══════════════════════════════════════════════════════════════════════
-- v3.3 — Marketplace accepts any part number (seller listings) -- v3.3 — Marketplace accepts any part number (seller listings)
-- Target: nexus_autoparts (master DB) -- Target: nexus_autoparts (master DB) / tenants with warehouse_inventory
-- Date: 2026-05-17 -- Date: 2026-05-17
-- --
-- Makes warehouse_inventory part_id nullable and adds seller-defined -- Makes warehouse_inventory part_id nullable and adds seller-defined
@@ -8,8 +8,10 @@
-- Existing OEM-matched listings are untouched. -- Existing OEM-matched listings are untouched.
-- ═══════════════════════════════════════════════════════════════════════ -- ═══════════════════════════════════════════════════════════════════════
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'warehouse_inventory') THEN
-- ─── 1. WAREHOUSE_INVENTORY — add seller listing columns ───────────── -- ─── 1. WAREHOUSE_INVENTORY — add seller listing columns ─────────────
ALTER TABLE warehouse_inventory ALTER TABLE warehouse_inventory
ADD COLUMN IF NOT EXISTS seller_part_number VARCHAR(100), ADD COLUMN IF NOT EXISTS seller_part_number VARCHAR(100),
ADD COLUMN IF NOT EXISTS seller_part_name VARCHAR(300), ADD COLUMN IF NOT EXISTS seller_part_name VARCHAR(300),
@@ -20,12 +22,6 @@ ALTER TABLE warehouse_inventory
ALTER TABLE warehouse_inventory ALTER COLUMN part_id DROP NOT NULL; ALTER TABLE warehouse_inventory ALTER COLUMN part_id DROP NOT NULL;
-- ─── 2. WAREHOUSE_INVENTORY — drop old unique, add partial uniques ─── -- ─── 2. WAREHOUSE_INVENTORY — drop old unique, add partial uniques ───
-- The old constraint was on (user_id, part_id, warehouse_location).
-- We replace it with two partial unique indexes:
-- - OEM items: (bodega_id, part_id, warehouse_location) WHERE part_id IS NOT NULL
-- - Seller items: (bodega_id, seller_part_number, warehouse_location) WHERE part_id IS NULL
ALTER TABLE warehouse_inventory ALTER TABLE warehouse_inventory
DROP CONSTRAINT IF EXISTS warehouse_inventory_user_id_part_id_warehouse_location_key; DROP CONSTRAINT IF EXISTS warehouse_inventory_user_id_part_id_warehouse_location_key;
@@ -52,7 +48,6 @@ ALTER TABLE warehouse_inventory
); );
-- ─── 3. WAREHOUSE_INVENTORY — search indexes ───────────────────────── -- ─── 3. WAREHOUSE_INVENTORY — search indexes ─────────────────────────
CREATE INDEX IF NOT EXISTS idx_wi_seller_pn CREATE INDEX IF NOT EXISTS idx_wi_seller_pn
ON warehouse_inventory (bodega_id, seller_part_number) ON warehouse_inventory (bodega_id, seller_part_number)
WHERE part_id IS NULL; WHERE part_id IS NULL;
@@ -68,20 +63,25 @@ USING gin (to_tsvector('spanish',
COALESCE(seller_part_name, '') || ' ' || COALESCE(seller_part_number, '') COALESCE(seller_part_name, '') || ' ' || COALESCE(seller_part_number, '')
)) ))
WHERE part_id IS NULL; WHERE part_id IS NULL;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'purchase_order_items') THEN
-- ─── 4. PURCHASE_ORDER_ITEMS — make part_id nullable ───────────────── -- ─── 4. PURCHASE_ORDER_ITEMS — make part_id nullable ─────────────────
IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'purchase_order_items' AND column_name = 'part_id') THEN
ALTER TABLE purchase_order_items ALTER TABLE purchase_order_items
ALTER COLUMN part_id DROP NOT NULL; ALTER COLUMN part_id DROP NOT NULL;
END IF;
-- Add a flag so seller listings can be distinguished in POs -- Add a flag so seller listings can be distinguished in POs
ALTER TABLE purchase_order_items ALTER TABLE purchase_order_items
ADD COLUMN IF NOT EXISTS is_seller_listing BOOLEAN NOT NULL DEFAULT FALSE; ADD COLUMN IF NOT EXISTS is_seller_listing BOOLEAN NOT NULL DEFAULT FALSE;
END IF;
END $$;
-- ─── 5. Back-compat: ensure existing rows are valid ────────────────── -- ─── 5. Back-compat: ensure existing rows are valid ──────────────────
DO $$
-- Existing rows should have part_id set and seller_part_number NULL. BEGIN
-- If any row violates the new check, this will fail loudly. IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'warehouse_inventory') THEN
UPDATE warehouse_inventory UPDATE warehouse_inventory
SET seller_part_number = NULL SET seller_part_number = NULL
WHERE part_id IS NOT NULL AND seller_part_number IS NOT NULL; WHERE part_id IS NOT NULL AND seller_part_number IS NOT NULL;
@@ -89,3 +89,5 @@ WHERE part_id IS NOT NULL AND seller_part_number IS NOT NULL;
UPDATE warehouse_inventory UPDATE warehouse_inventory
SET part_id = NULL SET part_id = NULL
WHERE part_id IS NULL AND seller_part_number IS NULL; WHERE part_id IS NULL AND seller_part_number IS NULL;
END IF;
END $$;

View File

@@ -1,11 +1,15 @@
-- : SKIP
-- Migration v3.3: Materialized view part_vehicle_preview -- Migration v3.3: Materialized view part_vehicle_preview
-- Purpose: Pre-compute the "most recent vehicle" per part to eliminate -- Purpose: Pre-compute the "most recent vehicle" per part to eliminate
-- DISTINCT ON + 4 JOINs over vehicle_parts (254 GB, 2B+ rows) at query time. -- DISTINCT ON + 4 JOINs over vehicle_parts (254 GB, 2B+ rows) at query time.
-- --
-- Notes: -- NOTE: This migration targets the vehicle_database, not tenant databases.
-- - CREATE MATERIALIZED VIEW without CONCURRENTLY (first creation). -- The runner skips files marked with ': SKIP' on the first line.
-- - REFRESH MATERIALIZED VIEW CONCURRENTLY is possible after the unique index exists. -- To apply manually on the vehicle database, run:
-- - Run with statement_timeout = 0; this may take hours on first creation. --
-- psql <vehicle_db> -f pos/migrations/v3.3_materialized_view.sql
--
-- (Remove the ': SKIP' line above before manual execution.)
SET statement_timeout = 0; SET statement_timeout = 0;
@@ -26,6 +30,3 @@ ORDER BY vp.part_id, y.year_car DESC;
CREATE UNIQUE INDEX idx_pvp_part ON part_vehicle_preview(part_id); CREATE UNIQUE INDEX idx_pvp_part ON part_vehicle_preview(part_id);
CREATE INDEX idx_pvp_brand ON part_vehicle_preview(name_brand); CREATE INDEX idx_pvp_brand ON part_vehicle_preview(name_brand);
-- Grant select to application roles if needed
-- GRANT SELECT ON part_vehicle_preview TO nexus_app;

View File

@@ -1,10 +1,21 @@
-- : SKIP
-- ============================================================ -- ============================================================
-- v3.5 WhatsApp State Machine -- v3.5 WhatsApp State Machine
-- Reorganización del chatbot de AI libre a flujo estructurado -- Reorganización del chatbot de AI libre a flujo estructurado
--
-- NOTE: This migration requires the WhatsApp tables (whatsapp_sessions,
-- whatsapp_messages) to be present. Tenant DBs without WhatsApp enabled
-- should skip this file.
-- Marked with ': SKIP' so the runner skips it unless WhatsApp is configured.
-- To apply manually on a tenant with WhatsApp tables:
-- psql <tenant_db> -f pos/migrations/v3.5_whatsapp_state_machine.sql
-- (Remove the ': SKIP' line above before manual execution.)
-- ============================================================ -- ============================================================
DO $$
BEGIN
-- 1. Extender whatsapp_sessions con estado y contexto -- 1. Extender whatsapp_sessions con estado y contexto
-- --------------------------------------------------- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'whatsapp_sessions') THEN
ALTER TABLE whatsapp_sessions ALTER TABLE whatsapp_sessions
ADD COLUMN IF NOT EXISTS state VARCHAR(50) DEFAULT 'idle', ADD COLUMN IF NOT EXISTS state VARCHAR(50) DEFAULT 'idle',
ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}', ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}',
@@ -13,13 +24,13 @@ ADD COLUMN IF NOT EXISTS branch_id INTEGER REFERENCES branches(id),
ADD COLUMN IF NOT EXISTS learning_cycle INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS learning_cycle INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(); ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
-- Índices para lookups rápidos de sesión
CREATE INDEX IF NOT EXISTS idx_wa_sessions_state ON whatsapp_sessions(state); CREATE INDEX IF NOT EXISTS idx_wa_sessions_state ON whatsapp_sessions(state);
CREATE INDEX IF NOT EXISTS idx_wa_sessions_customer ON whatsapp_sessions(customer_id); CREATE INDEX IF NOT EXISTS idx_wa_sessions_customer ON whatsapp_sessions(customer_id);
CREATE INDEX IF NOT EXISTS idx_wa_sessions_updated ON whatsapp_sessions(updated_at); CREATE INDEX IF NOT EXISTS idx_wa_sessions_updated ON whatsapp_sessions(updated_at);
END IF;
-- 2. Tabla de vínculo persistente WA ID ↔ Cliente -- 2. Tabla de vínculo persistente WA ID ↔ Cliente
-- ------------------------------------------------ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'customers') THEN
CREATE TABLE IF NOT EXISTS wa_customer_links ( CREATE TABLE IF NOT EXISTS wa_customer_links (
phone VARCHAR(50) PRIMARY KEY, phone VARCHAR(50) PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id), customer_id INTEGER NOT NULL REFERENCES customers(id),
@@ -29,7 +40,6 @@ CREATE TABLE IF NOT EXISTS wa_customer_links (
CREATE INDEX IF NOT EXISTS idx_wa_cust_link_customer ON wa_customer_links(customer_id); CREATE INDEX IF NOT EXISTS idx_wa_cust_link_customer ON wa_customer_links(customer_id);
-- Trigger para updated_at
CREATE OR REPLACE FUNCTION update_wa_link_timestamp() CREATE OR REPLACE FUNCTION update_wa_link_timestamp()
RETURNS TRIGGER AS $$ RETURNS TRIGGER AS $$
BEGIN BEGIN
@@ -42,9 +52,12 @@ DROP TRIGGER IF EXISTS trg_wa_link_updated ON wa_customer_links;
CREATE TRIGGER trg_wa_link_updated CREATE TRIGGER trg_wa_link_updated
BEFORE UPDATE ON wa_customer_links BEFORE UPDATE ON wa_customer_links
FOR EACH ROW EXECUTE FUNCTION update_wa_link_timestamp(); FOR EACH ROW EXECUTE FUNCTION update_wa_link_timestamp();
END IF;
-- 3. Tabla de sesiones de aprendizaje (piezas no resueltas) -- 3. Tabla de sesiones de aprendizaje (piezas no resueltas)
-- --------------------------------------------------------- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'customers')
AND EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'inventory')
AND EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'sales') THEN
CREATE TABLE IF NOT EXISTS wa_learning_sessions ( CREATE TABLE IF NOT EXISTS wa_learning_sessions (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
phone VARCHAR(50) NOT NULL, phone VARCHAR(50) NOT NULL,
@@ -62,9 +75,10 @@ CREATE INDEX IF NOT EXISTS idx_wa_learn_phone ON wa_learning_sessions(phone);
CREATE INDEX IF NOT EXISTS idx_wa_learn_status ON wa_learning_sessions(status); CREATE INDEX IF NOT EXISTS idx_wa_learn_status ON wa_learning_sessions(status);
CREATE INDEX IF NOT EXISTS idx_wa_learn_customer ON wa_learning_sessions(customer_id); CREATE INDEX IF NOT EXISTS idx_wa_learn_customer ON wa_learning_sessions(customer_id);
CREATE INDEX IF NOT EXISTS idx_wa_learn_created ON wa_learning_sessions(created_at); CREATE INDEX IF NOT EXISTS idx_wa_learn_created ON wa_learning_sessions(created_at);
END IF;
-- 4. Tabla de configuración de envío por sucursal -- 4. Tabla de configuración de envío por sucursal
-- ------------------------------------------------ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'branches') THEN
CREATE TABLE IF NOT EXISTS branch_delivery_config ( CREATE TABLE IF NOT EXISTS branch_delivery_config (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
branch_id INTEGER NOT NULL UNIQUE REFERENCES branches(id), branch_id INTEGER NOT NULL UNIQUE REFERENCES branches(id),
@@ -77,15 +91,18 @@ CREATE TABLE IF NOT EXISTS branch_delivery_config (
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
); );
END IF;
-- 5. Agregar push_name a whatsapp_messages (schema drift existente) -- 5. Agregar push_name a whatsapp_messages
-- ------------------------------------------------------------------ IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'whatsapp_messages') THEN
ALTER TABLE whatsapp_messages ALTER TABLE whatsapp_messages
ADD COLUMN IF NOT EXISTS push_name VARCHAR(200); ADD COLUMN IF NOT EXISTS push_name VARCHAR(200);
END IF;
-- 6. Migrar datos existentes: vincular por teléfono -- 6. Migrar datos existentes
-- -------------------------------------------------- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'whatsapp_sessions')
-- Intentar vincular sesiones WA existentes con customers por teléfono AND EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'wa_customer_links')
AND EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'customers') THEN
INSERT INTO wa_customer_links (phone, customer_id) INSERT INTO wa_customer_links (phone, customer_id)
SELECT ws.phone, c.id SELECT ws.phone, c.id
FROM whatsapp_sessions ws FROM whatsapp_sessions ws
@@ -93,8 +110,9 @@ JOIN customers c ON c.phone = ws.phone
WHERE ws.phone IS NOT NULL AND c.phone IS NOT NULL WHERE ws.phone IS NOT NULL AND c.phone IS NOT NULL
ON CONFLICT (phone) DO NOTHING; ON CONFLICT (phone) DO NOTHING;
-- Actualizar customer_id en whatsapp_sessions desde el vínculo
UPDATE whatsapp_sessions ws UPDATE whatsapp_sessions ws
SET customer_id = wcl.customer_id SET customer_id = wcl.customer_id
FROM wa_customer_links wcl FROM wa_customer_links wcl
WHERE ws.phone = wcl.phone AND ws.customer_id IS NULL; WHERE ws.phone = wcl.phone AND ws.customer_id IS NULL;
END IF;
END $$;

View File

@@ -1,6 +1,8 @@
-- : SKIP
-- v3.9_supplier_catalog_prices.sql -- v3.9_supplier_catalog_prices.sql
-- Per-tenant supplier pricing for items in the master supplier_catalog. -- Per-tenant supplier pricing for items in the master supplier_catalog.
-- This table lives in the master DB and is joined by tenant_id. -- This table lives in the master DB and is joined by tenant_id.
-- Apply manually to the master database.
CREATE TABLE IF NOT EXISTS supplier_catalog_prices ( CREATE TABLE IF NOT EXISTS supplier_catalog_prices (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,