Skip to main content
Glama
SchemaAnalyzer.test.ts10.3 kB
import { describe, it, expect, beforeEach } from 'vitest'; import { Column } from '../entities/Column'; import { ForeignKey } from '../entities/ForeignKey'; import { Index } from '../entities/Index'; import { TableInfo } from '../entities/TableInfo'; import { SchemaAnalyzer } from './SchemaAnalyzer'; describe('SchemaAnalyzer', () => { let analyzer: SchemaAnalyzer; let usersTable: TableInfo; let postsTable: TableInfo; let logsTable: TableInfo; let userView: TableInfo; beforeEach(() => { analyzer = new SchemaAnalyzer(); // Users table - well-designed with PK usersTable = new TableInfo( 'users', 'table', [ new Column('id', 'INTEGER', true, false), new Column('name', 'TEXT', false, false), new Column('email', 'TEXT', false, true), ], [new Index('idx_users_email', 'users', ['email'], true)] ); // Posts table - has FK and index postsTable = new TableInfo( 'posts', 'table', [ new Column('id', 'INTEGER', true, false), new Column('user_id', 'INTEGER', false, false), new Column('title', 'TEXT', false, false), new Column('content', 'TEXT', false, true), ], [new Index('idx_posts_user_id', 'posts', ['user_id'])], [new ForeignKey('posts', 'user_id', 'users', 'id', 'CASCADE')] ); // Logs table - no PK (problematic) logsTable = new TableInfo('logs', 'table', [ new Column('timestamp', 'TEXT', false, true), new Column('message', 'TEXT', false, true), ]); // User view userView = new TableInfo('user_view', 'view', [ new Column('id', 'INTEGER'), new Column('name', 'TEXT'), ]); }); describe('analyzeSchema()', () => { it('should analyze overall schema statistics', () => { const result = analyzer.analyzeSchema([usersTable, postsTable, logsTable, userView]); expect(result.totalTables).toBe(4); expect(result.totalColumns).toBe(11); expect(result.tablesWithPrimaryKeys).toBe(2); // users, posts expect(result.tablesWithoutPrimaryKeys).toBe(2); // logs, user_view expect(result.tablesWithForeignKeys).toBe(1); // posts expect(result.tablesWithIndexes).toBe(2); // users, posts expect(result.totalIndexes).toBe(2); expect(result.totalForeignKeys).toBe(1); expect(result.totalViews).toBe(1); }); it('should handle empty table array', () => { const result = analyzer.analyzeSchema([]); expect(result.totalTables).toBe(0); expect(result.totalColumns).toBe(0); expect(result.tablesWithPrimaryKeys).toBe(0); }); it('should count all columns correctly', () => { const result = analyzer.analyzeSchema([usersTable, postsTable]); expect(result.totalColumns).toBe(7); // 3 + 4 }); }); describe('analyzeTable()', () => { it('should analyze table with primary key', () => { const analysis = analyzer.analyzeTable(usersTable); expect(analysis.tableName).toBe('users'); expect(analysis.columnCount).toBe(3); expect(analysis.hasPrimaryKey).toBe(true); expect(analysis.primaryKeyColumns).toEqual(['id']); expect(analysis.requiredColumns).toEqual(['id', 'name']); expect(analysis.foreignKeyCount).toBe(0); expect(analysis.indexCount).toBe(1); expect(analysis.isView).toBe(false); }); it('should analyze table without primary key', () => { const analysis = analyzer.analyzeTable(logsTable); expect(analysis.hasPrimaryKey).toBe(false); expect(analysis.primaryKeyColumns).toEqual([]); }); it('should analyze table with foreign keys', () => { const analysis = analyzer.analyzeTable(postsTable); expect(analysis.foreignKeyCount).toBe(1); expect(analysis.hasPrimaryKey).toBe(true); }); it('should identify views', () => { const analysis = analyzer.analyzeTable(userView); expect(analysis.isView).toBe(true); }); }); describe('analyzeColumnTypes()', () => { it('should count column type distribution', () => { const typeDistribution = analyzer.analyzeColumnTypes([usersTable, postsTable]); expect(typeDistribution.get('INTEGER')).toBe(3); // id columns expect(typeDistribution.get('TEXT')).toBe(4); // name, email, title, content }); it('should handle empty tables', () => { const typeDistribution = analyzer.analyzeColumnTypes([]); expect(typeDistribution.size).toBe(0); }); it('should aggregate types across multiple tables', () => { const table1 = new TableInfo('t1', 'table', [ new Column('id', 'INTEGER', true), new Column('name', 'TEXT'), ]); const table2 = new TableInfo('t2', 'table', [ new Column('id', 'INTEGER', true), new Column('value', 'REAL'), ]); const typeDistribution = analyzer.analyzeColumnTypes([table1, table2]); expect(typeDistribution.get('INTEGER')).toBe(2); expect(typeDistribution.get('TEXT')).toBe(1); expect(typeDistribution.get('REAL')).toBe(1); }); }); describe('identifyProblematicTables()', () => { it('should identify tables without primary keys', () => { const problematic = analyzer.identifyProblematicTables([ usersTable, postsTable, logsTable, ]); expect(problematic.length).toBe(1); expect(problematic[0].name).toBe('logs'); }); it('should identify tables with unindexed foreign keys', () => { const badTable = new TableInfo( 'comments', 'table', [ new Column('id', 'INTEGER', true), new Column('post_id', 'INTEGER'), new Column('text', 'TEXT'), ], [], // No indexes! [new ForeignKey('comments', 'post_id', 'posts', 'id')] ); const problematic = analyzer.identifyProblematicTables([usersTable, badTable]); expect(problematic.length).toBe(1); expect(problematic[0].name).toBe('comments'); }); it('should return empty array for well-designed schema', () => { const problematic = analyzer.identifyProblematicTables([usersTable, postsTable]); expect(problematic.length).toBe(0); }); it('should not flag views as problematic', () => { const problematic = analyzer.identifyProblematicTables([userView]); // Views without PKs are acceptable expect(problematic.length).toBe(0); }); }); describe('analyzeNullability()', () => { it('should analyze nullable columns', () => { const analysis = analyzer.analyzeNullability(usersTable); expect(analysis.nullableColumns.length).toBe(1); // email expect(analysis.nullableColumns[0].name).toBe('email'); expect(analysis.requiredColumns.length).toBe(2); // id, name expect(analysis.nullablePercentage).toBeCloseTo(33.33, 1); }); it('should handle all required columns', () => { const strictTable = new TableInfo('strict', 'table', [ new Column('id', 'INTEGER', true, false), new Column('name', 'TEXT', false, false), ]); const analysis = analyzer.analyzeNullability(strictTable); expect(analysis.nullableColumns.length).toBe(0); expect(analysis.requiredColumns.length).toBe(2); expect(analysis.nullablePercentage).toBe(0); }); it('should handle all nullable columns', () => { const analysis = analyzer.analyzeNullability(logsTable); expect(analysis.nullableColumns.length).toBe(2); expect(analysis.requiredColumns.length).toBe(0); expect(analysis.nullablePercentage).toBe(100); }); }); describe('getTablesByComplexity()', () => { it('should sort tables by complexity', () => { const sorted = analyzer.getTablesByComplexity([usersTable, postsTable, logsTable]); // posts: 4 columns + 1 index + 1 FK = 6 // users: 3 columns + 1 index + 0 FK = 4 // logs: 2 columns + 0 index + 0 FK = 2 expect(sorted[0].table.name).toBe('posts'); expect(sorted[0].complexity).toBe(6); expect(sorted[1].table.name).toBe('users'); expect(sorted[1].complexity).toBe(4); expect(sorted[2].table.name).toBe('logs'); expect(sorted[2].complexity).toBe(2); }); it('should handle single table', () => { const sorted = analyzer.getTablesByComplexity([usersTable]); expect(sorted.length).toBe(1); expect(sorted[0].table.name).toBe('users'); }); it('should handle empty array', () => { const sorted = analyzer.getTablesByComplexity([]); expect(sorted.length).toBe(0); }); }); describe('identifyOrphanedTables()', () => { it('should identify tables with no relationships', () => { const orphaned = analyzer.identifyOrphanedTables([usersTable, postsTable, logsTable]); expect(orphaned.length).toBe(1); expect(orphaned[0].name).toBe('logs'); }); it('should not flag tables with incoming foreign keys', () => { // users is referenced by posts, so not orphaned const orphaned = analyzer.identifyOrphanedTables([usersTable, postsTable]); expect(orphaned.length).toBe(0); }); it('should not flag tables with outgoing foreign keys', () => { // posts references users, so not orphaned const orphaned = analyzer.identifyOrphanedTables([postsTable]); expect(orphaned.length).toBe(0); }); it('should not flag views as orphaned', () => { const orphaned = analyzer.identifyOrphanedTables([userView]); expect(orphaned.length).toBe(0); }); it('should handle complex relationship networks', () => { const commentsTable = new TableInfo( 'comments', 'table', [new Column('id', 'INTEGER', true), new Column('post_id', 'INTEGER')], [], [new ForeignKey('comments', 'post_id', 'posts', 'id')] ); const tagsTable = new TableInfo('tags', 'table', [ new Column('id', 'INTEGER', true), new Column('name', 'TEXT'), ]); const orphaned = analyzer.identifyOrphanedTables([ usersTable, postsTable, commentsTable, tagsTable, ]); // Only tags is orphaned (no FK in/out) expect(orphaned.length).toBe(1); expect(orphaned[0].name).toBe('tags'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/semanticintent/semantic-d1-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server