227 lines
6.5 KiB
PL/PgSQL
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;
|