From e1598809b78da7af690e984e6ccb987fe9f73ba7 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Mon, 25 May 2026 00:15:25 +0200 Subject: [PATCH] refactor(connectors): keep concrete dialect classes internal --- .../src/connectors/bigquery/dialect.test.ts | 8 ---- .../cli/src/connectors/bigquery/dialect.ts | 14 +------ .../src/connectors/clickhouse/dialect.test.ts | 9 ----- .../cli/src/connectors/clickhouse/dialect.ts | 40 +------------------ .../cli/src/connectors/mysql/dialect.test.ts | 9 ----- packages/cli/src/connectors/mysql/dialect.ts | 16 +------- .../src/connectors/postgres/dialect.test.ts | 17 -------- .../cli/src/connectors/postgres/dialect.ts | 20 +--------- .../src/connectors/snowflake/dialect.test.ts | 8 ---- .../cli/src/connectors/snowflake/dialect.ts | 5 +-- packages/cli/src/connectors/sqlite/dialect.ts | 5 +-- .../src/connectors/sqlserver/dialect.test.ts | 11 ----- .../cli/src/connectors/sqlserver/dialect.ts | 12 +----- 13 files changed, 7 insertions(+), 167 deletions(-) diff --git a/packages/cli/src/connectors/bigquery/dialect.test.ts b/packages/cli/src/connectors/bigquery/dialect.test.ts index d2033bd9..0008932a 100644 --- a/packages/cli/src/connectors/bigquery/dialect.test.ts +++ b/packages/cli/src/connectors/bigquery/dialect.test.ts @@ -38,14 +38,6 @@ describe('KtxBigQueryDialect', () => { ); }); - it('rewrites colon parameters to BigQuery named parameters', () => { - expect(dialect.prepareQuery('SELECT * FROM orders WHERE id = :id AND id_2 = :id_2', { id: 1, id_2: 2 })).toEqual({ - sql: 'SELECT * FROM orders WHERE id = @id AND id_2 = @id_2', - params: { id: 1, id_2: 2 }, - }); - expect(dialect.prepareQuery('SELECT * FROM orders')).toEqual({ sql: 'SELECT * FROM orders', params: undefined }); - }); - it('keeps unsupported statistics explicit', () => { expect(dialect.generateColumnStatisticsQuery('analytics', 'orders')).toBeNull(); }); diff --git a/packages/cli/src/connectors/bigquery/dialect.ts b/packages/cli/src/connectors/bigquery/dialect.ts index 3a46f416..0e2f883e 100644 --- a/packages/cli/src/connectors/bigquery/dialect.ts +++ b/packages/cli/src/connectors/bigquery/dialect.ts @@ -10,6 +10,7 @@ import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/typ type BigQueryTableNameRef = Pick & Partial>; +/** @internal */ export class KtxBigQueryDialect implements KtxDialect { readonly type = 'bigquery' as const; @@ -107,19 +108,6 @@ export class KtxBigQueryDialect implements KtxDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND TRIM(CAST(${quotedColumn} AS STRING)) != '' ORDER BY RAND() LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: Record } { - if (!params) { - return { sql, params: undefined }; - } - let processedSql = sql; - const processedParams: Record = {}; - for (const [key, value] of Object.entries(params)) { - processedSql = processedSql.replace(new RegExp(`:${key}\\b`, 'g'), `@${key}`); - processedParams[key] = value; - } - return { sql: processedSql, params: Object.keys(processedParams).length > 0 ? processedParams : undefined }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; diff --git a/packages/cli/src/connectors/clickhouse/dialect.test.ts b/packages/cli/src/connectors/clickhouse/dialect.test.ts index be6fcee5..de1deda1 100644 --- a/packages/cli/src/connectors/clickhouse/dialect.test.ts +++ b/packages/cli/src/connectors/clickhouse/dialect.test.ts @@ -36,13 +36,4 @@ describe('KtxClickHouseDialect', () => { expect(dialect.getLimitOffsetClause(10, 20)).toBe('LIMIT 10 OFFSET 20'); }); - it('prepares named parameters using ClickHouse typed placeholders', () => { - expect(dialect.prepareQuery('select * from events where id = :id and event_name = :name', { - id: 10, - name: 'signup', - })).toEqual({ - sql: 'select * from events where id = {id:Int64} and event_name = {name:String}', - params: { id: 10, name: 'signup' }, - }); - }); }); diff --git a/packages/cli/src/connectors/clickhouse/dialect.ts b/packages/cli/src/connectors/clickhouse/dialect.ts index 5a43c8fa..9e470cae 100644 --- a/packages/cli/src/connectors/clickhouse/dialect.ts +++ b/packages/cli/src/connectors/clickhouse/dialect.ts @@ -10,6 +10,7 @@ import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/typ type ClickHouseTableNameRef = Pick & Partial>; +/** @internal */ export class KtxClickHouseDialect implements KtxDialect { readonly type = 'clickhouse' as const; @@ -115,29 +116,6 @@ export class KtxClickHouseDialect implements KtxDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND trim(toString(${quotedColumn})) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: Record } { - if (!params) { - return { sql, params: undefined }; - } - - let parameterizedQuery = sql; - const queryParams: Record = {}; - const sortedKeys = Object.keys(params).sort((a, b) => b.length - a.length); - - for (const key of sortedKeys) { - const placeholder = `:${key}`; - if (parameterizedQuery.includes(placeholder)) { - parameterizedQuery = parameterizedQuery.replace( - new RegExp(`:${key}\\b`, 'g'), - `{${key}:${this.inferClickHouseType(params[key])}}`, - ); - queryParams[key] = params[key]; - } - } - - return { sql: parameterizedQuery, params: queryParams }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; @@ -220,20 +198,4 @@ export class KtxClickHouseDialect implements KtxDialect { return value.startsWith(prefix) && value.endsWith(')') ? value.slice(prefix.length, -1) : value; } - private inferClickHouseType(value: unknown): string { - if (value === null || value === undefined) { - return 'String'; - } - if (typeof value === 'boolean') { - return 'Bool'; - } - if (typeof value === 'number') { - return Number.isInteger(value) ? 'Int64' : 'Float64'; - } - if (value instanceof Date) { - return 'DateTime'; - } - return 'String'; - } - } diff --git a/packages/cli/src/connectors/mysql/dialect.test.ts b/packages/cli/src/connectors/mysql/dialect.test.ts index d50dcb39..d22f286f 100644 --- a/packages/cli/src/connectors/mysql/dialect.test.ts +++ b/packages/cli/src/connectors/mysql/dialect.test.ts @@ -36,13 +36,4 @@ describe('KtxMysqlDialect', () => { expect(dialect.getLimitOffsetClause(10, 20)).toBe('LIMIT 10 OFFSET 20'); }); - it('prepares named parameters in deterministic SQL placeholder order', () => { - expect(dialect.prepareQuery('select * from orders where id = :id and status = :status', { - status: 'paid', - id: 10, - })).toEqual({ - sql: 'select * from orders where id = ? and status = ?', - params: [10, 'paid'], - }); - }); }); diff --git a/packages/cli/src/connectors/mysql/dialect.ts b/packages/cli/src/connectors/mysql/dialect.ts index 6e2f1e63..7f9cc725 100644 --- a/packages/cli/src/connectors/mysql/dialect.ts +++ b/packages/cli/src/connectors/mysql/dialect.ts @@ -10,6 +10,7 @@ import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/typ type MysqlTableNameRef = Pick & Partial>; +/** @internal */ export class KtxMysqlDialect implements KtxDialect { readonly type = 'mysql' as const; @@ -109,21 +110,6 @@ export class KtxMysqlDialect implements KtxDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND TRIM(CAST(${quotedColumn} AS CHAR)) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: unknown[] } { - if (!params) { - return { sql, params: undefined }; - } - const values: unknown[] = []; - const parameterizedQuery = sql.replace(/:([A-Za-z_][A-Za-z0-9_]*)\b/g, (placeholder, key: string) => { - if (!(key in params)) { - return placeholder; - } - values.push(params[key]); - return '?'; - }); - return { sql: parameterizedQuery, params: values }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; diff --git a/packages/cli/src/connectors/postgres/dialect.test.ts b/packages/cli/src/connectors/postgres/dialect.test.ts index fa5e28b3..596cae49 100644 --- a/packages/cli/src/connectors/postgres/dialect.test.ts +++ b/packages/cli/src/connectors/postgres/dialect.test.ts @@ -31,21 +31,4 @@ describe('KtxPostgresDialect', () => { expect(dialect.generateColumnStatisticsQuery('public', 'orders')).toContain('FROM pg_stats s'); }); - it('prepares named parameters with PostgreSQL positional parameters', () => { - expect( - dialect.prepareQuery('select * from orders where id = :id and status = :status', { id: 1, status: 'paid' }), - ).toEqual({ - sql: 'select * from orders where id = $1 and status = $2', - params: [1, 'paid'], - }); - expect( - dialect.prepareQuery('select :Client_Name_10, :Client_Name_1', { - Client_Name_1: 'short', - Client_Name_10: 'long', - }), - ).toEqual({ - sql: 'select $2, $1', - params: ['short', 'long'], - }); - }); }); diff --git a/packages/cli/src/connectors/postgres/dialect.ts b/packages/cli/src/connectors/postgres/dialect.ts index c3d6b77a..49d5677d 100644 --- a/packages/cli/src/connectors/postgres/dialect.ts +++ b/packages/cli/src/connectors/postgres/dialect.ts @@ -10,6 +10,7 @@ import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/typ type PostgresTableNameRef = Pick & Partial>; +/** @internal */ export class KtxPostgresDialect implements KtxDialect { readonly type = 'postgres' as const; @@ -110,25 +111,6 @@ export class KtxPostgresDialect implements KtxDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND TRIM(CAST(${quotedColumn} AS TEXT)) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: unknown[] } { - if (!params) { - return { sql, params: undefined }; - } - const paramNames = Object.keys(params); - const values: unknown[] = new Array(paramNames.length); - const paramIndexMap = new Map(); - paramNames.forEach((name, index) => { - paramIndexMap.set(name, index + 1); - values[index] = params[name]; - }); - const sortedKeys = [...paramNames].sort((a, b) => b.length - a.length); - let parameterizedQuery = sql; - for (const name of sortedKeys) { - parameterizedQuery = parameterizedQuery.replace(new RegExp(`:${name}\\b`, 'g'), `$${paramIndexMap.get(name)}`); - } - return { sql: parameterizedQuery, params: values }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; diff --git a/packages/cli/src/connectors/snowflake/dialect.test.ts b/packages/cli/src/connectors/snowflake/dialect.test.ts index 991a30b5..8d34a0fc 100644 --- a/packages/cli/src/connectors/snowflake/dialect.test.ts +++ b/packages/cli/src/connectors/snowflake/dialect.test.ts @@ -36,14 +36,6 @@ describe('KtxSnowflakeDialect', () => { ); }); - it('passes Snowflake positional parameters as bind arrays', () => { - expect(dialect.prepareQuery('SELECT * FROM ORDERS WHERE ID = ? AND STATUS = ?', { id: 1, status: 'paid' })).toEqual({ - sql: 'SELECT * FROM ORDERS WHERE ID = ? AND STATUS = ?', - params: [1, 'paid'], - }); - expect(dialect.prepareQuery('SELECT * FROM ORDERS')).toEqual({ sql: 'SELECT * FROM ORDERS', params: undefined }); - }); - it('keeps unsupported statistics explicit', () => { expect(dialect.generateColumnStatisticsQuery('PUBLIC', 'ORDERS')).toBeNull(); }); diff --git a/packages/cli/src/connectors/snowflake/dialect.ts b/packages/cli/src/connectors/snowflake/dialect.ts index 322aeeb7..3fe04101 100644 --- a/packages/cli/src/connectors/snowflake/dialect.ts +++ b/packages/cli/src/connectors/snowflake/dialect.ts @@ -10,6 +10,7 @@ import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/typ type SnowflakeTableNameRef = Pick & Partial>; +/** @internal */ export class KtxSnowflakeDialect implements KtxDialect { readonly type = 'snowflake' as const; @@ -110,10 +111,6 @@ export class KtxSnowflakeDialect implements KtxDialect { return `SELECT ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND TRIM(CAST(${quotedColumn} AS STRING)) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: unknown[] } { - return { sql, params: params ? Object.values(params) : undefined }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; diff --git a/packages/cli/src/connectors/sqlite/dialect.ts b/packages/cli/src/connectors/sqlite/dialect.ts index fa626cc3..5472b674 100644 --- a/packages/cli/src/connectors/sqlite/dialect.ts +++ b/packages/cli/src/connectors/sqlite/dialect.ts @@ -10,6 +10,7 @@ import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/typ type SqliteTableNameRef = Pick & Partial>; +/** @internal */ export class KtxSqliteDialect implements KtxDialect { readonly type = 'sqlite' as const; @@ -96,10 +97,6 @@ export class KtxSqliteDialect implements KtxDialect { return `SELECT ${quoted} FROM ${tableName} WHERE ${quoted} IS NOT NULL AND TRIM(CAST(${quoted} AS TEXT)) != '' LIMIT ${limit}`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: unknown } { - return params ? { sql, params } : { sql }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return ''; diff --git a/packages/cli/src/connectors/sqlserver/dialect.test.ts b/packages/cli/src/connectors/sqlserver/dialect.test.ts index 8890f332..863b01f4 100644 --- a/packages/cli/src/connectors/sqlserver/dialect.test.ts +++ b/packages/cli/src/connectors/sqlserver/dialect.test.ts @@ -34,15 +34,4 @@ describe('KtxSqlServerDialect', () => { expect(dialect.getLimitOffsetClause(10, 20)).toBe(''); }); - it('prepares named parameters using SQL Server @ parameters', () => { - expect( - dialect.prepareQuery('select * from events where id = :id and name = :name', { - id: 10, - name: 'signup', - }), - ).toEqual({ - sql: 'select * from events where id = @id and name = @name', - params: { id: 10, name: 'signup' }, - }); - }); }); diff --git a/packages/cli/src/connectors/sqlserver/dialect.ts b/packages/cli/src/connectors/sqlserver/dialect.ts index 00b04fe8..6b1804f4 100644 --- a/packages/cli/src/connectors/sqlserver/dialect.ts +++ b/packages/cli/src/connectors/sqlserver/dialect.ts @@ -10,6 +10,7 @@ import type { KtxSchemaDimensionType, KtxTableRef } from '../../context/scan/typ type SqlServerTableNameRef = Pick & Partial>; +/** @internal */ export class KtxSqlServerDialect implements KtxDialect { readonly type = 'sqlserver' as const; @@ -104,17 +105,6 @@ export class KtxSqlServerDialect implements KtxDialect { return `SELECT TOP ${limit} ${quotedColumn} FROM ${tableName} WHERE ${quotedColumn} IS NOT NULL AND LTRIM(RTRIM(CAST(${quotedColumn} AS NVARCHAR(MAX)))) != ''`; } - prepareQuery(sql: string, params?: Record): { sql: string; params?: Record } { - if (!params) { - return { sql, params: undefined }; - } - let parameterizedQuery = sql; - for (const key of Object.keys(params)) { - parameterizedQuery = parameterizedQuery.replace(new RegExp(`:${key}\\b`, 'g'), `@${key}`); - } - return { sql: parameterizedQuery, params }; - } - getRandomSampleFilter(samplePct: number): string { if (samplePct <= 0 || samplePct >= 1) { return '';