- Deprecated/broken migration files archived - New auth_schema migration (015) - Trading service events migration (016) - Migration renumbering utility - Migration test suite
421 lines
15 KiB
PL/PgSQL
421 lines
15 KiB
PL/PgSQL
-- ================================================================================================
|
|
-- Schema Validation Test Suite
|
|
-- PostgreSQL 16.10 + TimescaleDB 2.22.1
|
|
-- Validates all required tables, constraints, indexes, and types exist
|
|
-- ================================================================================================
|
|
|
|
BEGIN;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 1: Core Tables Existence
|
|
-- Verify all critical tables are created
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_required_tables TEXT[] := ARRAY[
|
|
'trading_events',
|
|
'risk_events',
|
|
'audit_events',
|
|
'orders',
|
|
'executions',
|
|
'positions',
|
|
'accounts',
|
|
'users',
|
|
'roles',
|
|
'permissions',
|
|
'user_roles',
|
|
'role_permissions',
|
|
'api_keys',
|
|
'jwt_revocations',
|
|
'rate_limits',
|
|
'compliance_reports',
|
|
'market_data_raw',
|
|
'market_data_aggregated',
|
|
'config_parameters',
|
|
'config_history',
|
|
'symbols',
|
|
'symbol_metadata'
|
|
];
|
|
v_table TEXT;
|
|
v_missing_tables TEXT[] := ARRAY[]::TEXT[];
|
|
v_found_count INTEGER := 0;
|
|
BEGIN
|
|
FOREACH v_table IN ARRAY v_required_tables
|
|
LOOP
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_name = v_table
|
|
) THEN
|
|
v_found_count := v_found_count + 1;
|
|
ELSE
|
|
v_missing_tables := array_append(v_missing_tables, v_table);
|
|
END IF;
|
|
END LOOP;
|
|
|
|
IF array_length(v_missing_tables, 1) IS NULL THEN
|
|
RAISE NOTICE 'TEST 1 PASS: All % required tables exist', array_length(v_required_tables, 1);
|
|
ELSE
|
|
RAISE WARNING 'TEST 1 WARNING: Missing tables (% of %): %',
|
|
array_length(v_missing_tables, 1),
|
|
array_length(v_required_tables, 1),
|
|
array_to_string(v_missing_tables, ', ');
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 2: Required Enums Validation
|
|
-- Verify all enum types are defined
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_required_enums TEXT[] := ARRAY[
|
|
'trading_event_type',
|
|
'order_side',
|
|
'order_type',
|
|
'time_in_force',
|
|
'order_status',
|
|
'execution_type',
|
|
'risk_event_type',
|
|
'risk_severity',
|
|
'risk_metric_type',
|
|
'audit_event_type',
|
|
'audit_severity',
|
|
'system_component',
|
|
'account_type',
|
|
'position_side',
|
|
'execution_status'
|
|
];
|
|
v_enum TEXT;
|
|
v_missing_enums TEXT[] := ARRAY[]::TEXT[];
|
|
BEGIN
|
|
FOREACH v_enum IN ARRAY v_required_enums
|
|
LOOP
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_type
|
|
WHERE typname = v_enum
|
|
AND typtype = 'e'
|
|
) THEN
|
|
v_missing_enums := array_append(v_missing_enums, v_enum);
|
|
END IF;
|
|
END LOOP;
|
|
|
|
IF array_length(v_missing_enums, 1) IS NULL THEN
|
|
RAISE NOTICE 'TEST 2 PASS: All % required enum types exist', array_length(v_required_enums, 1);
|
|
ELSE
|
|
RAISE WARNING 'TEST 2 WARNING: Missing enum types: %', array_to_string(v_missing_enums, ', ');
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 3: Primary Key Constraints
|
|
-- Verify all tables have primary keys
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_table_record RECORD;
|
|
v_tables_without_pk TEXT[] := ARRAY[]::TEXT[];
|
|
v_checked_count INTEGER := 0;
|
|
BEGIN
|
|
FOR v_table_record IN
|
|
SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_type = 'BASE TABLE'
|
|
AND table_name NOT LIKE '%_old'
|
|
AND table_name NOT LIKE '%_backup'
|
|
LOOP
|
|
v_checked_count := v_checked_count + 1;
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.table_constraints
|
|
WHERE table_schema = 'public'
|
|
AND table_name = v_table_record.table_name
|
|
AND constraint_type = 'PRIMARY KEY'
|
|
) THEN
|
|
v_tables_without_pk := array_append(v_tables_without_pk, v_table_record.table_name);
|
|
END IF;
|
|
END LOOP;
|
|
|
|
IF array_length(v_tables_without_pk, 1) IS NULL THEN
|
|
RAISE NOTICE 'TEST 3 PASS: All % tables have primary key constraints', v_checked_count;
|
|
ELSE
|
|
RAISE WARNING 'TEST 3 WARNING: Tables without primary keys: %', array_to_string(v_tables_without_pk, ', ');
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 4: Foreign Key Constraints
|
|
-- Verify critical foreign key relationships
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_required_fks TEXT[][] := ARRAY[
|
|
ARRAY['orders', 'account_id', 'accounts'],
|
|
ARRAY['executions', 'order_id', 'orders'],
|
|
ARRAY['positions', 'account_id', 'accounts'],
|
|
ARRAY['user_roles', 'user_id', 'users'],
|
|
ARRAY['user_roles', 'role_id', 'roles'],
|
|
ARRAY['role_permissions', 'role_id', 'roles'],
|
|
ARRAY['role_permissions', 'permission_id', 'permissions'],
|
|
ARRAY['api_keys', 'user_id', 'users']
|
|
];
|
|
v_fk TEXT[];
|
|
v_missing_fks TEXT[] := ARRAY[]::TEXT[];
|
|
v_fk_desc TEXT;
|
|
BEGIN
|
|
FOREACH v_fk SLICE 1 IN ARRAY v_required_fks
|
|
LOOP
|
|
v_fk_desc := v_fk[1] || '.' || v_fk[2] || ' -> ' || v_fk[3];
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.table_constraints tc
|
|
JOIN information_schema.key_column_usage kcu
|
|
ON tc.constraint_name = kcu.constraint_name
|
|
AND tc.table_schema = kcu.table_schema
|
|
JOIN information_schema.constraint_column_usage ccu
|
|
ON ccu.constraint_name = tc.constraint_name
|
|
AND ccu.table_schema = tc.table_schema
|
|
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
AND tc.table_name = v_fk[1]
|
|
AND kcu.column_name = v_fk[2]
|
|
AND ccu.table_name = v_fk[3]
|
|
) THEN
|
|
-- Check if table and column exist before marking as missing FK
|
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = v_fk[1])
|
|
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = v_fk[1] AND column_name = v_fk[2])
|
|
AND EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = v_fk[3])
|
|
THEN
|
|
v_missing_fks := array_append(v_missing_fks, v_fk_desc);
|
|
END IF;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
IF array_length(v_missing_fks, 1) IS NULL THEN
|
|
RAISE NOTICE 'TEST 4 PASS: All critical foreign key relationships verified';
|
|
ELSE
|
|
RAISE WARNING 'TEST 4 WARNING: Missing foreign keys: %', array_to_string(v_missing_fks, ', ');
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 5: Index Coverage - Performance Indexes
|
|
-- Verify critical indexes exist for query performance
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_required_indexes TEXT[][] := ARRAY[
|
|
ARRAY['trading_events', 'symbol'],
|
|
ARRAY['trading_events', 'event_type'],
|
|
ARRAY['trading_events', 'correlation_id'],
|
|
ARRAY['risk_events', 'symbol'],
|
|
ARRAY['risk_events', 'account_id'],
|
|
ARRAY['risk_events', 'severity'],
|
|
ARRAY['audit_events', 'user_id'],
|
|
ARRAY['audit_events', 'component'],
|
|
ARRAY['orders', 'symbol'],
|
|
ARRAY['orders', 'account_id'],
|
|
ARRAY['orders', 'status'],
|
|
ARRAY['executions', 'order_id'],
|
|
ARRAY['positions', 'account_id'],
|
|
ARRAY['positions', 'symbol']
|
|
];
|
|
v_idx TEXT[];
|
|
v_missing_indexes TEXT[] := ARRAY[]::TEXT[];
|
|
v_idx_desc TEXT;
|
|
BEGIN
|
|
FOREACH v_idx SLICE 1 IN ARRAY v_required_indexes
|
|
LOOP
|
|
v_idx_desc := v_idx[1] || '.' || v_idx[2];
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_indexes
|
|
WHERE tablename = v_idx[1]
|
|
AND indexdef LIKE '%' || v_idx[2] || '%'
|
|
) THEN
|
|
-- Check if table exists before marking as missing index
|
|
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = v_idx[1]) THEN
|
|
v_missing_indexes := array_append(v_missing_indexes, v_idx_desc);
|
|
END IF;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
IF array_length(v_missing_indexes, 1) IS NULL THEN
|
|
RAISE NOTICE 'TEST 5 PASS: All critical performance indexes exist';
|
|
ELSE
|
|
RAISE WARNING 'TEST 5 WARNING: Missing indexes: %', array_to_string(v_missing_indexes, ', ');
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 6: NOT NULL Constraints - Critical Fields
|
|
-- Verify critical fields have NOT NULL constraints
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_required_not_nulls TEXT[][] := ARRAY[
|
|
ARRAY['trading_events', 'event_timestamp'],
|
|
ARRAY['trading_events', 'event_type'],
|
|
ARRAY['trading_events', 'symbol'],
|
|
ARRAY['risk_events', 'event_timestamp'],
|
|
ARRAY['risk_events', 'severity'],
|
|
ARRAY['audit_events', 'event_timestamp'],
|
|
ARRAY['audit_events', 'event_type'],
|
|
ARRAY['orders', 'symbol'],
|
|
ARRAY['orders', 'side'],
|
|
ARRAY['orders', 'order_type'],
|
|
ARRAY['accounts', 'id'],
|
|
ARRAY['users', 'username'],
|
|
ARRAY['users', 'email']
|
|
];
|
|
v_nn TEXT[];
|
|
v_missing_not_nulls TEXT[] := ARRAY[]::TEXT[];
|
|
v_nn_desc TEXT;
|
|
BEGIN
|
|
FOREACH v_nn SLICE 1 IN ARRAY v_required_not_nulls
|
|
LOOP
|
|
v_nn_desc := v_nn[1] || '.' || v_nn[2];
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM information_schema.columns
|
|
WHERE table_name = v_nn[1]
|
|
AND column_name = v_nn[2]
|
|
AND is_nullable = 'NO'
|
|
) THEN
|
|
-- Check if column exists
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_name = v_nn[1] AND column_name = v_nn[2]
|
|
) THEN
|
|
v_missing_not_nulls := array_append(v_missing_not_nulls, v_nn_desc);
|
|
END IF;
|
|
END IF;
|
|
END LOOP;
|
|
|
|
IF array_length(v_missing_not_nulls, 1) IS NULL THEN
|
|
RAISE NOTICE 'TEST 6 PASS: All critical NOT NULL constraints exist';
|
|
ELSE
|
|
RAISE WARNING 'TEST 6 WARNING: Missing NOT NULL constraints: %', array_to_string(v_missing_not_nulls, ', ');
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 7: CHECK Constraints - Data Integrity
|
|
-- Verify CHECK constraints for data validation
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_constraint_count INTEGER;
|
|
BEGIN
|
|
-- Count CHECK constraints in critical tables
|
|
SELECT COUNT(*)
|
|
INTO v_constraint_count
|
|
FROM information_schema.table_constraints
|
|
WHERE constraint_type = 'CHECK'
|
|
AND table_schema = 'public'
|
|
AND table_name IN ('trading_events', 'risk_events', 'orders', 'executions', 'positions');
|
|
|
|
IF v_constraint_count > 0 THEN
|
|
RAISE NOTICE 'TEST 7 PASS: Found % CHECK constraints for data integrity', v_constraint_count;
|
|
ELSE
|
|
RAISE WARNING 'TEST 7 WARNING: No CHECK constraints found in critical tables';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 8: Unique Constraints - Duplicate Prevention
|
|
-- Verify unique constraints exist
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_unique_count INTEGER;
|
|
BEGIN
|
|
SELECT COUNT(*)
|
|
INTO v_unique_count
|
|
FROM information_schema.table_constraints
|
|
WHERE constraint_type = 'UNIQUE'
|
|
AND table_schema = 'public';
|
|
|
|
IF v_unique_count > 0 THEN
|
|
RAISE NOTICE 'TEST 8 PASS: Found % UNIQUE constraints', v_unique_count;
|
|
ELSE
|
|
RAISE WARNING 'TEST 8 WARNING: No UNIQUE constraints found';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 9: Default Values - Auto-population
|
|
-- Verify critical defaults are set
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_defaults_with_defaults TEXT[] := ARRAY[]::TEXT[];
|
|
v_col_record RECORD;
|
|
BEGIN
|
|
FOR v_col_record IN
|
|
SELECT table_name, column_name, column_default
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
AND (
|
|
(column_name LIKE '%_id' AND column_default LIKE '%uuid_generate%')
|
|
OR (column_name LIKE '%_timestamp' AND column_default IS NOT NULL)
|
|
OR (column_name = 'created_at' AND column_default IS NOT NULL)
|
|
OR (column_name = 'updated_at' AND column_default IS NOT NULL)
|
|
)
|
|
LOOP
|
|
v_defaults_with_defaults := array_append(v_defaults_with_defaults,
|
|
v_col_record.table_name || '.' || v_col_record.column_name);
|
|
END LOOP;
|
|
|
|
IF array_length(v_defaults_with_defaults, 1) > 0 THEN
|
|
RAISE NOTICE 'TEST 9 PASS: Found % columns with default values', array_length(v_defaults_with_defaults, 1);
|
|
ELSE
|
|
RAISE WARNING 'TEST 9 WARNING: No default values found on expected columns';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- TEST 10: Database Extensions
|
|
-- Verify required PostgreSQL extensions are installed
|
|
-- ================================================================================================
|
|
DO $$
|
|
DECLARE
|
|
v_required_extensions TEXT[] := ARRAY[
|
|
'uuid-ossp',
|
|
'pgcrypto',
|
|
'timescaledb'
|
|
];
|
|
v_ext TEXT;
|
|
v_missing_extensions TEXT[] := ARRAY[]::TEXT[];
|
|
v_installed_extensions TEXT[] := ARRAY[]::TEXT[];
|
|
BEGIN
|
|
FOREACH v_ext IN ARRAY v_required_extensions
|
|
LOOP
|
|
IF EXISTS (
|
|
SELECT 1 FROM pg_extension WHERE extname = v_ext
|
|
) THEN
|
|
v_installed_extensions := array_append(v_installed_extensions, v_ext);
|
|
ELSE
|
|
v_missing_extensions := array_append(v_missing_extensions, v_ext);
|
|
END IF;
|
|
END LOOP;
|
|
|
|
IF array_length(v_missing_extensions, 1) IS NULL THEN
|
|
RAISE NOTICE 'TEST 10 PASS: All required extensions installed: %', array_to_string(v_installed_extensions, ', ');
|
|
ELSE
|
|
RAISE WARNING 'TEST 10 WARNING: Missing extensions: %', array_to_string(v_missing_extensions, ', ');
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ================================================================================================
|
|
-- Cleanup: Rollback transaction
|
|
-- ================================================================================================
|
|
ROLLBACK;
|
|
|
|
-- Test Summary
|
|
SELECT 'Schema Validation Test Suite Completed - Review NOTICE/WARNING messages above' AS test_summary;
|