Skip to content

Commit e729ac1

Browse files
committed
added .toRawSQL method for clickhouse dialect
1 parent 265e786 commit e729ac1

8 files changed

Lines changed: 301 additions & 9 deletions

File tree

integration-tests/tests/clickhouse/clickhouse-core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ export const createAllArrayDataTypesTable = async (sql: ClickHouseSQL) => {
8686
\`fixed_string_array\` Array(FixedString(10)),
8787
\`date_array\` Array(Date),
8888
\`date32_array\` Array(Date32),
89-
\`date_time_array\` Array(DateTime),
90-
\`date_time64_array\` Array(DateTime64),
89+
\`dateTime_array\` Array(DateTime),
90+
\`dateTime64_array\` Array(DateTime64),
9191
\`enum_array\` Array(Enum('hello', 'world')),
9292
\`uuid_array\` Array(UUID),
9393
\`json_array\` Array(JSON),

integration-tests/tests/clickhouse/waddler.test.ts

Lines changed: 193 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,83 @@ test('all types in sql.values test', async () => {
490490
.command();
491491

492492
await sql.unsafe(`select * from \`all_data_types\`;`, [], { rowMode: 'object' }).query();
493-
// console.log(res1[0]);
493+
});
494+
495+
test('all types in sql.toRawSQL test', async () => {
496+
await dropAllDataTypesTable(sql);
497+
await createAllDataTypesTable(sql);
498+
499+
const expectedRes = {
500+
int8: 127,
501+
int16: 32767,
502+
int32: 2147483647,
503+
int64: 9223372036854775807n,
504+
int128: 170141183460469231731687303715884105727n,
505+
int256: 57896044618658097711785492504343953926634992332820282019728792003956564819967n,
506+
uint8: 255,
507+
uint16: 65535,
508+
uint32: 4294967295,
509+
uint64: 18446744073709551615n,
510+
uint128: 340282366920938463463374607431768211455n,
511+
uint256: 115792089237316195423570985008687907853269984665640564039457584007913129639935n,
512+
float32: 10.123,
513+
float64: 100.123456,
514+
bfloat16: 1.1171875,
515+
decimal32: 10.23,
516+
decimal64: 100.23,
517+
decimal128: 1000.23,
518+
decimal256: 10000.23,
519+
string: `qwe'"rty`,
520+
fixed_string: `qwe'"rty12`,
521+
date: '2024-10-31',
522+
date32: '2024-10-31',
523+
date_time: new Date('2024-10-31T14:25:29'),
524+
date_time64: new Date('2024-10-31T14:25:29.123'),
525+
enum: 'hello',
526+
uuid: '61f0c404-5cb3-11e7-907b-a6006ad3dba0',
527+
json: {
528+
name: 'alex',
529+
age: 26,
530+
bookIds: [1, 2, 3],
531+
vacationRate: 2.5,
532+
aliases: ['sasha', 'sanya'],
533+
isMarried: true,
534+
},
535+
ipv4: '116.253.40.133',
536+
ipv6: '2a02:aa08:e000:3100::2',
537+
boolean: true,
538+
variant_uint8_string: 'qwerty',
539+
low_cardinality_string: 'qwerty',
540+
nullable_string: '',
541+
point: '(10,-10)',
542+
ring: '[(0,0),(10,0),(10,10),(0,10)]',
543+
line_string: '[(0,0),(10,0),(10,10),(0,10)]',
544+
multi_line_string: '[[(0,0),(10,0),(10,10),(0,10)],[(1,1),(2,2),(3,3)]]',
545+
polygon: '[[(20,20),(50,20),(50,50),(20,50)],[(30,30),(50,50),(50,30)]]',
546+
multi_polygon: '[[[(0,0),(10,0),(10,10),(0,10)]],[[(20,20),(50,20),(50,50),(20,50)],[(30,30),(50,50),(50,30)]]]',
547+
tuple_uint8_string: new TupleParam([0, 'a']),
548+
map_string_uint8: new Map([['key1', 1], ['key2', 10]]),
549+
dynamic: 'qwerty',
550+
} as Record<string, any>;
551+
552+
for (const valueKey of Object.keys(expectedRes)) {
553+
const rawSql0 = sql`insert into \`all_data_types\`(${sql.raw(valueKey)}) values(${expectedRes[valueKey]});`
554+
.toRawSQL();
555+
const rawSql1 = sqlQuery`insert into \`all_data_types\`(${sql.raw(valueKey)}) values(${expectedRes[valueKey]});`
556+
.toRawSQL();
557+
expect(rawSql0).toEqual(rawSql1);
558+
await sql.unsafe(rawSql0, {}).command();
559+
const selectQuery = sql`select (${sql.raw(valueKey)}) from \`all_data_types\` where ${sql.raw(valueKey)} = ${
560+
expectedRes[valueKey]
561+
};`;
562+
const selectQueryRawSql = selectQuery.toRawSQL();
563+
const res0 = await sql.unsafe(selectQueryRawSql);
564+
const res1 = await sql.unsafe(`select (${valueKey}) from \`all_data_types\`;`);
565+
// bigints starting from int128 or uint128 need to be inlined as strings for comparisons to work correctly.
566+
expect(res0.length !== 0 || res1.length !== 0).toBe(true);
567+
568+
await sql.unsafe(`truncate table \`all_data_types\`;`).command();
569+
}
494570
});
495571

