Files
foxhunt/migrations/tests/test_schema_validation.sql
jgrusewski 0d9f890aa6 🗄️ Wave 112: Migration cleanup and new schemas
- Deprecated/broken migration files archived
- New auth_schema migration (015)
- Trading service events migration (016)
- Migration renumbering utility
- Migration test suite
2025-10-05 22:23:37 +02:00

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;