Skip to main content
Glama
RelationshipAnalyzer.test.ts10.4 kB
import { describe, it, expect, beforeEach } from 'vitest'; import { Column } from '../entities/Column'; import { ForeignKey } from '../entities/ForeignKey'; import { Relationship } from '../entities/Relationship'; import { TableInfo } from '../entities/TableInfo'; import { RelationshipAnalyzer } from './RelationshipAnalyzer'; describe('RelationshipAnalyzer', () => { let analyzer: RelationshipAnalyzer; let usersTable: TableInfo; let postsTable: TableInfo; let commentsTable: TableInfo; let categoriesTable: TableInfo; beforeEach(() => { analyzer = new RelationshipAnalyzer(); // Users table (no FK) usersTable = new TableInfo('users', 'table', [ new Column('id', 'INTEGER', true), new Column('name', 'TEXT'), ]); // Posts table (FK to users) postsTable = new TableInfo( 'posts', 'table', [ new Column('id', 'INTEGER', true), new Column('user_id', 'INTEGER'), new Column('title', 'TEXT'), ], [], [new ForeignKey('posts', 'user_id', 'users', 'id', 'CASCADE')] ); // Comments table (FK to posts and users) commentsTable = new TableInfo( 'comments', 'table', [ new Column('id', 'INTEGER', true), new Column('post_id', 'INTEGER'), new Column('user_id', 'INTEGER'), new Column('text', 'TEXT'), ], [], [ new ForeignKey('comments', 'post_id', 'posts', 'id', 'CASCADE'), new ForeignKey('comments', 'user_id', 'users', 'id', 'SET NULL'), ] ); // Categories table (self-referential) categoriesTable = new TableInfo( 'categories', 'table', [ new Column('id', 'INTEGER', true), new Column('name', 'TEXT'), new Column('parent_id', 'INTEGER'), ], [], [new ForeignKey('categories', 'parent_id', 'categories', 'id', 'CASCADE')] ); }); describe('extractRelationships()', () => { it('should extract relationships from tables', () => { const relationships = analyzer.extractRelationships([usersTable, postsTable, commentsTable]); expect(relationships.length).toBe(3); expect(relationships[0].fromTable).toBe('posts'); expect(relationships[0].toTable).toBe('users'); expect(relationships[1].fromTable).toBe('comments'); expect(relationships[1].toTable).toBe('posts'); expect(relationships[2].fromTable).toBe('comments'); expect(relationships[2].toTable).toBe('users'); }); it('should return empty array for tables without FK', () => { const relationships = analyzer.extractRelationships([usersTable]); expect(relationships.length).toBe(0); }); it('should preserve FK semantics in relationships', () => { const relationships = analyzer.extractRelationships([postsTable]); expect(relationships[0].onDelete).toBe('CASCADE'); expect(relationships[0].fromColumn).toBe('user_id'); expect(relationships[0].toColumn).toBe('id'); }); }); describe('getRelationshipsForTable()', () => { it('should get outgoing relationships', () => { const allRels = analyzer.extractRelationships([usersTable, postsTable, commentsTable]); const result = analyzer.getRelationshipsForTable('comments', allRels); expect(result.outgoing.length).toBe(2); expect(result.outgoing[0].toTable).toBe('posts'); expect(result.outgoing[1].toTable).toBe('users'); }); it('should get incoming relationships', () => { const allRels = analyzer.extractRelationships([usersTable, postsTable, commentsTable]); const result = analyzer.getRelationshipsForTable('users', allRels); expect(result.outgoing.length).toBe(0); // users doesn't reference anyone expect(result.incoming.length).toBe(2); // posts and comments reference users }); it('should handle table with no relationships', () => { const allRels = analyzer.extractRelationships([usersTable, postsTable]); const result = analyzer.getRelationshipsForTable('nonexistent', allRels); expect(result.outgoing.length).toBe(0); expect(result.incoming.length).toBe(0); }); }); describe('buildDependencyGraph()', () => { it('should build graph from relationships', () => { const rels = analyzer.extractRelationships([usersTable, postsTable, commentsTable]); const graph = analyzer.buildDependencyGraph(rels); expect(graph.nodes).toContain('users'); expect(graph.nodes).toContain('posts'); expect(graph.nodes).toContain('comments'); expect(graph.edges.length).toBe(3); }); it('should create edges with column information', () => { const rels = analyzer.extractRelationships([postsTable]); const graph = analyzer.buildDependencyGraph(rels); expect(graph.edges[0].from).toBe('posts'); expect(graph.edges[0].to).toBe('users'); expect(graph.edges[0].column).toBe('user_id'); }); it('should handle empty relationships', () => { const graph = analyzer.buildDependencyGraph([]); expect(graph.nodes.length).toBe(0); expect(graph.edges.length).toBe(0); }); }); describe('detectCircularDependencies()', () => { it('should detect self-referential cycle', () => { const rels = analyzer.extractRelationships([categoriesTable]); const cycles = analyzer.detectCircularDependencies(rels); expect(cycles.length).toBeGreaterThan(0); expect(cycles[0]).toContain('categories'); }); it('should detect multi-table cycles', () => { // Create circular dependency: A -> B -> C -> A const tableA = new TableInfo( 'a', 'table', [new Column('id', 'INTEGER', true), new Column('b_id', 'INTEGER')], [], [new ForeignKey('a', 'b_id', 'b', 'id')] ); const tableB = new TableInfo( 'b', 'table', [new Column('id', 'INTEGER', true), new Column('c_id', 'INTEGER')], [], [new ForeignKey('b', 'c_id', 'c', 'id')] ); const tableC = new TableInfo( 'c', 'table', [new Column('id', 'INTEGER', true), new Column('a_id', 'INTEGER')], [], [new ForeignKey('c', 'a_id', 'a', 'id')] ); const rels = analyzer.extractRelationships([tableA, tableB, tableC]); const cycles = analyzer.detectCircularDependencies(rels); expect(cycles.length).toBeGreaterThan(0); }); it('should return empty for acyclic graph', () => { const rels = analyzer.extractRelationships([usersTable, postsTable, commentsTable]); const cycles = analyzer.detectCircularDependencies(rels); expect(cycles.length).toBe(0); }); }); describe('getCascadeChains()', () => { it('should find cascade chains', () => { const rels = analyzer.extractRelationships([usersTable, postsTable, commentsTable]); const chains = analyzer.getCascadeChains('users', rels); expect(chains.length).toBeGreaterThan(0); // Deleting user cascades to posts, then to comments }); it('should return empty for table with no cascades', () => { const rels = analyzer.extractRelationships([commentsTable]); const chains = analyzer.getCascadeChains('comments', rels); expect(chains.length).toBeGreaterThanOrEqual(1); // includes itself }); }); describe('getSelfReferentialRelationships()', () => { it('should identify self-referential relationships', () => { const rels = analyzer.extractRelationships([categoriesTable]); const selfRefs = analyzer.getSelfReferentialRelationships(rels); expect(selfRefs.length).toBe(1); expect(selfRefs[0].isSelfReferential()).toBe(true); }); it('should return empty for non-self-referential', () => { const rels = analyzer.extractRelationships([postsTable]); const selfRefs = analyzer.getSelfReferentialRelationships(rels); expect(selfRefs.length).toBe(0); }); }); describe('getRequiredRelationships()', () => { it('should get CASCADE relationships', () => { const rels = analyzer.extractRelationships([postsTable, commentsTable]); const required = analyzer.getRequiredRelationships(rels); expect(required.length).toBe(2); // posts->users CASCADE, comments->posts CASCADE expect(required.every((r) => r.isRequired())).toBe(true); }); }); describe('getOptionalRelationships()', () => { it('should get SET NULL relationships', () => { const rels = analyzer.extractRelationships([commentsTable]); const optional = analyzer.getOptionalRelationships(rels); expect(optional.length).toBe(1); // comments->users SET NULL expect(optional[0].isOptional()).toBe(true); }); }); describe('getIndependentTables()', () => { it('should find tables with no FK', () => { const independent = analyzer.getIndependentTables([ usersTable, postsTable, commentsTable, ]); expect(independent.length).toBe(1); expect(independent[0].name).toBe('users'); }); it('should return all tables if none have FK', () => { const independent = analyzer.getIndependentTables([usersTable]); expect(independent.length).toBe(1); }); }); describe('getPopulationOrder()', () => { it('should return topological order', () => { const rels = analyzer.extractRelationships([usersTable, postsTable, commentsTable]); const order = analyzer.getPopulationOrder(rels); // users must come before posts (no dependencies) // posts must come before comments const usersIndex = order.indexOf('users'); const postsIndex = order.indexOf('posts'); const commentsIndex = order.indexOf('comments'); expect(usersIndex).toBeLessThan(postsIndex); expect(postsIndex).toBeLessThan(commentsIndex); }); it('should handle empty relationships', () => { const order = analyzer.getPopulationOrder([]); expect(order.length).toBe(0); }); it('should handle single relationship', () => { const rels = analyzer.extractRelationships([postsTable]); const order = analyzer.getPopulationOrder(rels); expect(order).toContain('users'); expect(order).toContain('posts'); expect(order.indexOf('users')).toBeLessThan(order.indexOf('posts')); }); }); });

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