import Database from 'better-sqlite3'; import { join } from 'node:path'; import { describe, expect, it } from 'vitest'; import { getDialectForDriver } from '../../../src/context/connections/dialects.js'; import { snapshotToKtxEnrichedSchema } from '../../../src/context/scan/local-enrichment.js'; import { loadKtxRelationshipBenchmarkFixture, maskKtxRelationshipBenchmarkSnapshot } from '../../../src/context/scan/relationship-benchmarks.js'; import { discoverKtxCompositeRelationships } from '../../../src/context/scan/relationship-composite-candidates.js'; import { profileKtxRelationshipSchema, type KtxRelationshipReadOnlyExecutor } from '../../../src/context/scan/relationship-profiling.js'; import type { KtxQueryResult, KtxReadOnlyQueryInput, KtxScanContext } from '../../../src/context/scan/types.js'; class TestSqliteExecutor implements KtxRelationshipReadOnlyExecutor { private readonly db: Database.Database; constructor(dataPath: string) { this.db = new Database(dataPath, { readonly: true, fileMustExist: true }); } async executeReadOnly(input: KtxReadOnlyQueryInput, _ctx: KtxScanContext): Promise { const rows = this.db.prepare(input.sql).all() as Record[]; const headers = Object.keys(rows[0] ?? {}); return { headers, rows: rows.map((row) => headers.map((header) => row[header])), totalRows: rows.length, rowCount: rows.length, }; } close(): void { this.db.close(); } } describe('composite relationship discovery detector', () => { it('infers composite primary keys and validates composite foreign keys from row evidence', async () => { const fixtureRoot = new URL('../../fixtures/relationship-benchmarks', import.meta.url); const fixture = await loadKtxRelationshipBenchmarkFixture( join(fixtureRoot.pathname, 'composite_keys_no_declared_constraints'), ); const snapshot = maskKtxRelationshipBenchmarkSnapshot(fixture.snapshot, 'declared_pks_and_declared_fks_removed'); const schema = snapshotToKtxEnrichedSchema(snapshot, new Map()); const executor = new TestSqliteExecutor(fixture.dataPath ?? ''); const profiles = await profileKtxRelationshipSchema({ connectionId: snapshot.connectionId, dialect: getDialectForDriver(snapshot.driver), schema, executor, ctx: { runId: 'test:composite-profile' }, }); const result = await discoverKtxCompositeRelationships({ connectionId: snapshot.connectionId, dialect: getDialectForDriver(snapshot.driver), schema, profiles, executor, ctx: { runId: 'test:composite-detect' }, }); executor.close(); expect(result.primaryKeys.map((item) => `${item.table.name}.(${item.columns.join(',')})`)).toEqual([ 'order_line_allocations.(order_id,line_number,warehouse_code)', 'order_lines.(order_id,line_number)', ]); expect( result.relationships.map( (item) => `${item.from.table.name}.(${item.from.columns.join(',')})->${item.to.table.name}.(${item.to.columns.join(',')})`, ), ).toEqual(['order_line_allocations.(order_id,line_number)->order_lines.(order_id,line_number)']); expect(result.relationships[0]).toMatchObject({ relationshipType: 'many_to_one', status: 'accepted', confidence: 0.95, validation: { targetUniqueness: 1, sourceCoverage: 1, violationCount: 0, violationRatio: 0, reasons: ['composite_validation_passed'], }, }); expect(result.queryCount).toBeGreaterThan(0); }); });