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:
@@ -77,11 +77,19 @@ def apply_migration(db_name, version):
|
||||
print(f" ERROR: Migration file not found: {filepath}")
|
||||
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)
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
with open(filepath) as f:
|
||||
cur.execute(f.read())
|
||||
cur.execute(sql)
|
||||
conn.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
-- 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
|
||||
--
|
||||
-- Makes warehouse_inventory part_id nullable and adds seller-defined
|
||||
@@ -8,42 +8,38 @@
|
||||
-- Existing OEM-matched listings are untouched.
|
||||
-- ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
-- ─── 1. WAREHOUSE_INVENTORY — add seller listing columns ─────────────
|
||||
|
||||
ALTER TABLE warehouse_inventory
|
||||
DO $$
|
||||
BEGIN
|
||||
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_name VARCHAR(300),
|
||||
ADD COLUMN IF NOT EXISTS seller_category VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS tenant_inventory_id INTEGER;
|
||||
|
||||
-- Make part_id nullable so seller listings (without catalog match) can exist
|
||||
ALTER TABLE warehouse_inventory ALTER COLUMN part_id DROP NOT NULL;
|
||||
-- Make part_id nullable so seller listings (without catalog match) can exist
|
||||
ALTER TABLE warehouse_inventory ALTER COLUMN part_id DROP NOT NULL;
|
||||
|
||||
-- ─── 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
|
||||
-- ─── 2. WAREHOUSE_INVENTORY — drop old unique, add partial uniques ───
|
||||
ALTER TABLE warehouse_inventory
|
||||
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
|
||||
ON warehouse_inventory(bodega_id, part_id, warehouse_location)
|
||||
WHERE part_id IS NOT NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_wi_unique_oem
|
||||
ON warehouse_inventory(bodega_id, part_id, warehouse_location)
|
||||
WHERE part_id IS NOT NULL;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_wi_unique_seller
|
||||
ON warehouse_inventory(bodega_id, seller_part_number, warehouse_location)
|
||||
WHERE part_id IS NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_wi_unique_seller
|
||||
ON warehouse_inventory(bodega_id, seller_part_number, warehouse_location)
|
||||
WHERE part_id IS NULL;
|
||||
|
||||
-- Ensure every row has either part_id or seller_part_number
|
||||
ALTER TABLE warehouse_inventory
|
||||
-- Ensure every row has either part_id or seller_part_number
|
||||
ALTER TABLE warehouse_inventory
|
||||
DROP CONSTRAINT IF EXISTS chk_wi_part_or_seller;
|
||||
|
||||
ALTER TABLE warehouse_inventory
|
||||
ALTER TABLE warehouse_inventory
|
||||
ADD CONSTRAINT chk_wi_part_or_seller
|
||||
CHECK (
|
||||
(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)
|
||||
);
|
||||
|
||||
-- ─── 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
|
||||
ON warehouse_inventory (bodega_id, seller_part_number)
|
||||
WHERE part_id IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_wi_seller_category
|
||||
ON warehouse_inventory (seller_category)
|
||||
WHERE part_id IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_wi_seller_category
|
||||
ON warehouse_inventory (seller_category)
|
||||
WHERE part_id IS NULL;
|
||||
|
||||
-- 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',
|
||||
-- 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, '')
|
||||
))
|
||||
WHERE part_id IS NULL;
|
||||
))
|
||||
WHERE part_id IS NULL;
|
||||
END IF;
|
||||
|
||||
-- ─── 4. PURCHASE_ORDER_ITEMS — make part_id nullable ─────────────────
|
||||
|
||||
ALTER TABLE purchase_order_items
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'purchase_order_items') THEN
|
||||
-- ─── 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 COLUMN part_id DROP NOT NULL;
|
||||
END IF;
|
||||
|
||||
-- Add a flag so seller listings can be distinguished in POs
|
||||
ALTER TABLE purchase_order_items
|
||||
-- Add a flag so seller listings can be distinguished in POs
|
||||
ALTER TABLE purchase_order_items
|
||||
ADD COLUMN IF NOT EXISTS is_seller_listing BOOLEAN NOT NULL DEFAULT FALSE;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ─── 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.
|
||||
-- If any row violates the new check, this will fail loudly.
|
||||
UPDATE warehouse_inventory
|
||||
SET seller_part_number = NULL
|
||||
WHERE part_id IS NOT NULL AND seller_part_number IS NOT NULL;
|
||||
|
||||
UPDATE warehouse_inventory
|
||||
SET part_id = NULL
|
||||
WHERE part_id IS NULL AND seller_part_number IS NULL;
|
||||
UPDATE warehouse_inventory
|
||||
SET part_id = NULL
|
||||
WHERE part_id IS NULL AND seller_part_number IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
-- : SKIP
|
||||
-- Migration v3.3: Materialized view part_vehicle_preview
|
||||
-- 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.
|
||||
--
|
||||
-- Notes:
|
||||
-- - CREATE MATERIALIZED VIEW without CONCURRENTLY (first creation).
|
||||
-- - REFRESH MATERIALIZED VIEW CONCURRENTLY is possible after the unique index exists.
|
||||
-- - Run with statement_timeout = 0; this may take hours on first creation.
|
||||
-- NOTE: This migration targets the vehicle_database, not tenant databases.
|
||||
-- The runner skips files marked with ': SKIP' on the first line.
|
||||
-- To apply manually on the vehicle database, run:
|
||||
--
|
||||
-- psql <vehicle_db> -f pos/migrations/v3.3_materialized_view.sql
|
||||
--
|
||||
-- (Remove the ': SKIP' line above before manual execution.)
|
||||
|
||||
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 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;
|
||||
|
||||
@@ -1,51 +1,64 @@
|
||||
-- : SKIP
|
||||
-- ============================================================
|
||||
-- v3.5 WhatsApp State Machine
|
||||
-- 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
|
||||
-- ---------------------------------------------------
|
||||
ALTER TABLE whatsapp_sessions
|
||||
ADD COLUMN IF NOT EXISTS state VARCHAR(50) DEFAULT 'idle',
|
||||
ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS customer_id INTEGER REFERENCES customers(id),
|
||||
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();
|
||||
DO $$
|
||||
BEGIN
|
||||
-- 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
|
||||
ADD COLUMN IF NOT EXISTS state VARCHAR(50) DEFAULT 'idle',
|
||||
ADD COLUMN IF NOT EXISTS state_data JSONB DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS customer_id INTEGER REFERENCES customers(id),
|
||||
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_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_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_updated ON whatsapp_sessions(updated_at);
|
||||
END IF;
|
||||
|
||||
-- 2. Tabla de vínculo persistente WA ID ↔ Cliente
|
||||
-- ------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wa_customer_links (
|
||||
-- 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 (
|
||||
phone VARCHAR(50) PRIMARY KEY,
|
||||
customer_id INTEGER NOT NULL REFERENCES customers(id),
|
||||
created_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()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
CREATE OR REPLACE FUNCTION update_wa_link_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_wa_link_updated ON wa_customer_links;
|
||||
CREATE TRIGGER trg_wa_link_updated
|
||||
DROP TRIGGER IF EXISTS trg_wa_link_updated ON wa_customer_links;
|
||||
CREATE TRIGGER trg_wa_link_updated
|
||||
BEFORE UPDATE ON wa_customer_links
|
||||
FOR EACH ROW EXECUTE FUNCTION update_wa_link_timestamp();
|
||||
END IF;
|
||||
|
||||
-- 3. Tabla de sesiones de aprendizaje (piezas no resueltas)
|
||||
-- ---------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS wa_learning_sessions (
|
||||
-- 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 (
|
||||
id SERIAL PRIMARY KEY,
|
||||
phone VARCHAR(50) NOT NULL,
|
||||
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),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
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_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_created ON wa_learning_sessions(created_at);
|
||||
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_customer ON wa_learning_sessions(customer_id);
|
||||
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
|
||||
-- ------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS branch_delivery_config (
|
||||
-- 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 (
|
||||
id SERIAL PRIMARY KEY,
|
||||
branch_id INTEGER NOT NULL UNIQUE REFERENCES branches(id),
|
||||
is_enabled BOOLEAN DEFAULT FALSE,
|
||||
@@ -76,25 +90,29 @@ CREATE TABLE IF NOT EXISTS branch_delivery_config (
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- 5. Agregar push_name a whatsapp_messages (schema drift existente)
|
||||
-- ------------------------------------------------------------------
|
||||
ALTER TABLE whatsapp_messages
|
||||
ADD COLUMN IF NOT EXISTS push_name VARCHAR(200);
|
||||
-- 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
|
||||
ADD COLUMN IF NOT EXISTS push_name VARCHAR(200);
|
||||
END IF;
|
||||
|
||||
-- 6. Migrar datos existentes: vincular por teléfono
|
||||
-- --------------------------------------------------
|
||||
-- Intentar vincular sesiones WA existentes con customers por teléfono
|
||||
INSERT INTO wa_customer_links (phone, customer_id)
|
||||
SELECT ws.phone, c.id
|
||||
FROM whatsapp_sessions ws
|
||||
JOIN customers c ON c.phone = ws.phone
|
||||
WHERE ws.phone IS NOT NULL AND c.phone IS NOT NULL
|
||||
ON CONFLICT (phone) DO NOTHING;
|
||||
-- 6. Migrar datos existentes
|
||||
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'whatsapp_sessions')
|
||||
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)
|
||||
SELECT ws.phone, c.id
|
||||
FROM whatsapp_sessions ws
|
||||
JOIN customers c ON c.phone = ws.phone
|
||||
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
|
||||
SET customer_id = wcl.customer_id
|
||||
FROM wa_customer_links wcl
|
||||
WHERE ws.phone = wcl.phone AND ws.customer_id IS NULL;
|
||||
UPDATE whatsapp_sessions ws
|
||||
SET customer_id = wcl.customer_id
|
||||
FROM wa_customer_links wcl
|
||||
WHERE ws.phone = wcl.phone AND ws.customer_id IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
-- : SKIP
|
||||
-- v3.9_supplier_catalog_prices.sql
|
||||
-- Per-tenant supplier pricing for items in the master supplier_catalog.
|
||||
-- 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 (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
Reference in New Issue
Block a user