-- ============================================================ -- 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;