496572
test('all array types in sql.values test', async () => {
@@ -555,7 +631,6 @@ test('all array types in sql.values test', async () => {
555631
const query = sql`insert into \`all_array_data_types\` values ${sql.values([allArrayDataTypesValues], types)};`
556632
.command();
557633
await query;
558-
// console.log(query.toSQL());
559634

560635
const res = await sql.unsafe(`select * from \`all_array_data_types\`;`, [], { rowMode: 'object' }).query();
561636

@@ -684,7 +759,122 @@ test('all array types in sql.values test', async () => {
684759
.command();
685760

686761
await sql.unsafe(`select * from \`all_array_data_types\`;`, [], { rowMode: 'object' }).query();
687-
// console.log(res1[0]);
762+
});
763+
764+
test('all array types in sql.toRawSQL test', async () => {
765+
await dropAllArrayDataTypesTable(sql);
766+
await createAllArrayDataTypesTable(sql);
767+
768+
const json = {
769+
name: 'alex',
770+
age: 26,
771+
bookIds: [1, 2, 3],
772+
vacationRate: 2.5,
773+
aliases: ['sasha', 'sanya'],
774+
isMarried: true,
775+
};
776+
777+
const expectedRes = {
778+
int8_array: [-1, 2, 127],
779+
int16_array: [-4, 5, 32767],
780+
int32_array: [-7, 8, 2147483647],
781+
int64_array: [-10, 11, 9223372036854775807n],
782+
int128_array: [-13, 14, 170141183460469231731687303715884105727n],
783+
int256_array: [
784+
-16,
785+
17,
786+
57896044618658097711785492504343953926634992332820282019728792003956564819967n,
787+
],
788+
uint8_array: [1, 2, 255],
789+
uint16_array: [4, 5, 65535],
790+
uint32_array: [7, 8, 4294967295],
791+
uint64_array: [10, 11, 18446744073709551615n],
792+
uint128_array: [13, 14, 340282366920938463463374607431768211455n],
793+
uint256_array: [
794+
16,
795+
17,
796+
115792089237316195423570985008687907853269984665640564039457584007913129639935n,
797+
],
798+
float32_array: [-1.234, 2.345],
799+
float64_array: [-3.456, 4.567],
800+
bfloat16_array: [-0.59765625, 0.69921875],
801+
decimal32_array: [0.123, -1.234],
802+
decimal64_array: [1.234, -2.345],
803+
decimal128_array: [3.456, -4.567],
804+
decimal256_array: [5.678, -6.789],
805+
string_array: [`qwe'"rty1`, `qwe'"rty2`],
806+
fixed_string_array: [`qwe'"rty12`, `qwe'"rty23`],
807+
date_array: ['2024-10-31', '2024-12-01'],
808+
date32_array: ['2024-10-30', '2024-11-30'],
809+
dateTime_array: ['2024-10-31 14:25:29', '2024-12-01 14:25:29'],
810+
dateTime64_array: ['2024-10-30 14:25:29.123', '2024-11-30 14:25:29.123'],
811+
enum_array: ['hello', 'world'],
812+
uuid_array: [
813+
'61f0c404-5cb3-11e7-907b-a6006ad3dba0',
814+
'61f0c404-5cb3-11e7-907b-a6006ad3dba0',
815+
],
816+
json_array: [json, json],
817+
ipv4_array: ['116.253.40.133', '116.253.40.134'],
818+
ipv6_array: ['2a02:aa08:e000:3100::2', '2a02:aa08:e000:3100::1'],
819+
boolean_array: [true, false],
820+
variant_uint8_string_array: ['qwerty', 1],
821+
low_cardinality_string_array: ['qwerty1', 'qwerty2'],
822+
nullable_string_array: [null, null],
823+
point_array: '[(10,-10),(11,-11)]',
824+
ring_array: '[[(0,0),(10,0),(10,10),(0,10)]]',
825+
line_string_array: '[[(0,0),(10,0),(10,10),(0,10)]]',
826+
multi_line_string_array: '[[[(0,0),(10,0),(10,10),(0,10)],[(1,1),(2,2),(3,3)]]]',
827+
polygon_array: '[[[(20,20),(50,20),(50,50),(20,50)],[(30,30),(50,50),(50,30)]]]',
828+
multi_polygon_array:
829+
'[[[[(0,0),(10,0),(10,10),(0,10)]],[[(20,20),(50,20),(50,50),(20,50)],[(30,30),(50,50),(50,30)]]]]',
830+
tuple_uint8_string_array: [new TupleParam([1, 'a']), new TupleParam([2, 'b'])],
831+
map_string_uint8_array: [new Map([['key1', 1], ['key2', 10]]), new Map([['key1', 2], ['key2', 11]])],
832+
dynamic_array: ['qwerty', 'qwerty1'],
833+
} as Record<string, any>;
834+
835+
for (const valueKey of Object.keys(expectedRes)) {
836+
const rawSql0 = sql`insert into \`all_array_data_types\`(${sql.raw(valueKey)}) values(${expectedRes[valueKey]});`
837+
.toRawSQL();
838+
const rawSql1 = sqlQuery`insert into \`all_array_data_types\`(${sql.raw(valueKey)}) values(${
839+
expectedRes[valueKey]
840+
});`
841+
.toRawSQL();
842+
expect(rawSql0).toEqual(rawSql1);
843+
await sql.unsafe(rawSql0, {}).command();
844+
let res0: Record<string, any>[] = [];
845+
if (
846+
// values of those types need to be cast when used in a select query.
847+
![
848+
'int128_array',
849+
'int256_array',
850+
'uint128_array',
851+
'uint256_array',
852+
'decimal32_array',
853+
'decimal64_array',
854+
'decimal128_array',
855+
'decimal256_array',
856+
'date_array',
857+
'date32_array',
858+
'dateTime_array',
859+
'dateTime64_array',
860+
'uuid_array',
861+
'json_array',
862+
'ipv4_array',
863+
'ipv6_array',
864+
'variant_uint8_string_array',
865+
].includes(valueKey)
866+
) {
867+
const selectQuery = sql`select (${sql.raw(valueKey)}) from \`all_array_data_types\` where ${
868+
sql.raw(valueKey)
869+
} = ${expectedRes[valueKey]};`;
870+
const selectQueryRawSql = selectQuery.toRawSQL();
871+
res0 = await sql.unsafe(selectQueryRawSql);
872+
}
873+
const res1 = await sql.unsafe(`select (${valueKey}) from \`all_array_data_types\`;`);
874+
expect(res0.length !== 0 || res1.length !== 0).toBe(true);
875+
876+
await sql.unsafe(`truncate table \`all_array_data_types\`;`).command();
877+
}
688878
});
689879

690880
test('all nd-array types in sql.values test', async () => {

waddler/src/clickhouse-core/dialect.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,63 @@ export class ClickHouseDialect extends Dialect {
152152

153153
throw new Error(`you can't specify ${typeof value} as value.`);
154154
}
155+
156+
override valueToRawSQL(value: Value): { sql: string } {
157+
if (Array.isArray(value)) {
158+
const mappedArray = value.map((valueI) => this.valueToRawSQL(valueI).sql);
159+
const mappedValue = `[${mappedArray.join(',')}]`;
160+
161+
return { sql: mappedValue };
162+
}
163+
164+
if (
165+
typeof value === 'bigint'
166+
|| typeof value === 'number'
167+
|| typeof value === 'boolean'
168+
|| value === null
169+
) {
170+
return { sql: `${value}` };
171+
}
172+
173+
if (value instanceof Date) {
174+
return { sql: `'${value.toISOString().replace('T', ' ').replace('Z', '')}'`.replace(/\.0+/, '') };
175+
}
176+
if (
177+
typeof value === 'string'
178+
) {
179+
return { sql: `'${value.replace(/\\/g, '\\\\').replace(/'/g, String.raw`\'`)}'` };
180+
}
181+
182+
if (value instanceof Map) {
183+
// Map type
184+
const mappedEntries: string[] = [];
185+
for (const entry of value) {
186+
mappedEntries.push(entry.map((entryI) => this.valueToRawSQL(entryI).sql).join(','));
187+
}
188+
const mappedValue = `map(${mappedEntries.join(',')})`;
189+
return { sql: mappedValue };
190+
}
191+
192+
if (typeof value === 'object' && value.constructor?.name === 'TupleParam') {
193+
const mappedTupleParam = (value as { values: any[] }).values.map((tupleParamI) =>
194+
this.valueToRawSQL(tupleParamI).sql
195+
);
196+
const mappedValue = `(${mappedTupleParam.join(',')})`;
197+
return { sql: mappedValue };
198+
}
199+
200+
if (typeof value === 'object') {
201+
// should be JSON type
202+
203+
return { sql: `'${JSON.stringify(value)}'` };
204+
}
205+
206+
if (value === undefined) {
207+
throw new Error("value can't be undefined, maybe you mean sql.default?");
208+
}
209+
210+
throw new Error(`you can't specify ${typeof value} as value.`);
211+
}
155212
}
156213

157214
export class ClickHouseSQLCommonParam extends SQLCommonParam {

waddler/src/clickhouse-core/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { TupleParam } from '@clickhouse/client';
2-
31
export function makeClickHouseArray(array: any[], typeToCast?: string) {
42
let stringifyArray: boolean = true;
53
let baseTypeToCast: string | undefined;
@@ -31,7 +29,9 @@ export function makeClickHouseArray(array: any[], typeToCast?: string) {
3129
}
3230

3331
// Map type
34-
if (value instanceof Map || value instanceof TupleParam) {
32+
if (
33+
value instanceof Map || (typeof value === 'object' && value !== null && value.constructor?.name === 'TupleParam')
34+
) {
3535
return value;
3636
}
3737

waddler/src/mysql/mysql2/session.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export class MySql2SQLTemplate<T> extends SQLTemplate<T> {
5454

5555
// wrapping mysql2 driver error in new js error to add stack trace to it
5656
try {
57-
const conn = ((isPool(this.client) ? await this.client.getConnection() : this.client) as object as {
57+
conn = ((isPool(this.client) ? await this.client.getConnection() : this.client) as object as {
5858
connection: CallbackConnection;
5959
}).connection;
6060

waddler/src/sql-template-params.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ export abstract class Dialect implements BuildQueryConfig {
2929
colIdx: number;
3030
paramsCount: number;
3131
}): { sql: string; addParamsCount?: number };
32+
33+
valueToRawSQL(_value: Value): { sql: string } {
34+
throw new Error(`method valueToRawSQL is not implemented for dialect ${this.constructor.name}`);
35+
}
3236
}
3337

3438
export abstract class SQLChunk {
@@ -57,6 +61,14 @@ export class SQLQuery<DialectT extends Dialect = Dialect> extends SQLChunk {
5761
toSQL() {
5862
return this.generateSQL();
5963
}
64+
65+
/**
66+
* Currently method is implemented only for ClickHouse dialect.
67+
* @returns
68+
*/
69+
toRawSQL() {
70+
return this.sqlWrapper.getRawQuery(this.dialect);
71+
}
6072
}
6173

6274
export class SQLCommonParam extends SQLChunk {

waddler/src/sql-template.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ export abstract class SQLTemplate<T, DialectT extends Dialect = Dialect> {
2626
return this.sqlWrapper.getQuery<DialectT>(this.dialect);
2727
}
2828

29+
/**
30+
* Currently method is implemented only for ClickHouse dialect.
31+
* @returns
32+
*/
33+
toRawSQL() {
34+
return this.sqlWrapper.getRawQuery(this.dialect);
35+
}
36+
2937
catch<TResult = never>(
3038
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined,
3139
): Promise<T[] | TResult> {

waddler/src/sql.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,31 @@ export class SQLWrapper {
116116
return this;
117117
}
118118

119+
getRawQuery(dialect: Dialect) {
120+
let query = '';
121+
122+
for (const chunk of this.queryChunks) {
123+
if (
124+
chunk instanceof SQLString
125+
|| chunk instanceof SQLRaw
126+
|| chunk instanceof SQLDefault
127+
) {
128+
query += chunk.generateSQL().sql;
129+
}
130+
131+
if (chunk instanceof SQLIdentifier) {
132+
query += chunk.generateSQL({ dialect }).sql;
133+
}
134+
135+
if (chunk instanceof SQLValues || chunk instanceof SQLCommonParam) {
136+
const sql = dialect.valueToRawSQL(chunk.value).sql;
137+
query += sql;
138+
}
139+
}
140+
141+
return query;
142+
}
143+
119144
getQuery<
120145
DialectT extends Dialect,
121146
ParamsType extends 'array' | 'object' = IsEqual<DialectT, ClickHouseDialect> extends true ? 'object' : 'array',

0 commit comments

Comments
 (0)