200 lines
6.8 KiB
JavaScript
200 lines
6.8 KiB
JavaScript
/**
|
|
* Database Schema Validation Script
|
|
* Validates that all required tables, indexes, and constraints exist
|
|
*/
|
|
|
|
import { rawQuery } from '../src/db.js';
|
|
|
|
const REQUIRED_TABLES = [
|
|
'options_flow',
|
|
'daily_analysis',
|
|
'OptionsFlow_monthly',
|
|
'AlertStream_monthly',
|
|
'alertstream_monthly',
|
|
'prices_intraday_1m',
|
|
'prices_daily',
|
|
'price_daily',
|
|
'price_1m',
|
|
'minute_bars_et',
|
|
'daily_bars',
|
|
'bucket_members'
|
|
];
|
|
|
|
async function validateSchema() {
|
|
console.log('🔍 Starting database schema validation...\n');
|
|
|
|
const issues = [];
|
|
const warnings = [];
|
|
|
|
try {
|
|
// 1. Check all required tables exist
|
|
console.log('📋 Checking required tables...');
|
|
const tablesQuery = `
|
|
SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
AND table_name = ANY($1::text[])
|
|
ORDER BY table_name;
|
|
`;
|
|
const existingTables = await rawQuery(tablesQuery, [REQUIRED_TABLES]);
|
|
const existingTableNames = existingTables.map(t => t.table_name);
|
|
|
|
for (const table of REQUIRED_TABLES) {
|
|
if (existingTableNames.includes(table)) {
|
|
console.log(` ✅ ${table}`);
|
|
} else {
|
|
console.log(` ❌ ${table} - MISSING`);
|
|
issues.push(`Missing table: ${table}`);
|
|
}
|
|
}
|
|
|
|
// 2. Check indexes on critical tables
|
|
console.log('\n📊 Checking indexes on critical tables...');
|
|
const indexesQuery = `
|
|
SELECT tablename, indexname, indexdef
|
|
FROM pg_indexes
|
|
WHERE schemaname = 'public'
|
|
AND tablename IN ('prices_intraday_1m', 'prices_daily', 'OptionsFlow_monthly')
|
|
ORDER BY tablename, indexname;
|
|
`;
|
|
const indexes = await rawQuery(indexesQuery);
|
|
|
|
const criticalIndexes = {
|
|
'prices_intraday_1m': ['idx_prices_intraday_1m_symbol_ts', 'idx_prices_intraday_1m_ts'],
|
|
'prices_daily': ['idx_prices_daily_symbol_date', 'idx_prices_daily_date'],
|
|
'OptionsFlow_monthly': ['idx_optionsflow_monthly_symbol', 'idx_optionsflow_monthly_timestamp']
|
|
};
|
|
|
|
for (const [table, requiredIndexes] of Object.entries(criticalIndexes)) {
|
|
const tableIndexes = indexes.filter(idx => idx.tablename === table);
|
|
const existingIndexNames = tableIndexes.map(idx => idx.indexname);
|
|
|
|
for (const indexName of requiredIndexes) {
|
|
if (existingIndexNames.some(name => name.includes(indexName.split('_').slice(-2).join('_')))) {
|
|
console.log(` ✅ ${table}.${indexName}`);
|
|
} else {
|
|
console.log(` ⚠️ ${table}.${indexName} - Missing or different name`);
|
|
warnings.push(`Index ${indexName} on ${table} may be missing`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. Verify PRIMARY KEY constraints
|
|
console.log('\n🔑 Checking PRIMARY KEY constraints...');
|
|
const pkQuery = `
|
|
SELECT tc.table_name, kc.column_name, tc.constraint_name
|
|
FROM information_schema.table_constraints tc
|
|
JOIN information_schema.key_column_usage kc
|
|
ON tc.constraint_name = kc.constraint_name
|
|
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
AND tc.table_schema = 'public'
|
|
AND tc.table_name IN ('prices_intraday_1m', 'prices_daily')
|
|
ORDER BY tc.table_name;
|
|
`;
|
|
const primaryKeys = await rawQuery(pkQuery);
|
|
|
|
const expectedPKs = {
|
|
'prices_intraday_1m': ['symbol', 'ts'],
|
|
'prices_daily': ['symbol', 'Date']
|
|
};
|
|
|
|
for (const [table, expectedCols] of Object.entries(expectedPKs)) {
|
|
const tablePKs = primaryKeys.filter(pk => pk.table_name === table);
|
|
const pkColumns = tablePKs.map(pk => pk.column_name);
|
|
|
|
const hasAllColumns = expectedCols.every(col =>
|
|
pkColumns.some(pkCol => pkCol.toLowerCase() === col.toLowerCase())
|
|
);
|
|
|
|
if (hasAllColumns && tablePKs.length > 0) {
|
|
console.log(` ✅ ${table} - PRIMARY KEY on (${expectedCols.join(', ')})`);
|
|
} else {
|
|
console.log(` ❌ ${table} - Missing PRIMARY KEY on (${expectedCols.join(', ')})`);
|
|
issues.push(`Missing PRIMARY KEY on ${table}`);
|
|
}
|
|
}
|
|
|
|
// 4. Check for data type inconsistencies
|
|
console.log('\n🔤 Checking data type consistency...');
|
|
const dataTypeQuery = `
|
|
SELECT
|
|
table_name,
|
|
column_name,
|
|
data_type,
|
|
character_maximum_length
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
AND table_name IN ('prices_intraday_1m', 'prices_daily')
|
|
AND column_name IN ('symbol', 'ts', 'Date', 'timestamp')
|
|
ORDER BY table_name, column_name;
|
|
`;
|
|
const columns = await rawQuery(dataTypeQuery);
|
|
|
|
// Check for redundant timestamp columns
|
|
const intradayColumns = columns.filter(c => c.table_name === 'prices_intraday_1m');
|
|
const hasTs = intradayColumns.some(c => c.column_name === 'ts');
|
|
const hasTimestamp = intradayColumns.some(c => c.column_name === 'timestamp');
|
|
|
|
if (hasTs && hasTimestamp) {
|
|
console.log(` ⚠️ prices_intraday_1m has both 'ts' and 'timestamp' columns (may be redundant)`);
|
|
warnings.push('prices_intraday_1m has redundant timestamp columns');
|
|
} else if (hasTs) {
|
|
console.log(` ✅ prices_intraday_1m uses 'ts' column`);
|
|
} else if (hasTimestamp) {
|
|
console.log(` ⚠️ prices_intraday_1m only has 'timestamp' column (expected 'ts')`);
|
|
warnings.push('prices_intraday_1m missing expected ts column');
|
|
}
|
|
|
|
// Check Date column capitalization
|
|
const dailyColumns = columns.filter(c => c.table_name === 'prices_daily');
|
|
const hasDate = dailyColumns.some(c => c.column_name === 'Date');
|
|
if (hasDate) {
|
|
console.log(` ✅ prices_daily uses 'Date' column (capital D)`);
|
|
} else {
|
|
console.log(` ⚠️ prices_daily may not have 'Date' column with capital D`);
|
|
warnings.push('prices_daily Date column capitalization check');
|
|
}
|
|
|
|
// 5. Summary
|
|
console.log('\n' + '='.repeat(60));
|
|
console.log('📊 VALIDATION SUMMARY');
|
|
console.log('='.repeat(60));
|
|
|
|
if (issues.length === 0 && warnings.length === 0) {
|
|
console.log('✅ Schema validation passed! All checks successful.');
|
|
return true;
|
|
}
|
|
|
|
if (warnings.length > 0) {
|
|
console.log(`\n⚠️ Warnings (${warnings.length}):`);
|
|
warnings.forEach(w => console.log(` - ${w}`));
|
|
}
|
|
|
|
if (issues.length > 0) {
|
|
console.log(`\n❌ Issues found (${issues.length}):`);
|
|
issues.forEach(i => console.log(` - ${i}`));
|
|
console.log('\n💡 Run the schema.sql and missing_tables.sql scripts to fix issues.');
|
|
return false;
|
|
}
|
|
|
|
console.log('\n✅ Schema validation passed with warnings.');
|
|
return true;
|
|
|
|
} catch (error) {
|
|
console.error('\n❌ Schema validation failed with error:');
|
|
console.error(error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Run validation
|
|
validateSchema()
|
|
.then(success => {
|
|
process.exit(success ? 0 : 1);
|
|
})
|
|
.catch(error => {
|
|
console.error('Fatal error:', error);
|
|
process.exit(1);
|
|
});
|
|
|