@vasoyaprince14/sql-analyzer
Version:
🚀 Enhanced SQL database analyzer with AI-powered insights, comprehensive security analysis, RLS policy auditing, and beautiful HTML reports
314 lines • 12.2 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaAnalyzer = void 0;
class SchemaAnalyzer {
constructor(client) {
this.client = client;
}
/**
* Analyze database schema
*/
async analyze() {
try {
const tables = await this.getTables();
const relationships = await this.getRelationships();
const recommendations = await this.generateRecommendations(tables, relationships);
// Calculate scores
const normalizationScore = this.calculateNormalizationScore(tables, relationships);
const performanceScore = this.calculatePerformanceScore(tables);
return {
tables,
relationships,
recommendations,
normalizationScore,
performanceScore
};
}
catch (error) {
throw new Error(`Schema analysis failed: ${error}`);
}
}
/**
* Get all tables with their structure
*/
async getTables() {
const result = await this.client.query(`
SELECT
t.table_name,
c.column_name,
c.data_type,
c.is_nullable,
c.column_default,
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key,
CASE WHEN fk.column_name IS NOT NULL THEN true ELSE false END as is_foreign_key,
fk.referenced_table_name,
fk.referenced_column_name
FROM information_schema.tables t
LEFT JOIN information_schema.columns c ON t.table_name = c.table_name
LEFT JOIN (
SELECT ku.table_name, ku.column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name
WHERE tc.constraint_type = 'PRIMARY KEY'
) pk ON t.table_name = pk.table_name AND c.column_name = pk.column_name
LEFT JOIN (
SELECT
ku.table_name,
ku.column_name,
ccu.table_name as referenced_table_name,
ccu.column_name as referenced_column_name
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage ku ON tc.constraint_name = ku.constraint_name
JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
) fk ON t.table_name = fk.table_name AND c.column_name = fk.column_name
WHERE t.table_schema = 'public'
ORDER BY t.table_name, c.ordinal_position
`);
const tableMap = new Map();
result.rows.forEach(row => {
const tableName = row.table_name;
if (!tableMap.has(tableName)) {
tableMap.set(tableName, {
name: tableName,
columns: [],
indexes: [],
rowCount: 0,
size: '0 MB'
});
}
const table = tableMap.get(tableName);
if (row.column_name) {
table.columns.push({
name: row.column_name,
type: row.data_type,
nullable: row.is_nullable === 'YES',
defaultValue: row.column_default,
isPrimaryKey: row.is_primary_key,
isForeignKey: row.is_foreign_key,
referencedTable: row.referenced_table_name,
referencedColumn: row.referenced_column_name
});
}
});
// Get indexes for each table
for (const table of tableMap.values()) {
table.indexes = await this.getTableIndexes(table.name);
}
// Get row counts and sizes
for (const table of tableMap.values()) {
const stats = await this.getTableStats(table.name);
table.rowCount = stats.rowCount;
table.size = stats.size;
}
return Array.from(tableMap.values());
}
/**
* Get indexes for a table
*/
async getTableIndexes(tableName) {
const result = await this.client.query(`
SELECT
i.relname as index_name,
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns,
am.amname as index_type,
ix.indisunique as is_unique,
CASE
WHEN pg_catalog.pg_get_expr(ix.indpred, ix.indrelid) IS NOT NULL THEN true
ELSE false
END as is_partial
FROM pg_class t
JOIN pg_index ix ON t.oid = ix.indrelid
JOIN pg_class i ON ix.indexrelid = i.oid
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
JOIN pg_am am ON i.relam = am.oid
WHERE t.relname = $1
GROUP BY i.relname, am.amname, ix.indisunique, ix.indpred, ix.indrelid
`, [tableName]);
return result.rows.map(row => ({
name: row.index_name,
columns: row.columns,
type: this.mapIndexType(row.index_type),
unique: row.is_unique,
partial: row.is_partial
}));
}
/**
* Get table statistics
*/
async getTableStats(tableName) {
try {
const result = await this.client.query(`
SELECT
n_tup_ins + n_tup_upd + n_tup_del as row_count,
pg_size_pretty(pg_total_relation_size($1::regclass)) as size
FROM pg_stat_user_tables
WHERE relname = $1
`, [tableName]);
if (result.rows.length > 0) {
return {
rowCount: parseInt(result.rows[0].row_count) || 0,
size: result.rows[0].size || '0 MB'
};
}
}
catch (error) {
console.warn(`Failed to get stats for table ${tableName}:`, error);
}
return { rowCount: 0, size: '0 MB' };
}
/**
* Get relationships between tables
*/
async getRelationships() {
const result = await this.client.query(`
SELECT
tc.table_name,
kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE constraint_type = 'FOREIGN KEY'
`);
return result.rows.map(row => ({
fromTable: row.table_name,
fromColumn: row.column_name,
toTable: row.foreign_table_name,
toColumn: row.foreign_column_name,
type: 'one_to_many' // Simplified - in reality you'd need more analysis
}));
}
/**
* Generate schema recommendations
*/
async generateRecommendations(tables, relationships) {
const recommendations = [];
// Check for missing indexes on foreign keys
for (const rel of relationships) {
const fromTable = tables.find(t => t.name === rel.fromTable);
if (fromTable) {
const hasIndex = fromTable.indexes.some(idx => idx.columns.includes(rel.fromColumn));
if (!hasIndex) {
recommendations.push({
type: 'index',
priority: 'high',
title: `Missing index on foreign key ${rel.fromTable}.${rel.fromColumn}`,
description: `Foreign key columns should be indexed for better JOIN performance`,
sql: `CREATE INDEX idx_${rel.fromTable}_${rel.fromColumn} ON ${rel.fromTable}(${rel.fromColumn});`,
impact: 'high'
});
}
}
}
// Check for tables without primary keys
for (const table of tables) {
const hasPrimaryKey = table.columns.some(col => col.isPrimaryKey);
if (!hasPrimaryKey) {
recommendations.push({
type: 'constraint',
priority: 'high',
title: `Missing primary key on table ${table.name}`,
description: `Every table should have a primary key for data integrity`,
sql: `ALTER TABLE ${table.name} ADD PRIMARY KEY (id);`,
impact: 'high'
});
}
}
// Check for large tables without indexes
for (const table of tables) {
if ((table.rowCount || 0) > 1000 && table.indexes.length === 0) {
recommendations.push({
type: 'index',
priority: 'medium',
title: `Large table ${table.name} has no indexes`,
description: `Consider adding indexes for frequently queried columns`,
sql: `-- Add indexes based on query patterns`,
impact: 'medium'
});
}
}
// Check for data type optimizations
for (const table of tables) {
for (const column of table.columns) {
if (column.type === 'character varying' && !column.nullable) {
recommendations.push({
type: 'data_type',
priority: 'low',
title: `Consider using CHAR instead of VARCHAR for ${table.name}.${column.name}`,
description: `Fixed-length strings can be more efficient for non-nullable columns`,
sql: `ALTER TABLE ${table.name} ALTER COLUMN ${column.name} TYPE CHAR(255);`,
impact: 'low'
});
}
}
}
return recommendations;
}
/**
* Calculate normalization score
*/
calculateNormalizationScore(tables, relationships) {
let score = 10;
// Deduct points for denormalization indicators
for (const table of tables) {
// Check for wide tables (many columns)
if (table.columns.length > 20) {
score -= 1;
}
// Check for tables with many nullable columns
const nullableColumns = table.columns.filter(col => col.nullable).length;
if (nullableColumns > table.columns.length * 0.5) {
score -= 1;
}
}
// Check for proper foreign key relationships
const tablesWithFKs = new Set(relationships.map(r => r.fromTable));
const tablesWithoutFKs = tables.filter(t => !tablesWithFKs.has(t.name)).length;
if (tablesWithoutFKs > tables.length * 0.3) {
score -= 1;
}
return Math.max(0, score);
}
/**
* Calculate performance score
*/
calculatePerformanceScore(tables) {
let score = 10;
for (const table of tables) {
// Deduct points for large tables without indexes
if ((table.rowCount || 0) > 10000 && table.indexes.length === 0) {
score -= 2;
}
// Deduct points for tables with too many indexes
if (table.indexes.length > 10) {
score -= 1;
}
// Deduct points for tables without primary keys
const hasPrimaryKey = table.columns.some(col => col.isPrimaryKey);
if (!hasPrimaryKey) {
score -= 2;
}
}
return Math.max(0, score);
}
/**
* Map PostgreSQL index type to our enum
*/
mapIndexType(pgType) {
switch (pgType) {
case 'btree': return 'btree';
case 'hash': return 'hash';
case 'gin': return 'gin';
case 'gist': return 'gist';
case 'spgist': return 'spgist';
case 'brin': return 'brin';
default: return 'btree';
}
}
}
exports.SchemaAnalyzer = SchemaAnalyzer;
//# sourceMappingURL=schema-analyzer.js.map