trading-platform-database-v2/ddl/schemas/feature_flags/tables/01-flags.sql
Adrian Flores Cortes e4d39b1293 [Sprint-2] feat: Add Feature Flags schema
Created feature_flags schema with:
- flags table: Global feature flag definitions
- user_flags table: User-level overrides
- evaluations table: Evaluation history for analytics
- evaluate_flag() function: PL/pgSQL evaluation with priorities

Features:
- Percentage-based rollout
- User override support
- Targeting rules (plan, role)
- Expiration support
- Initial flags for trading platform

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 15:38:38 -06:00

260 lines
9.7 KiB
PL/PgSQL

-- ============================================================================
-- Trading Platform - Feature Flags Schema
-- Created: Sprint 2 - TASK-2026-01-30-ANALISIS-INTEGRACION
-- Based on: template-saas SAAS-009 Feature Flags (simplified without multi-tenancy)
-- ============================================================================
-- Create schema if not exists
CREATE SCHEMA IF NOT EXISTS feature_flags;
-- ============================================================================
-- ENUMS
-- ============================================================================
-- Flag status enum
DO $$ BEGIN
CREATE TYPE feature_flags.flag_status AS ENUM ('disabled', 'enabled', 'percentage');
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
-- Rollout stage enum
DO $$ BEGIN
CREATE TYPE feature_flags.rollout_stage AS ENUM ('development', 'beta', 'production');
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
-- ============================================================================
-- TABLE: flags (Global feature flag definitions)
-- ============================================================================
CREATE TABLE IF NOT EXISTS feature_flags.flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Identification
code VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
description TEXT,
category VARCHAR(100) DEFAULT 'general',
-- Status and Rollout
status feature_flags.flag_status NOT NULL DEFAULT 'disabled',
rollout_stage feature_flags.rollout_stage NOT NULL DEFAULT 'development',
rollout_percentage INTEGER DEFAULT 0,
default_value BOOLEAN NOT NULL DEFAULT false,
-- Targeting (simplified - by plan or role)
targeting_rules JSONB DEFAULT '[]',
-- Example: [{"type": "plan", "operator": "in", "values": ["pro", "premium"]}]
-- Metadata
metadata JSONB DEFAULT '{}',
tags JSONB DEFAULT '[]',
-- Lifecycle
is_permanent BOOLEAN NOT NULL DEFAULT false,
expires_at TIMESTAMPTZ,
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_by VARCHAR(100),
-- Constraints
CONSTRAINT valid_flag_code CHECK (code ~ '^[a-z][a-z0-9_]*$'),
CONSTRAINT valid_percentage CHECK (rollout_percentage >= 0 AND rollout_percentage <= 100)
);
-- ============================================================================
-- TABLE: user_flags (User-level flag overrides)
-- ============================================================================
CREATE TABLE IF NOT EXISTS feature_flags.user_flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE,
-- Override
is_enabled BOOLEAN NOT NULL,
reason VARCHAR(500),
expires_at TIMESTAMPTZ,
-- Audit
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_by UUID,
-- Unique constraint per user/flag
CONSTRAINT unique_user_flag UNIQUE (user_id, flag_id)
);
-- ============================================================================
-- TABLE: evaluations (Evaluation history for analytics)
-- ============================================================================
CREATE TABLE IF NOT EXISTS feature_flags.evaluations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL,
-- Evaluation result
flag_code VARCHAR(100) NOT NULL,
result BOOLEAN NOT NULL,
evaluation_reason VARCHAR(100) NOT NULL,
-- Reasons: 'default', 'user_override', 'targeting_rule', 'percentage', 'expired'
-- Context
context JSONB DEFAULT '{}',
-- Timestamp
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ============================================================================
-- INDEXES
-- ============================================================================
-- Flags indexes
CREATE INDEX IF NOT EXISTS idx_flags_code ON feature_flags.flags(code);
CREATE INDEX IF NOT EXISTS idx_flags_status ON feature_flags.flags(status) WHERE status != 'disabled';
CREATE INDEX IF NOT EXISTS idx_flags_category ON feature_flags.flags(category);
-- User flags indexes
CREATE INDEX IF NOT EXISTS idx_user_flags_user ON feature_flags.user_flags(user_id);
CREATE INDEX IF NOT EXISTS idx_user_flags_flag ON feature_flags.user_flags(flag_id);
-- Evaluations indexes (partitioned by date for analytics)
CREATE INDEX IF NOT EXISTS idx_evaluations_flag ON feature_flags.evaluations(flag_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_evaluations_user ON feature_flags.evaluations(user_id, created_at DESC) WHERE user_id IS NOT NULL;
-- ============================================================================
-- FUNCTION: evaluate_flag
-- Evaluates a feature flag for a given user
-- ============================================================================
CREATE OR REPLACE FUNCTION feature_flags.evaluate_flag(
p_flag_code VARCHAR(100),
p_user_id UUID DEFAULT NULL
) RETURNS BOOLEAN AS $$
DECLARE
v_flag RECORD;
v_user_override RECORD;
v_result BOOLEAN;
v_reason VARCHAR(100);
BEGIN
-- Get flag definition
SELECT * INTO v_flag FROM feature_flags.flags WHERE code = p_flag_code;
IF NOT FOUND THEN
RETURN false;
END IF;
-- Check if expired
IF v_flag.expires_at IS NOT NULL AND v_flag.expires_at < NOW() THEN
v_result := false;
v_reason := 'expired';
-- Log evaluation
INSERT INTO feature_flags.evaluations (flag_id, user_id, flag_code, result, evaluation_reason)
VALUES (v_flag.id, p_user_id, p_flag_code, v_result, v_reason);
RETURN v_result;
END IF;
-- Check user override (highest priority)
IF p_user_id IS NOT NULL THEN
SELECT * INTO v_user_override
FROM feature_flags.user_flags
WHERE flag_id = v_flag.id
AND user_id = p_user_id
AND (expires_at IS NULL OR expires_at > NOW());
IF FOUND THEN
v_result := v_user_override.is_enabled;
v_reason := 'user_override';
-- Log evaluation
INSERT INTO feature_flags.evaluations (flag_id, user_id, flag_code, result, evaluation_reason)
VALUES (v_flag.id, p_user_id, p_flag_code, v_result, v_reason);
RETURN v_result;
END IF;
END IF;
-- Check flag status
CASE v_flag.status
WHEN 'disabled' THEN
v_result := false;
v_reason := 'default';
WHEN 'enabled' THEN
v_result := true;
v_reason := 'default';
WHEN 'percentage' THEN
-- Simple percentage-based rollout using user_id hash
IF p_user_id IS NOT NULL THEN
v_result := (abs(hashtext(p_user_id::text)) % 100) < v_flag.rollout_percentage;
ELSE
v_result := (random() * 100) < v_flag.rollout_percentage;
END IF;
v_reason := 'percentage';
ELSE
v_result := v_flag.default_value;
v_reason := 'default';
END CASE;
-- Log evaluation
INSERT INTO feature_flags.evaluations (flag_id, user_id, flag_code, result, evaluation_reason)
VALUES (v_flag.id, p_user_id, p_flag_code, v_result, v_reason);
RETURN v_result;
END;
$$ LANGUAGE plpgsql;
-- ============================================================================
-- TRIGGER: Update updated_at on flags
-- ============================================================================
CREATE OR REPLACE FUNCTION feature_flags.update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_flags_updated_at ON feature_flags.flags;
CREATE TRIGGER trg_flags_updated_at
BEFORE UPDATE ON feature_flags.flags
FOR EACH ROW EXECUTE FUNCTION feature_flags.update_timestamp();
DROP TRIGGER IF EXISTS trg_user_flags_updated_at ON feature_flags.user_flags;
CREATE TRIGGER trg_user_flags_updated_at
BEFORE UPDATE ON feature_flags.user_flags
FOR EACH ROW EXECUTE FUNCTION feature_flags.update_timestamp();
-- ============================================================================
-- COMMENTS
-- ============================================================================
COMMENT ON TABLE feature_flags.flags IS 'Global feature flag definitions';
COMMENT ON TABLE feature_flags.user_flags IS 'User-level feature flag overrides';
COMMENT ON TABLE feature_flags.evaluations IS 'Feature flag evaluation history for analytics';
COMMENT ON FUNCTION feature_flags.evaluate_flag IS 'Evaluates a feature flag for a given user with priority: user_override > percentage > default';
-- ============================================================================
-- INITIAL FLAGS (common flags for trading platform)
-- ============================================================================
INSERT INTO feature_flags.flags (code, name, description, category, status, rollout_stage, default_value)
VALUES
('enable_dark_mode', 'Dark Mode', 'Enable dark mode UI theme', 'ui', 'enabled', 'production', true),
('enable_advanced_charts', 'Advanced Charts', 'Enable advanced TradingView-like charts', 'trading', 'enabled', 'production', true),
('enable_ml_signals', 'ML Signals', 'Show ML prediction signals on charts', 'ml', 'enabled', 'production', true),
('enable_llm_assistant', 'LLM Assistant', 'Enable AI trading assistant chat', 'assistant', 'percentage', 'beta', false),
('enable_portfolio_rebalancing', 'Portfolio Rebalancing', 'Enable automatic portfolio rebalancing', 'portfolio', 'disabled', 'development', false),
('enable_mt4_gateway', 'MT4 Gateway', 'Enable MetaTrader 4 integration', 'trading', 'disabled', 'development', false),
('enable_2fa_enforcement', '2FA Enforcement', 'Require 2FA for all users', 'security', 'disabled', 'development', false),
('enable_paper_trading', 'Paper Trading', 'Enable paper trading mode', 'trading', 'enabled', 'production', true)
ON CONFLICT (code) DO NOTHING;