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,42 +8,38 @@
-- Existing OEM-matched listings are untouched. -- Existing OEM-matched listings are untouched.
-- ═══════════════════════════════════════════════════════════════════════ -- ═══════════════════════════════════════════════════════════════════════
-- ─── 1. WAREHOUSE_INVENTORY — add seller listing columns ───────────── DO $$
BEGIN
ALTER TABLE warehouse_inventory IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'warehouse_inventory') THEN
-- ─── 1. WAREHOUSE_INVENTORY — add seller listing columns ─────────────
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),
ADD COLUMN IF NOT EXISTS seller_category VARCHAR(100), ADD COLUMN IF NOT EXISTS seller_category VARCHAR(100),
ADD COLUMN IF NOT EXISTS tenant_inventory_id INTEGER; ADD COLUMN IF NOT EXISTS tenant_inventory_id INTEGER;
-- Make part_id nullable so seller listings (without catalog match) can exist -- Make part_id nullable so seller listings (without catalog match) can exist
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 ───
ALTER TABLE warehouse_inventory
-- 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
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;
DROP INDEX IF EXISTS idx_wi_unique_composite; DROP INDEX IF EXISTS idx_wi_unique_composite;
CREATE UNIQUE INDEX IF NOT EXISTS idx_wi_unique_oem CREATE UNIQUE INDEX IF NOT EXISTS idx_wi_unique_oem
ON warehouse_inventory(bodega_id, part_id, warehouse_location) ON warehouse_inventory(bodega_id, part_id, warehouse_location)
WHERE part_id IS NOT NULL; WHERE part_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_wi_unique_seller CREATE UNIQUE INDEX IF NOT EXISTS idx_wi_unique_seller
ON warehouse_inventory(bodega_id, seller_part_number, warehouse_location) ON warehouse_inventory(bodega_id, seller_part_number, warehouse_location)
WHERE part_id IS NULL; WHERE part_id IS NULL;
-- Ensure every row has either part_id or seller_part_number -- Ensure every row has either part_id or seller_part_number
ALTER TABLE warehouse_inventory ALTER TABLE warehouse_inventory
DROP CONSTRAINT IF EXISTS chk_wi_part_or_seller; DROP CONSTRAINT IF EXISTS chk_wi_part_or_seller;
ALTER TABLE warehouse_inventory ALTER TABLE warehouse_inventory
ADD CONSTRAINT chk_wi_part_or_seller ADD CONSTRAINT chk_wi_part_or_seller
CHECK ( CHECK (
(part_id IS NOT NULL AND seller_part_number IS NULL) (part_id IS NOT NULL AND seller_part_number IS NULL)
@@ -51,41 +47,47 @@ ALTER TABLE warehouse_inventory
(part_id IS NULL AND seller_part_number IS NOT NULL) (part_id IS NULL AND seller_part_number IS NOT NULL)
); );
-- ─── 3. WAREHOUSE_INVENTORY — search indexes ───────────────────────── -- ─── 3. WAREHOUSE_INVENTORY — search indexes ─────────────────────────
CREATE INDEX IF NOT EXISTS idx_wi_seller_pn
ON warehouse_inventory (bodega_id, seller_part_number)
WHERE part_id IS NULL;
CREATE INDEX IF NOT EXISTS idx_wi_seller_pn CREATE INDEX IF NOT EXISTS idx_wi_seller_category
ON warehouse_inventory (bodega_id, seller_part_number) ON warehouse_inventory (seller_category)
WHERE part_id IS NULL; WHERE part_id IS NULL;
CREATE INDEX IF NOT EXISTS idx_wi_seller_category -- GIN index for text search on seller listings
ON warehouse_inventory (seller_category) CREATE INDEX IF NOT EXISTS idx_wi_seller_search
WHERE part_id IS NULL; ON warehouse_inventory
USING gin (to_tsvector('spanish',
-- GIN index for text search on seller listings
CREATE INDEX IF NOT EXISTS idx_wi_seller_search
ON warehouse_inventory
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;
-- ─── 4. PURCHASE_ORDER_ITEMS — make part_id nullable ───────────────── IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'purchase_order_items') THEN
-- ─── 4. PURCHASE_ORDER_ITEMS — make part_id nullable ─────────────────
ALTER TABLE purchase_order_items 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 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 $$
BEGIN
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'warehouse_inventory') THEN
UPDATE warehouse_inventory
SET seller_part_number = NULL
WHERE part_id IS NOT NULL AND seller_part_number IS NOT NULL;
-- Existing rows should have part_id set and seller_part_number NULL. UPDATE warehouse_inventory
-- If any row violates the new check, this will fail loudly. SET part_id = NULL
UPDATE warehouse_inventory WHERE part_id IS NULL AND seller_part_number IS NULL;
SET seller_part_number = NULL END IF;
WHERE part_id IS NOT NULL AND seller_part_number IS NOT NULL; END $$;
UPDATE warehouse_inventory
SET part_id = NULL
WHERE part_id IS NULL AND seller_part_number IS NULL;

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,51 +1,64 @@
-- : 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.)
-- ============================================================ -- ============================================================
-- 1. Extender whatsapp_sessions con estado y contexto DO $$
-- --------------------------------------------------- BEGIN
ALTER TABLE whatsapp_sessions -- 1. Extender whatsapp_sessions con estado y contexto
ADD COLUMN IF NOT EXISTS state VARCHAR(50) DEFAULT 'idle', IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'whatsapp_sessions') THEN
ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}', ALTER TABLE whatsapp_sessions
ADD COLUMN IF NOT EXISTS customer_id INTEGER REFERENCES customers(id), ADD COLUMN IF NOT EXISTS state VARCHAR(50) DEFAULT 'idle',
ADD COLUMN IF NOT EXISTS branch_id INTEGER REFERENCES branches(id), ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}',
ADD COLUMN IF NOT EXISTS learning_cycle INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS customer_id INTEGER REFERENCES customers(id),
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(); 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 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),
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
); );
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
NEW.updated_at = NOW(); NEW.updated_at = NOW();
RETURN NEW; RETURN NEW;
END; END;
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_wa_link_updated ON wa_customer_links; 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')
CREATE TABLE IF NOT EXISTS wa_learning_sessions ( 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 (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
phone VARCHAR(50) NOT NULL, phone VARCHAR(50) NOT NULL,
customer_id INTEGER REFERENCES customers(id), customer_id INTEGER REFERENCES customers(id),
@@ -56,16 +69,17 @@ CREATE TABLE IF NOT EXISTS wa_learning_sessions (
resolution_sale_id INTEGER REFERENCES sales(id), resolution_sale_id INTEGER REFERENCES sales(id),
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
resolved_at TIMESTAMPTZ resolved_at TIMESTAMPTZ
); );
CREATE INDEX IF NOT EXISTS idx_wa_learn_phone ON wa_learning_sessions(phone); 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),
is_enabled BOOLEAN DEFAULT FALSE, is_enabled BOOLEAN DEFAULT FALSE,
@@ -76,25 +90,29 @@ CREATE TABLE IF NOT EXISTS branch_delivery_config (
notes TEXT, notes TEXT,
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')
INSERT INTO wa_customer_links (phone, customer_id) AND EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'customers') THEN
SELECT ws.phone, c.id INSERT INTO wa_customer_links (phone, customer_id)
FROM whatsapp_sessions ws SELECT ws.phone, c.id
JOIN customers c ON c.phone = ws.phone FROM whatsapp_sessions ws
WHERE ws.phone IS NOT NULL AND c.phone IS NOT NULL JOIN customers c ON c.phone = ws.phone
ON CONFLICT (phone) DO NOTHING; WHERE ws.phone IS NOT NULL AND c.phone IS NOT NULL
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,