ktx/packages/cli/src/local-adapters.ts

174 lines
6.5 KiB
TypeScript
Raw Normal View History

2026-05-10 23:12:26 +02:00
import { join } from 'node:path';
2026-05-10 23:51:24 +02:00
import { createBigQueryLiveDatabaseIntrospection, isKtxBigQueryConnectionConfig } from '@ktx/connector-bigquery';
import { createClickHouseLiveDatabaseIntrospection, isKtxClickHouseConnectionConfig } from '@ktx/connector-clickhouse';
import { createMysqlLiveDatabaseIntrospection, isKtxMysqlConnectionConfig } from '@ktx/connector-mysql';
2026-05-10 23:12:26 +02:00
import {
createPostgresLiveDatabaseIntrospection,
2026-05-10 23:51:24 +02:00
isKtxPostgresConnectionConfig,
type KtxPostgresConnectionConfig,
KtxPostgresHistoricSqlQueryClient,
} from '@ktx/connector-postgres';
import { createSqliteLiveDatabaseIntrospection, isKtxSqliteConnectionConfig } from '@ktx/connector-sqlite';
import { createSqlServerLiveDatabaseIntrospection, isKtxSqlServerConnectionConfig } from '@ktx/connector-sqlserver';
2026-05-10 23:12:26 +02:00
import {
createDaemonLiveDatabaseIntrospection,
createDefaultLocalIngestAdapters,
type DefaultLocalIngestAdaptersOptions,
type LiveDatabaseIntrospectionPort,
LiveDatabaseSourceAdapter,
type SourceAdapter,
2026-05-10 23:51:24 +02:00
} from '@ktx/context/ingest';
import type { KtxLocalProject } from '@ktx/context/project';
import { createHttpSqlAnalysisPort } from '@ktx/context/sql-analysis';
2026-05-10 23:12:26 +02:00
function hasSnowflakeDriver(connection: unknown): boolean {
return (
typeof connection === 'object' &&
connection !== null &&
String((connection as { driver?: unknown }).driver ?? '').toLowerCase() === 'snowflake'
);
}
2026-05-10 23:51:24 +02:00
function createKtxCliLiveDatabaseIntrospection(
project: KtxLocalProject,
2026-05-10 23:12:26 +02:00
options: DefaultLocalIngestAdaptersOptions = {},
): LiveDatabaseIntrospectionPort {
const daemon = createDaemonLiveDatabaseIntrospection({
connections: project.config.connections,
...options.databaseIntrospection,
...(options.databaseIntrospectionUrl ? { baseUrl: options.databaseIntrospectionUrl } : {}),
});
const sqlite = createSqliteLiveDatabaseIntrospection({
projectDir: project.projectDir,
connections: project.config.connections,
});
const mysql = createMysqlLiveDatabaseIntrospection({
connections: project.config.connections,
});
const postgres = createPostgresLiveDatabaseIntrospection({
connections: project.config.connections,
});
const clickhouse = createClickHouseLiveDatabaseIntrospection({
connections: project.config.connections,
});
const sqlserver = createSqlServerLiveDatabaseIntrospection({
connections: project.config.connections,
});
const bigquery = createBigQueryLiveDatabaseIntrospection({
connections: project.config.connections,
});
return {
async extractSchema(connectionId: string) {
const connection = project.config.connections[connectionId];
2026-05-10 23:51:24 +02:00
if (isKtxPostgresConnectionConfig(connection)) {
2026-05-10 23:12:26 +02:00
return postgres.extractSchema(connectionId);
}
2026-05-10 23:51:24 +02:00
if (isKtxSqliteConnectionConfig(connection)) {
2026-05-10 23:12:26 +02:00
return sqlite.extractSchema(connectionId);
}
2026-05-10 23:51:24 +02:00
if (isKtxMysqlConnectionConfig(connection)) {
2026-05-10 23:12:26 +02:00
return mysql.extractSchema(connectionId);
}
2026-05-10 23:51:24 +02:00
if (isKtxClickHouseConnectionConfig(connection)) {
2026-05-10 23:12:26 +02:00
return clickhouse.extractSchema(connectionId);
}
2026-05-10 23:51:24 +02:00
if (isKtxSqlServerConnectionConfig(connection)) {
2026-05-10 23:12:26 +02:00
return sqlserver.extractSchema(connectionId);
}
2026-05-10 23:51:24 +02:00
if (isKtxBigQueryConnectionConfig(connection)) {
2026-05-10 23:12:26 +02:00
return bigquery.extractSchema(connectionId);
}
if (hasSnowflakeDriver(connection)) {
2026-05-10 23:51:24 +02:00
const { createSnowflakeLiveDatabaseIntrospection, isKtxSnowflakeConnectionConfig } = await import(
'@ktx/connector-snowflake'
2026-05-10 23:12:26 +02:00
);
2026-05-10 23:51:24 +02:00
if (!isKtxSnowflakeConnectionConfig(connection)) {
2026-05-10 23:12:26 +02:00
return daemon.extractSchema(connectionId);
}
const snowflake = createSnowflakeLiveDatabaseIntrospection({
connections: project.config.connections,
});
return snowflake.extractSchema(connectionId);
}
return daemon.extractSchema(connectionId);
},
};
}
2026-05-10 23:51:24 +02:00
interface KtxCliLocalIngestAdaptersOptions extends DefaultLocalIngestAdaptersOptions {
2026-05-10 23:12:26 +02:00
historicSqlConnectionId?: string;
sqlAnalysisUrl?: string;
}
2026-05-10 23:51:24 +02:00
function isEnabledPostgresHistoricSqlConnection(connection: KtxPostgresConnectionConfig | undefined): boolean {
if (!connection || !isKtxPostgresConnectionConfig(connection)) {
2026-05-10 23:12:26 +02:00
return false;
}
const historicSql =
typeof connection.historicSql === 'object' &&
connection.historicSql !== null &&
!Array.isArray(connection.historicSql)
? (connection.historicSql as Record<string, unknown>)
: null;
return historicSql?.enabled === true && historicSql.dialect === 'postgres';
}
2026-05-10 23:51:24 +02:00
function createEphemeralPostgresHistoricSqlClient(project: KtxLocalProject, connectionId: string) {
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
if (!isKtxPostgresConnectionConfig(connection)) {
2026-05-10 23:12:26 +02:00
throw new Error(
`Historic SQL local ingest requires a Postgres connection, got ${String(connection?.driver ?? 'unknown')}`,
);
}
return {
async executeQuery(sql: string, params?: unknown[]) {
2026-05-10 23:51:24 +02:00
const client = new KtxPostgresHistoricSqlQueryClient({
2026-05-10 23:12:26 +02:00
connectionId,
connection,
});
try {
return await client.executeQuery(sql, params);
} finally {
await client.cleanup();
}
},
};
}
2026-05-10 23:51:24 +02:00
function historicSqlOptionsForLocalRun(project: KtxLocalProject, options: KtxCliLocalIngestAdaptersOptions) {
2026-05-10 23:12:26 +02:00
const connectionId = options.historicSqlConnectionId;
if (!connectionId) {
return undefined;
}
2026-05-10 23:51:24 +02:00
const connection = project.config.connections[connectionId] as KtxPostgresConnectionConfig | undefined;
2026-05-10 23:12:26 +02:00
if (!isEnabledPostgresHistoricSqlConnection(connection)) {
return undefined;
}
return {
sqlAnalysis: createHttpSqlAnalysisPort({
baseUrl:
options.sqlAnalysisUrl ??
2026-05-10 23:51:24 +02:00
process.env.KTX_SQL_ANALYSIS_URL ??
process.env.KTX_DAEMON_URL ??
2026-05-10 23:12:26 +02:00
'http://127.0.0.1:8765',
}),
postgresQueryClient: createEphemeralPostgresHistoricSqlClient(project, connectionId),
2026-05-10 23:51:24 +02:00
postgresBaselineRootDir: join(project.projectDir, '.ktx/cache/historic-sql'),
2026-05-10 23:12:26 +02:00
};
}
2026-05-10 23:51:24 +02:00
export function createKtxCliLocalIngestAdapters(
project: KtxLocalProject,
options: KtxCliLocalIngestAdaptersOptions = {},
2026-05-10 23:12:26 +02:00
): SourceAdapter[] {
const historicSql = historicSqlOptionsForLocalRun(project, options);
const base = createDefaultLocalIngestAdapters(project, {
...options,
...(historicSql ? { historicSql } : {}),
});
const liveDatabase = new LiveDatabaseSourceAdapter({
2026-05-10 23:51:24 +02:00
introspection: createKtxCliLiveDatabaseIntrospection(project, options),
2026-05-10 23:12:26 +02:00
});
return base.map((adapter) => (adapter.source === 'live-database' ? liveDatabase : adapter));
}