learning_ai_invt_trdg/backend/schema/008_schema_gap_backfill.sql

227 lines
6.5 KiB
PL/PgSQL

-- ============================================================
-- Migration 008: Schema Gap Backfill (Idempotent)
-- Date: 2026-02-15
-- Purpose:
-- 1) Patch common missing columns on legacy DBs
-- 2) Keep orders.qty and orders.quantity in sync
-- 3) Ensure trade lifecycle traceability fields exist
-- 4) Add bot_snapshots table for rolling bot_state backups
-- ============================================================
BEGIN;
-- ------------------------------------------------------------
-- A) trade_history compatibility
-- ------------------------------------------------------------
ALTER TABLE IF EXISTS trade_history
ADD COLUMN IF NOT EXISTS trade_id text;
ALTER TABLE IF EXISTS trade_history
ADD COLUMN IF NOT EXISTS source text;
-- Ensure source has valid values and no nulls.
UPDATE trade_history
SET source = CASE
WHEN upper(coalesce(source, '')) IN ('BOT', 'MANUAL') THEN upper(source)
WHEN profile_id IS NOT NULL THEN 'BOT'
ELSE 'MANUAL'
END
WHERE source IS NULL
OR btrim(source) = ''
OR upper(source) NOT IN ('BOT', 'MANUAL');
ALTER TABLE IF EXISTS trade_history
ALTER COLUMN source SET DEFAULT 'BOT';
ALTER TABLE IF EXISTS trade_history
ALTER COLUMN source SET NOT NULL;
CREATE INDEX IF NOT EXISTS idx_trade_history_trade_id
ON trade_history (trade_id);
CREATE INDEX IF NOT EXISTS idx_trade_history_source
ON trade_history (source);
CREATE INDEX IF NOT EXISTS idx_trade_history_profile_trade_id_created
ON trade_history (profile_id, trade_id, created_at DESC)
WHERE trade_id IS NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_trade_history_trade_id_not_blank'
AND conrelid = 'trade_history'::regclass
) THEN
ALTER TABLE trade_history
ADD CONSTRAINT chk_trade_history_trade_id_not_blank
CHECK (trade_id IS NULL OR btrim(trade_id) <> '')
NOT VALID;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_trade_history_trade_id_format'
AND conrelid = 'trade_history'::regclass
) THEN
ALTER TABLE trade_history
ADD CONSTRAINT chk_trade_history_trade_id_format
CHECK (trade_id IS NULL OR trade_id LIKE 'TRD-%')
NOT VALID;
END IF;
END $$;
-- ------------------------------------------------------------
-- B) orders compatibility
-- ------------------------------------------------------------
ALTER TABLE IF EXISTS orders
ADD COLUMN IF NOT EXISTS trade_id text;
ALTER TABLE IF EXISTS orders
ADD COLUMN IF NOT EXISTS action text;
ALTER TABLE IF EXISTS orders
ADD COLUMN IF NOT EXISTS source text;
ALTER TABLE IF EXISTS orders
ADD COLUMN IF NOT EXISTS quantity numeric;
-- Source normalization.
UPDATE orders
SET source = CASE
WHEN upper(coalesce(source, '')) IN ('BOT', 'MANUAL') THEN upper(source)
WHEN profile_id IS NOT NULL THEN 'BOT'
ELSE 'MANUAL'
END
WHERE source IS NULL
OR btrim(source) = ''
OR upper(source) NOT IN ('BOT', 'MANUAL');
ALTER TABLE IF EXISTS orders
ALTER COLUMN source SET DEFAULT 'BOT';
ALTER TABLE IF EXISTS orders
ALTER COLUMN source SET NOT NULL;
-- Keep qty/quantity aligned for legacy + current clients.
UPDATE orders
SET quantity = qty
WHERE quantity IS NULL
AND qty IS NOT NULL;
UPDATE orders
SET qty = quantity
WHERE qty IS NULL
AND quantity IS NOT NULL;
CREATE OR REPLACE FUNCTION sync_orders_qty_quantity()
RETURNS trigger AS $$
BEGIN
IF NEW.qty IS NULL AND NEW.quantity IS NOT NULL THEN
NEW.qty := NEW.quantity;
ELSIF NEW.quantity IS NULL AND NEW.qty IS NOT NULL THEN
NEW.quantity := NEW.qty;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_orders_sync_qty_quantity ON orders;
CREATE TRIGGER trg_orders_sync_qty_quantity
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION sync_orders_qty_quantity();
CREATE INDEX IF NOT EXISTS idx_orders_trade_id
ON orders (trade_id);
CREATE INDEX IF NOT EXISTS idx_orders_source
ON orders (source);
CREATE INDEX IF NOT EXISTS idx_orders_profile_trade_id_created
ON orders (profile_id, trade_id, created_at DESC)
WHERE trade_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_orders_profile_trade_id_action_created
ON orders (profile_id, trade_id, action, created_at DESC)
WHERE trade_id IS NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_orders_action_lifecycle'
AND conrelid = 'orders'::regclass
) THEN
ALTER TABLE orders
ADD CONSTRAINT chk_orders_action_lifecycle
CHECK (action IS NULL OR action IN ('ENTRY', 'EXIT'))
NOT VALID;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_orders_trade_id_not_blank'
AND conrelid = 'orders'::regclass
) THEN
ALTER TABLE orders
ADD CONSTRAINT chk_orders_trade_id_not_blank
CHECK (trade_id IS NULL OR btrim(trade_id) <> '')
NOT VALID;
END IF;
END $$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'chk_orders_trade_id_format'
AND conrelid = 'orders'::regclass
) THEN
ALTER TABLE orders
ADD CONSTRAINT chk_orders_trade_id_format
CHECK (trade_id IS NULL OR trade_id LIKE 'TRD-%')
NOT VALID;
END IF;
END $$;
-- ------------------------------------------------------------
-- C) bot_snapshots table for rolling state backups
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS bot_snapshots (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
state_json jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_bot_snapshots_user_created
ON bot_snapshots (user_id, created_at DESC);
COMMIT;
-- ------------------------------------------------------------
-- Post-run checks (optional)
-- ------------------------------------------------------------
-- SELECT column_name FROM information_schema.columns
-- WHERE table_schema='public' AND table_name='orders'
-- AND column_name IN ('qty','quantity','trade_id','action','source');
--
-- SELECT column_name FROM information_schema.columns
-- WHERE table_schema='public' AND table_name='trade_history'
-- AND column_name IN ('trade_id','source');
--
-- SELECT count(*) AS snapshots FROM bot_snapshots;