Skip to content

Commit 216b1e7

Browse files
committed
added sql function export to clickhouse
1 parent 4f49e0a commit 216b1e7

35 files changed

Lines changed: 295 additions & 147 deletions

File tree

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -184,37 +184,37 @@ export const commonClickHouseTests = () => {
184184
test('base test with number param', (ctx) => {
185185
const res = ctx.sql`select ${1};`.toSQL();
186186

187-
expect(res).toStrictEqual({ query: `select {val1:String};`, params: [['val1', 1]] });
187+
expect(res).toStrictEqual({ query: `select {param1:String};`, params: [['param1', 1]] });
188188
});
189189

190190
test('base test with bigint param', (ctx) => {
191191
const res = ctx.sql`select ${BigInt(10)};`.toSQL();
192192

193-
expect(res).toStrictEqual({ query: `select {val1:String};`, params: [['val1', 10n]] });
193+
expect(res).toStrictEqual({ query: `select {param1:String};`, params: [['param1', 10n]] });
194194
});
195195

196196
test('base test with string param', (ctx) => {
197197
const res = ctx.sql`select ${'hello world.'};`.toSQL();
198198

199-
expect(res).toStrictEqual({ query: `select {val1:String};`, params: [['val1', 'hello world.']] });
199+
expect(res).toStrictEqual({ query: `select {param1:String};`, params: [['param1', 'hello world.']] });
200200
});
201201

202202
test('base test with boolean param', (ctx) => {
203203
const res = ctx.sql`select ${true};`.toSQL();
204204

205-
expect(res).toStrictEqual({ query: `select {val1:String};`, params: [['val1', true]] });
205+
expect(res).toStrictEqual({ query: `select {param1:String};`, params: [['param1', true]] });
206206
});
207207

208208
test('base test with Date param', (ctx) => {
209209
const res = ctx.sql`select ${new Date('10.04.2025')};`.toSQL();
210210

211-
expect(res).toStrictEqual({ query: `select {val1:String};`, params: [['val1', new Date('10.04.2025')]] });
211+
expect(res).toStrictEqual({ query: `select {param1:String};`, params: [['param1', new Date('10.04.2025')]] });
212212
});
213213

214214
test('base test with null param', (ctx) => {
215215
const res = ctx.sql`select ${null};`.toSQL();
216216

217-
expect(res).toStrictEqual({ query: `select {val1:String};`, params: [['val1', null]] });
217+
expect(res).toStrictEqual({ query: `select {param1:String};`, params: [['param1', null]] });
218218
});
219219

220220
// sql.append
@@ -226,8 +226,8 @@ export const commonClickHouseTests = () => {
226226

227227
const res = query.toSQL();
228228
expect(res).toStrictEqual({
229-
query: 'select * from users where id = {val1:String} or id = {val2:String} or id = {val3:String};',
230-
params: [['val1', 1], ['val2', 3], ['val3', 4]],
229+
query: 'select * from users where id = {param1:String} or id = {param2:String} or id = {param3:String};',
230+
params: [['param1', 1], ['param2', 3], ['param3', 4]],
231231
});
232232
});
233233

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { sql } from 'waddler/clickhouse';
2+
3+
export const filter = ({ id, name, email }: { id?: number; name?: string; email?: string }) => {
4+
const filters = [];
5+
if (id) filters.push(sql`id = ${sql.param(id, 'Int32')}`);
6+
if (name) filters.push(sql`name = ${sql.param(name, 'String')}`);
7+
if (email) filters.push(sql`email = ${sql.param(email, 'String')}`);
8+
9+
const finalSqlFilter = sql``;
10+
const sqlFiltersDelimeter = sql` and `;
11+
for (const [idx, filterSql] of filters.entries()) {
12+
finalSqlFilter.append(filterSql);
13+
if (idx !== filters.length - 1) finalSqlFilter.append(sqlFiltersDelimeter);
14+
}
15+
16+
return finalSqlFilter;
17+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as waddlerTools from 'waddler/clickhouse';
2+
3+
export const filter = ({ id, name, email }: { id?: number; name?: string; email?: string }) => {
4+
const filters = [];
5+
if (id) filters.push(waddlerTools.sql`id = ${waddlerTools.sql.param(id, 'Int32')}`);
6+
if (name) filters.push(waddlerTools.sql`name = ${waddlerTools.sql.param(name, 'String')}`);
7+
if (email) filters.push(waddlerTools.sql`email = ${waddlerTools.sql.param(email, 'String')}`);
8+
9+
const finalSqlFilter = waddlerTools.sql``;
10+
const sqlFiltersDelimeter = waddlerTools.sql` and `;
11+
for (const [idx, filterSql] of filters.entries()) {
12+
finalSqlFilter.append(filterSql);
13+
if (idx !== filters.length - 1) finalSqlFilter.append(sqlFiltersDelimeter);
14+
}
15+
16+
return finalSqlFilter;
17+
};

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

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { type ClickHouseClient, createClient, TupleParam } from '@clickhouse/cli
22
import type Docker from 'dockerode';
33
import { afterAll, beforeAll, beforeEach, expect, test } from 'vitest';
44
import type { ClickHouseSQL } from 'waddler/clickhouse';
5-
import { waddler } from 'waddler/clickhouse';
5+
import { sql as sqlQuery, waddler } from 'waddler/clickhouse';
66
import { commonTests } from '../common.test.ts';
77
import { createClickHouseDockerDB, vitestExpectSoftDate } from '../utils.ts';
88
import {
@@ -15,6 +15,8 @@ import {
1515
dropAllDataTypesTable,
1616
dropAllNdarrayDataTypesTable,
1717
} from './clickhouse-core.ts';
18+
import { filter as filter1 } from './test-filters1.ts';
19+
import { filter as filter2 } from './test-filters2.ts';
1820

1921
let clickHouseContainer: Docker.Container;
2022
let clickHouseClient: ClickHouseClient;
@@ -1079,6 +1081,7 @@ test('sql.stream test', async () => {
10791081
});
10801082

10811083
test('query database test from documentation', async () => {
1084+
await sql.unsafe(`drop table if exists users;`).command();
10821085
await sql.unsafe(`create table users(
10831086
id Int32,
10841087
name String,
@@ -1106,7 +1109,11 @@ order by id;
11061109
email: string;
11071110
}[]
11081111
*/
1109-
await sql`alter table ${sql.identifier('users')} update age = ${31} where email = ${user[2]};`.command();
1112+
const query = sql`alter table ${sql.identifier('users')} update age = ${sql.param(31, 'Int32')} where email = ${
1113+
user[2]
1114+
};`.command();
1115+
// console.log(query.toSQL());
1116+
await query;
11101117
// console.log('User info updated!');
11111118

11121119
const stream = sql`select * from ${sql.identifier('users')};`.query().stream();
@@ -1127,5 +1134,55 @@ order by id;
11271134
await sql`alter table ${sql.identifier('users')} delete where email = ${user[2]};`.command();
11281135
// console.log('User deleted!');
11291136

1130-
await sql.unsafe(`drop table users;`).command();
1137+
await sql.unsafe(`drop table if exists users;`).command();
1138+
});
1139+
1140+
test('sql query api test', async () => {
1141+
const filter = sqlQuery`id = ${sql.param(1, 'Int32')} or ${sqlQuery`id = ${2}`}`;
1142+
filter.append(sqlQuery` and email = ${'hello@test.com'}`);
1143+
1144+
const query = sql`select * from ${sqlQuery.identifier('users')} where ${filter};`;
1145+
1146+
query.toSQL();
1147+
filter.toSQL();
1148+
});
1149+
1150+
test('embeding SQLQuery and SQLTemplate test', async () => {
1151+
await sql.unsafe(`drop table if exists users;`).command();
1152+
await sql.unsafe(`create table users(
1153+
id Int32,
1154+
name String,
1155+
age Int32,
1156+
email String
1157+
)
1158+
engine = MergeTree
1159+
order by id;
1160+
`).command();
1161+
1162+
await sql`insert into users values ${
1163+
sql.values([[1, 'a', 23, 'example1@gmail.com'], [2, 'b', 24, 'example2@gmail.com']])
1164+
}`.command();
1165+
1166+
await sql`select * from ${sql.identifier('users')};`;
1167+
// console.log(res);
1168+
1169+
const query1 = sql`select * from ${sql.identifier('users')} where ${filter1({ id: 1, name: 'a' })};`;
1170+
// console.log(query1.toSQL());
1171+
// console.log(await query1);
1172+
const res1 = await query1;
1173+
expect(res1.length).not.toBe(0);
1174+
1175+
const query2 = sql`select * from ${sql.identifier('users')} where ${filter2({ id: 1, name: 'a' })};`;
1176+
// console.log(query2.toSQL());
1177+
// console.log(await query2);
1178+
const res2 = await query2;
1179+
expect(res2.length).not.toBe(0);
1180+
1181+
const query3 = sql`select * from ${sql.identifier('users')} where ${sql`id = ${sql.param(1, 'Int32')}`};`;
1182+
// console.log(query3.toSQL());
1183+
const res3 = await query3;
1184+
// console.log(res3);
1185+
expect(res3.length).not.toBe(0);
1186+
1187+
await sql.unsafe(`drop table if exists users;`).command();
11311188
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ beforeAll(async () => {
4242
gelConnectionParams = dockerPayload.connectionParams;
4343
gelClient = createClient({ ...dockerPayload.connectionParams, tlsSecurity });
4444

45-
// await gelClient.querySQL(`select 1;`);
45+
await gelClient.querySQL(`select 1;`);
4646
sql = waddler({ client: gelClient });
4747
connected = true;
4848
break;

integration-tests/tests/pg/pg-core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const dropMoodEnumType = async (sql: SQL) => {
1919
export const createAllDataTypesTable = async (sql: SQL) => {
2020
await createMoodEnumType(sql);
2121

22-
await sql.unsafe(`create table all_data_types (
22+
await sql.unsafe(`create table if not exists all_data_types (
2323
"integer" integer,
2424
"smallint" smallint,
2525
"bigint" bigint,

integration-tests/tests/sqlite/sqlite-core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export type SqliteSQL = BetterSqlite3SQL | BunSqliteSQL | D1SQL | LibsqlSQL | Du
1010

1111
export const createAllDataTypesTable = async (sql: SqliteSQL) => {
1212
await sql`
13-
CREATE TABLE "all_data_types" (
13+
CREATE TABLE if not exists "all_data_types" (
1414
"integer_number" integer,
1515
"integer_bigint" integer,
1616
"real" real,

waddler/src/clickhouse-core/dialect.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import { getArrayDepth, makeClickHouseArray } from './utils.ts';
66

77
export class ClickHouseDialect extends Dialect {
88
escapeParam(lastParamIdx: number, typeToCast?: string): string {
9-
return `{val${lastParamIdx}:${typeToCast || 'String'}}`;
9+
return `{param${lastParamIdx}:${typeToCast || 'String'}}`;
1010
}
1111

1212
override formParam(param: any, lastParamIdx: number) {
13-
return [`val${lastParamIdx}`, param];
13+
return [`param${lastParamIdx}`, param];
1414
}
1515

1616
escapeIdentifier(identifier: string): string {
@@ -84,7 +84,7 @@ export class ClickHouseDialect extends Dialect {
8484
}
8585

8686
if (typeof value === 'bigint') {
87-
params.push([`val${lastParamIdx + params.length + 1}`, `${value}`] as any);
87+
params.push([`param${lastParamIdx + params.length + 1}`, `${value}`] as any);
8888
return this.escapeParam(lastParamIdx + params.length, types[colIdx]);
8989
}
9090

@@ -97,7 +97,7 @@ export class ClickHouseDialect extends Dialect {
9797
for (let i = 0; i < arrayDepth; i++) arrayTypeToCast = `Array(${arrayTypeToCast})`;
9898
}
9999

100-
params.push([`val${lastParamIdx + params.length + 1}`, mappedValue] as any);
100+
params.push([`param${lastParamIdx + params.length + 1}`, mappedValue] as any);
101101
return this.escapeParam(lastParamIdx + params.length, types[colIdx] ?? arrayTypeToCast);
102102
}
103103

@@ -108,19 +108,19 @@ export class ClickHouseDialect extends Dialect {
108108
|| value === null
109109
|| value instanceof Date
110110
) {
111-
params.push([`val${lastParamIdx + params.length + 1}`, value] as any);
111+
params.push([`param${lastParamIdx + params.length + 1}`, value] as any);
112112
return this.escapeParam(lastParamIdx + params.length, types[colIdx]);
113113
}
114114

115115
if (value instanceof Map || value instanceof TupleParam) {
116116
// Map, Tuple type
117-
params.push([`val${lastParamIdx + params.length + 1}`, value] as any);
117+
params.push([`param${lastParamIdx + params.length + 1}`, value] as any);
118118
return this.escapeParam(lastParamIdx + params.length, types[colIdx]);
119119
}
120120

121121
if (typeof value === 'object') {
122122
// should be JSON type
123-
params.push([`val${lastParamIdx + params.length + 1}`, JSON.stringify(value)] as any);
123+
params.push([`param${lastParamIdx + params.length + 1}`, JSON.stringify(value)] as any);
124124
return this.escapeParam(lastParamIdx + params.length, types[colIdx] ?? 'JSON');
125125
}
126126

waddler/src/clickhouse/driver.ts

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ClickHouseClient } from '@clickhouse/client';
22
import { createClient } from '@clickhouse/client';
33
import type { NodeClickHouseClientConfigOptions } from '@clickhouse/client/dist/config';
4-
import { SQLDefault, SQLIdentifier, SQLRaw, SQLValues } from '~/sql-template-params.ts';
4+
import { SQLCommonParam, SQLDefault, SQLIdentifier, SQLQuery, SQLRaw, SQLValues } from '~/sql-template-params.ts';
55
import { isConfig } from '~/utils.ts';
66
import type { DbType } from '../clickhouse-core/index.ts';
77
import { ClickHouseDialect, UnsafePromise } from '../clickhouse-core/index.ts';
@@ -63,28 +63,55 @@ export interface ClickHouseSQL extends Omit<SQL, 'unsafe' | 'values'> {
6363
* ```
6464
*/
6565
values(value: Values, types?: DbType[]): SQLValues;
66+
param(value: any, type: DbType): SQLCommonParam;
6667
}
6768

69+
export interface ClickHouseSQLQuery
70+
extends Pick<ClickHouseSQL, 'values' | 'param'>, Pick<SQL, 'identifier' | 'raw' | 'default'>
71+
{
72+
(strings: TemplateStringsArray, ...params: SQLParamType[]): SQLQuery;
73+
}
74+
75+
const SQLFunctions = {
76+
identifier: (value: Identifier<IdentifierObject>) => {
77+
return new SQLIdentifier(value);
78+
},
79+
values: (value: Values, types?: DbType[]) => {
80+
return new SQLValues(value, types);
81+
},
82+
param: (value: any, type?: DbType) => {
83+
return new SQLCommonParam(value, type);
84+
},
85+
raw: (value: Raw) => {
86+
return new SQLRaw(value);
87+
},
88+
default: new SQLDefault(),
89+
};
90+
91+
const sql = ((strings: TemplateStringsArray, ...params: SQLParamType[]): SQLQuery => {
92+
const sqlWrapper = new SQLWrapper();
93+
sqlWrapper.with({ templateParams: { strings, params } });
94+
const dialect = new ClickHouseDialect();
95+
96+
return new SQLQuery(sqlWrapper, dialect);
97+
}) as ClickHouseSQLQuery;
98+
99+
Object.assign(sql, SQLFunctions);
100+
101+
export { sql };
102+
68103
const createSqlTemplate = (
69104
client: ClickHouseClient,
70105
dialect: ClickHouseDialect,
71106
): ClickHouseSQL => {
72107
const fn = <T>(strings: TemplateStringsArray, ...params: SQLParamType[]): ClickHouseSQLTemplate<T> => {
73-
const sql = new SQLWrapper();
74-
sql.with({ templateParams: { strings, params } }).prepareQuery(dialect);
75-
return new ClickHouseSQLTemplate<T>(sql, client, dialect);
108+
const sqlWrapper = new SQLWrapper();
109+
sqlWrapper.with({ templateParams: { strings, params } }).prepareQuery(dialect);
110+
return new ClickHouseSQLTemplate<T>(sqlWrapper, client, dialect);
76111
};
77112

78113
Object.assign(fn, {
79-
identifier: (value: Identifier<IdentifierObject>) => {
80-
return new SQLIdentifier(value);
81-
},
82-
values: (value: Values, types?: DbType[]) => {
83-
return new SQLValues(value, types);
84-
},
85-
raw: (value: Raw) => {
86-
return new SQLRaw(value);
87-
},
114+
...SQLFunctions,
88115
unsafe: (
89116
query: string,
90117
params?: UnsafeParamType[],
@@ -93,15 +120,14 @@ const createSqlTemplate = (
93120
params = params ?? [];
94121
options = options ?? { rowMode: 'object' };
95122

96-
const sql = new SQLWrapper();
97-
sql.with({ rawParams: { query, params } });
123+
const sqlWrapper = new SQLWrapper();
124+
sqlWrapper.with({ rawParams: { query, params } });
98125

99-
const unsafeDriver = new ClickHouseSQLTemplate(sql, client, dialect, options);
126+
const unsafeDriver = new ClickHouseSQLTemplate(sqlWrapper, client, dialect, options);
100127
const unsafePromise = new UnsafePromise(unsafeDriver);
101128

102129
return unsafePromise;
103130
},
104-
default: new SQLDefault(),
105131
});
106132

107133
return fn as any;

waddler/src/clickhouse/session.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ export class ClickHouseSQLTemplate<T> extends SQLTemplate<T> {
88
public returningData: boolean = true;
99

1010
constructor(
11-
override sql: SQLWrapper,
11+
override sqlWrapper: SQLWrapper,
1212
protected readonly client: ClickHouseClient,
1313
dialect: ClickHouseDialect,
1414
private options: { rowMode: 'array' | 'object' } = { rowMode: 'object' },
1515
) {
16-
super(sql, dialect);
16+
super(sqlWrapper, dialect);
1717
}
1818

1919
command(): Omit<ClickHouseSQLTemplate<T>, 'command' | 'query'> {
@@ -27,7 +27,7 @@ export class ClickHouseSQLTemplate<T> extends SQLTemplate<T> {
2727
}
2828

2929
async execute() {
30-
const { query, params } = this.sql.getQuery();
30+
const { query, params } = this.sqlWrapper.getQuery();
3131
const queryParams = Object.fromEntries(params);
3232
try {
3333
if (this.returningData) {
@@ -52,7 +52,7 @@ export class ClickHouseSQLTemplate<T> extends SQLTemplate<T> {
5252
}
5353

5454
async *stream() {
55-
const { query, params } = this.sql.getQuery();
55+
const { query, params } = this.sqlWrapper.getQuery();
5656
const queryParams = Object.fromEntries(params);
5757
// wrapping clickhouse driver error in new js error to add stack trace to it
5858
try {

0 commit comments

Comments
 (0)