refactor(cli): resolve relationship dialect at scan boundary

This commit is contained in:
Andrey Avtomonov 2026-05-25 00:29:22 +02:00
parent c0932c98cb
commit f000221f78
4 changed files with 58 additions and 17 deletions

View file

@ -331,6 +331,27 @@ describe('local scan enrichment', () => {
expect(scanConnector.introspect).toHaveBeenCalledTimes(1);
});
it('fails when connector driver and snapshot driver differ', async () => {
const mismatchedConnector: KtxScanConnector = {
...connector(),
driver: 'mysql',
};
await expect(
runLocalScanEnrichment({
connectionId: 'warehouse',
mode: 'relationships',
detectRelationships: true,
connector: mismatchedConnector,
snapshot,
context: { runId: 'scan-run-driver-mismatch' },
providers: null,
}),
).rejects.toThrow(
'ktx scan connector driver "mysql" does not match snapshot driver "postgres" for connection "warehouse"',
);
});
it('runs deterministic relationship detection for relationship scans', async () => {
const result = await runLocalScanEnrichment({
connectionId: 'warehouse',

View file

@ -1,5 +1,6 @@
import pLimit from 'p-limit';
import type { KtxLlmRuntimePort } from '../../context/llm/runtime-port.js';
import { getDialectForDriver } from '../connections/dialects.js';
import { buildDefaultKtxProjectConfig, type KtxScanRelationshipConfig } from '../project/config.js';
import { KtxDescriptionGenerator } from './description-generation.js';
import { buildKtxColumnEmbeddingText } from './embedding-text.js';
@ -118,6 +119,18 @@ function targetMatchesForeignKey(table: KtxEnrichedTable, foreignKey: KtxSchemaF
);
}
function assertConnectorDriverMatchesSnapshot(input: {
connector: KtxScanConnector;
snapshot: KtxSchemaSnapshot;
connectionId: string;
}): void {
if (input.connector.driver !== input.snapshot.driver) {
throw new Error(
`ktx scan connector driver "${input.connector.driver}" does not match snapshot driver "${input.snapshot.driver}" for connection "${input.connectionId}"`,
);
}
}
function formalRelationshipsFromSnapshot(
snapshot: KtxSchemaSnapshot,
tables: readonly KtxEnrichedTable[],
@ -468,6 +481,12 @@ export async function runLocalScanEnrichment(
));
await progress?.update(0.05, `Loaded schema snapshot with ${snapshot.tables.length} tables`);
assertConnectorDriverMatchesSnapshot({
connector: input.connector,
snapshot,
connectionId: input.connectionId,
});
const dialect = getDialectForDriver(snapshot.driver);
const now = input.now ?? (() => new Date());
const state = completedKtxScanEnrichmentStateSummary();
const syncId = input.syncId ?? input.context.runId;
@ -575,7 +594,7 @@ export async function runLocalScanEnrichment(
await relationshipProgress?.update(0, 'Detecting relationships');
const detection = await discoverKtxRelationships({
connectionId: input.connectionId,
driver: snapshot.driver,
dialect,
connector: input.connector,
schema,
context: input.context,

View file

@ -1,6 +1,7 @@
import Database from 'better-sqlite3';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { KtxLlmRuntimePort } from '../../context/llm/runtime-port.js';
import { getDialectForDriver } from '../connections/dialects.js';
import { buildDefaultKtxProjectConfig } from '../project/config.js';
import { snapshotToKtxEnrichedSchema } from './local-enrichment.js';
import {
@ -308,7 +309,7 @@ describe('production relationship discovery', () => {
const result = await discoverKtxRelationships({
connectionId: 'warehouse',
driver: 'sqlite',
dialect: getDialectForDriver('sqlite'),
connector: connector(executor),
schema: snapshotToKtxEnrichedSchema(snapshot()),
context: { runId: 'relationship-run-1' },
@ -347,7 +348,7 @@ describe('production relationship discovery', () => {
const schema = naturalKeySnapshot();
const result = await discoverKtxRelationships({
connectionId: 'warehouse',
driver: 'sqlite',
dialect: getDialectForDriver('sqlite'),
connector: {
...connector(executor),
introspect: async () => schema,
@ -397,7 +398,7 @@ describe('production relationship discovery', () => {
const result = await discoverKtxRelationships({
connectionId: 'warehouse',
driver: 'sqlite',
dialect: getDialectForDriver('sqlite'),
connector: {
...connector(executor),
introspect: async () => sourceSnapshot,
@ -430,7 +431,7 @@ describe('production relationship discovery', () => {
it('keeps candidates review-only when read-only SQL is unavailable', async () => {
const result = await discoverKtxRelationships({
connectionId: 'warehouse',
driver: 'sqlite',
dialect: getDialectForDriver('sqlite'),
connector: connector(null),
schema: snapshotToKtxEnrichedSchema(snapshot()),
context: { runId: 'relationship-run-no-sql' },
@ -456,7 +457,7 @@ describe('production relationship discovery', () => {
const sourceSnapshot = declaredForeignKeySnapshot();
const result = await discoverKtxRelationships({
connectionId: 'warehouse',
driver: 'sqlite',
dialect: getDialectForDriver('sqlite'),
connector: connector(null),
schema: snapshotToKtxEnrichedSchema(sourceSnapshot),
context: { runId: 'formal-metadata-no-sql' },
@ -503,7 +504,7 @@ describe('production relationship discovery', () => {
const result = await discoverKtxRelationships({
connectionId: 'warehouse',
driver: 'sqlite',
dialect: getDialectForDriver('sqlite'),
connector: connector(executor),
schema: snapshotToKtxEnrichedSchema(llmOnlyRelationshipSnapshot()),
context: { runId: 'llm-relationship-orchestrator' },
@ -543,7 +544,7 @@ describe('production relationship discovery', () => {
const result = await discoverKtxRelationships({
connectionId: 'warehouse',
driver: 'sqlite',
dialect: getDialectForDriver('sqlite'),
connector: connector(executor),
schema: snapshotToKtxEnrichedSchema(snapshot()),
context: { runId: 'configured-thresholds' },
@ -604,7 +605,7 @@ describe('production relationship discovery', () => {
const result = await discoverKtxRelationships({
connectionId: 'warehouse',
driver: 'sqlite',
dialect: getDialectForDriver('sqlite'),
connector: {
...connector(executor),
introspect: async () => richSnapshot,
@ -658,7 +659,7 @@ describe('production relationship discovery', () => {
const result = await discoverKtxRelationships({
connectionId: maskedSnapshot.connectionId,
driver: maskedSnapshot.driver,
dialect: getDialectForDriver(maskedSnapshot.driver),
connector: testConnector,
schema: snapshotToKtxEnrichedSchema(maskedSnapshot, new Map()),
context: { runId: 'test:production-composite' },

View file

@ -1,4 +1,5 @@
import type { KtxLlmRuntimePort } from '../../context/llm/runtime-port.js';
import type { KtxDialect } from '../connections/dialects.js';
import type { KtxScanRelationshipConfig } from '../project/config.js';
import type { KtxEnrichedRelationship, KtxEnrichedSchema, KtxRelationshipUpdate } from './enrichment-types.js';
import {
@ -24,7 +25,6 @@ import {
} from './relationship-profiling.js';
import { validateKtxRelationshipDiscoveryCandidates } from './relationship-validation.js';
import type {
KtxConnectionDriver,
KtxScanConnector,
KtxScanContext,
KtxScanEnrichmentSummary,
@ -34,7 +34,7 @@ import type {
export interface DiscoverKtxRelationshipsInput {
connectionId: string;
driver: KtxConnectionDriver;
dialect: KtxDialect;
connector: KtxScanConnector;
schema: KtxEnrichedSchema;
context: KtxScanContext;
@ -122,7 +122,7 @@ function compositeSummary(relationships: readonly KtxCompositeRelationshipCandid
async function detectCompositeRelationships(input: {
connectionId: string;
driver: DiscoverKtxRelationshipsInput['driver'];
dialect: KtxDialect;
schema: KtxEnrichedSchema;
profile: KtxRelationshipProfileArtifact;
executor: KtxRelationshipReadOnlyExecutor | null;
@ -135,7 +135,7 @@ async function detectCompositeRelationships(input: {
try {
const compositeDetection = await discoverKtxCompositeRelationships({
connectionId: input.connectionId,
driver: input.driver,
driver: input.dialect.type,
schema: input.schema,
profiles: input.profile,
executor: input.executor,
@ -223,7 +223,7 @@ export async function discoverKtxRelationships(
const profileCache = createKtxRelationshipProfileCache();
const profile = await profileKtxRelationshipSchema({
connectionId: input.connectionId,
driver: input.driver,
driver: input.dialect.type,
schema: input.schema,
executor,
ctx: input.context,
@ -256,7 +256,7 @@ export async function discoverKtxRelationships(
warnings.push(...llmProposalResult.warnings);
const validated = await validateKtxRelationshipDiscoveryCandidates({
connectionId: input.connectionId,
driver: input.driver,
driver: input.dialect.type,
candidates,
profiles: profile,
executor,
@ -282,7 +282,7 @@ export async function discoverKtxRelationships(
});
const compositeRelationships = await detectCompositeRelationships({
connectionId: input.connectionId,
driver: input.driver,
dialect: input.dialect,
schema: input.schema,
profile,
executor,