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>
260 lines
9.7 KiB
PL/PgSQL
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;
|