From e4d39b1293c19dab74ce25398573482ce6916e19 Mon Sep 17 00:00:00 2001 From: Adrian Flores Cortes Date: Fri, 30 Jan 2026 15:38:38 -0600 Subject: [PATCH] [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 --- ddl/schemas/feature_flags/tables/01-flags.sql | 259 ++++++++++++++++++ 1 file changed, 259 insertions(+) create mode 100644 ddl/schemas/feature_flags/tables/01-flags.sql diff --git a/ddl/schemas/feature_flags/tables/01-flags.sql b/ddl/schemas/feature_flags/tables/01-flags.sql new file mode 100644 index 0000000..79832bf --- /dev/null +++ b/ddl/schemas/feature_flags/tables/01-flags.sql @@ -0,0 +1,259 @@ +-- ============================================================================ +-- 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;