@cleverbrush/knex-clickhouse 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +218 -0
- package/dist/makeEscape.d.ts +1 -0
- package/dist/makeEscape.js +158 -0
- package/package.json +39 -0
package/README.md
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Knex from 'knex';
|
|
2
|
+
import ClickhouseDriver from '@clickhouse/client';
|
|
3
|
+
export declare class ClickhouseKnexClient extends Knex.Client {
|
|
4
|
+
#private;
|
|
5
|
+
_driver(): typeof ClickhouseDriver;
|
|
6
|
+
constructor(config?: {});
|
|
7
|
+
_escapeBinding(value: any): any;
|
|
8
|
+
/**
|
|
9
|
+
*
|
|
10
|
+
* @param {import('@clickhouse/client').ClickHouseClient} connection
|
|
11
|
+
* @param {import('@clickhouse/client').QueryParams} obj
|
|
12
|
+
* @returns
|
|
13
|
+
*/
|
|
14
|
+
_query(connection: any, obj: any): Promise<any>;
|
|
15
|
+
/**
|
|
16
|
+
*
|
|
17
|
+
* @param {import('@clickhouse/client').ResultSet} obj
|
|
18
|
+
* @param {*} runner
|
|
19
|
+
* @returns
|
|
20
|
+
*/
|
|
21
|
+
processResponse(obj: any, runner: any): any;
|
|
22
|
+
acquireRawConnection(): Promise<any>;
|
|
23
|
+
destroyRawConnection(connection: any): Promise<void>;
|
|
24
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import Knex from 'knex';
|
|
2
|
+
import ClickhouseDriver from '@clickhouse/client';
|
|
3
|
+
import { makeEscape } from './makeEscape.js';
|
|
4
|
+
const parseDate = (value) => new Date(Date.parse(value));
|
|
5
|
+
// this function is taken from knex npm package
|
|
6
|
+
function arrayString(arr, esc) {
|
|
7
|
+
let result = '';
|
|
8
|
+
for (let i = 0; i < arr.length; i++) {
|
|
9
|
+
if (i > 0)
|
|
10
|
+
result += ',';
|
|
11
|
+
const val = arr[i];
|
|
12
|
+
if (val === null || typeof val === 'undefined') {
|
|
13
|
+
result += 'NULL';
|
|
14
|
+
}
|
|
15
|
+
else if (Array.isArray(val)) {
|
|
16
|
+
result += arrayString(val, esc);
|
|
17
|
+
}
|
|
18
|
+
else if (typeof val === 'number') {
|
|
19
|
+
result += val;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
result += esc(val);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return `[${result}]`;
|
|
26
|
+
}
|
|
27
|
+
export class ClickhouseKnexClient extends Knex.Client {
|
|
28
|
+
_driver() {
|
|
29
|
+
return ClickhouseDriver;
|
|
30
|
+
}
|
|
31
|
+
constructor(config = {}) {
|
|
32
|
+
super(config);
|
|
33
|
+
}
|
|
34
|
+
#typeParsers = {
|
|
35
|
+
Date: parseDate,
|
|
36
|
+
DateTime: parseDate,
|
|
37
|
+
'LowCardinality(Date)': parseDate,
|
|
38
|
+
'LowCardinality(DateTime)': parseDate,
|
|
39
|
+
'Nullable(Date)': parseDate,
|
|
40
|
+
'Nullable(DateTime)': parseDate,
|
|
41
|
+
"DateTime('UTC')": parseDate,
|
|
42
|
+
"Nullable(DateTime('UTC'))": parseDate,
|
|
43
|
+
'LowCardinality(Nullable(Date))': parseDate,
|
|
44
|
+
'LowCardinality(Nullable(DateTime))': parseDate,
|
|
45
|
+
UInt64: (value) => Number(value),
|
|
46
|
+
UInt32: (value) => Number(value)
|
|
47
|
+
};
|
|
48
|
+
#selectQueryRegex = /^(\s+)?(select|with)/i;
|
|
49
|
+
_escapeBinding(value) {
|
|
50
|
+
const escapeBinding = makeEscape({
|
|
51
|
+
escapeArray(val, esc) {
|
|
52
|
+
return arrayString(val, esc);
|
|
53
|
+
},
|
|
54
|
+
escapeString(str) {
|
|
55
|
+
let escaped = "'";
|
|
56
|
+
for (let i = 0; i < str.length; i++) {
|
|
57
|
+
const c = str[i];
|
|
58
|
+
if (c === "'") {
|
|
59
|
+
escaped += c + c;
|
|
60
|
+
}
|
|
61
|
+
else if (c === '\\') {
|
|
62
|
+
escaped += c + c;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
escaped += c;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
escaped += "'";
|
|
69
|
+
return escaped;
|
|
70
|
+
},
|
|
71
|
+
escapeObject(val, prepareValue, timezone, seen = []) {
|
|
72
|
+
if (val && typeof val.toPostgres === 'function') {
|
|
73
|
+
seen = seen || [];
|
|
74
|
+
if (seen.indexOf(val) !== -1) {
|
|
75
|
+
throw new Error(`circular reference detected while preparing "${val}" for query`);
|
|
76
|
+
}
|
|
77
|
+
seen.push(val);
|
|
78
|
+
return prepareValue(val.toPostgres(prepareValue), seen);
|
|
79
|
+
}
|
|
80
|
+
return JSON.stringify(val);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const result = escapeBinding(value);
|
|
84
|
+
if (typeof result === 'string') {
|
|
85
|
+
return result.replace(/\\/g, '\\\\');
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
*
|
|
91
|
+
* @param {import('@clickhouse/client').ClickHouseClient} connection
|
|
92
|
+
* @param {import('@clickhouse/client').QueryParams} obj
|
|
93
|
+
* @returns
|
|
94
|
+
*/
|
|
95
|
+
async _query(connection, obj) {
|
|
96
|
+
if (!obj || typeof obj === 'string') {
|
|
97
|
+
obj = { sql: obj };
|
|
98
|
+
}
|
|
99
|
+
else if (!obj.sql) {
|
|
100
|
+
throw new Error('The query is empty');
|
|
101
|
+
}
|
|
102
|
+
let { method } = obj;
|
|
103
|
+
if (method === 'raw' && this.#selectQueryRegex.test(obj.sql)) {
|
|
104
|
+
method = 'select';
|
|
105
|
+
}
|
|
106
|
+
const query = Array.isArray(obj.bindings) && obj.bindings.length > 0
|
|
107
|
+
? obj.sql
|
|
108
|
+
.split('?')
|
|
109
|
+
.map((x, i) => `${x}${obj.bindings.length > i
|
|
110
|
+
? this._escapeBinding(obj.bindings[i])
|
|
111
|
+
: ''}`)
|
|
112
|
+
.join('')
|
|
113
|
+
: obj.sql;
|
|
114
|
+
let response;
|
|
115
|
+
const queryParams = {
|
|
116
|
+
clickhouse_settings: {
|
|
117
|
+
exact_rows_before_limit: true
|
|
118
|
+
},
|
|
119
|
+
query,
|
|
120
|
+
format: 'JSONCompact'
|
|
121
|
+
};
|
|
122
|
+
obj.finalQuery = query;
|
|
123
|
+
switch (method) {
|
|
124
|
+
case 'select':
|
|
125
|
+
case 'first':
|
|
126
|
+
case 'pluck':
|
|
127
|
+
{
|
|
128
|
+
const p = await connection.query(queryParams);
|
|
129
|
+
response = await p.json();
|
|
130
|
+
const { data, meta, ...rest } = response;
|
|
131
|
+
obj.response = [data, meta, { ...rest }];
|
|
132
|
+
}
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
response = await connection.exec(queryParams);
|
|
136
|
+
}
|
|
137
|
+
return obj;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
*
|
|
141
|
+
* @param {import('@clickhouse/client').ResultSet} obj
|
|
142
|
+
* @param {*} runner
|
|
143
|
+
* @returns
|
|
144
|
+
*/
|
|
145
|
+
processResponse(obj, runner) {
|
|
146
|
+
if (obj == null)
|
|
147
|
+
return null;
|
|
148
|
+
const { response } = obj;
|
|
149
|
+
const [rows, fields, meta] = response || [[]];
|
|
150
|
+
const parsers = Array.isArray(fields)
|
|
151
|
+
? fields.map((field) => typeof this.#typeParsers[field.type] === 'function'
|
|
152
|
+
? this.#typeParsers[field.type]
|
|
153
|
+
: (x) => x)
|
|
154
|
+
: fields;
|
|
155
|
+
const rowToObj = (row) => fields.reduce((acc, field, index) => ({
|
|
156
|
+
...acc,
|
|
157
|
+
[field.name]: parsers[index](row[index])
|
|
158
|
+
}), {});
|
|
159
|
+
if (obj.output)
|
|
160
|
+
return obj.output.call(runner, rows, fields);
|
|
161
|
+
let { method } = obj;
|
|
162
|
+
if (method === 'raw' && this.#selectQueryRegex.test(obj.sql)) {
|
|
163
|
+
method = 'select';
|
|
164
|
+
}
|
|
165
|
+
switch (method) {
|
|
166
|
+
case 'select':
|
|
167
|
+
return [rows.map((r) => rowToObj(r)), fields, meta];
|
|
168
|
+
case 'first':
|
|
169
|
+
return [rowToObj(rows[0]), fields, meta];
|
|
170
|
+
case 'pluck': {
|
|
171
|
+
const pluckIndex = fields.findIndex((val) => val.name === obj.pluck);
|
|
172
|
+
if (pluckIndex === -1)
|
|
173
|
+
return rows;
|
|
174
|
+
return [rows.map((row) => row[pluckIndex]), obj.pluck, meta];
|
|
175
|
+
}
|
|
176
|
+
case 'insert':
|
|
177
|
+
case 'del':
|
|
178
|
+
case 'update':
|
|
179
|
+
case 'counter':
|
|
180
|
+
return response;
|
|
181
|
+
default:
|
|
182
|
+
return response;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* @type {import('@clickhouse/client').ClickHouseClient}
|
|
187
|
+
*/
|
|
188
|
+
#connection;
|
|
189
|
+
// Get a raw connection, called by the `pool` whenever a new
|
|
190
|
+
// connection needs to be added to the pool.
|
|
191
|
+
async acquireRawConnection() {
|
|
192
|
+
if (!this.#connection) {
|
|
193
|
+
const connection = ClickhouseDriver.createClient({
|
|
194
|
+
// @ts-ignore
|
|
195
|
+
url: this.config.connection.url,
|
|
196
|
+
// @ts-ignore
|
|
197
|
+
username: this.config.connection.user,
|
|
198
|
+
// @ts-ignore
|
|
199
|
+
password: this.config.connection.password,
|
|
200
|
+
// @ts-ignore
|
|
201
|
+
database: this.config.connection.database,
|
|
202
|
+
compression: { request: true, response: true }
|
|
203
|
+
});
|
|
204
|
+
// @ts-ignore
|
|
205
|
+
connection.connected = true;
|
|
206
|
+
this.#connection = connection;
|
|
207
|
+
}
|
|
208
|
+
return this.#connection;
|
|
209
|
+
}
|
|
210
|
+
async destroyRawConnection(connection) {
|
|
211
|
+
connection.close();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
Object.assign(ClickhouseKnexClient.prototype, {
|
|
215
|
+
dialect: 'clikhouse',
|
|
216
|
+
driverName: 'clikhouse',
|
|
217
|
+
canCancelQuery: true
|
|
218
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const makeEscape: (config?: any) => any;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* NOTE: functions below are taken from knex npm package,
|
|
3
|
+
* unfortunately they are not exported, so we have to copy them here
|
|
4
|
+
* because they are useful when it comes to write your own knex dialect
|
|
5
|
+
*/
|
|
6
|
+
const charsRegex = /[\0\b\t\n\r\x1a"'\\]/g; // eslint-disable-line no-control-regex
|
|
7
|
+
const charsMap = {
|
|
8
|
+
'\0': '\\0',
|
|
9
|
+
'\b': '\\b',
|
|
10
|
+
'\t': '\\t',
|
|
11
|
+
'\n': '\\n',
|
|
12
|
+
'\r': '\\r',
|
|
13
|
+
'\x1a': '\\Z',
|
|
14
|
+
'"': '\\"',
|
|
15
|
+
"'": "\\'",
|
|
16
|
+
'\\': '\\\\'
|
|
17
|
+
};
|
|
18
|
+
const wrapEscape = (escapeFn) => function finalEscape(val, ctx = {}) {
|
|
19
|
+
return escapeFn(val, finalEscape, ctx);
|
|
20
|
+
};
|
|
21
|
+
const escapeObject = (val, finalEscape, ctx) => {
|
|
22
|
+
if (val && typeof val.toSQL === 'function') {
|
|
23
|
+
return val.toSQL(ctx);
|
|
24
|
+
}
|
|
25
|
+
return JSON.stringify(val);
|
|
26
|
+
};
|
|
27
|
+
// eslint-disable-next-line
|
|
28
|
+
const escapeString = (val) => {
|
|
29
|
+
// eslint-disable-next-line
|
|
30
|
+
let chunkIndex = (charsRegex.lastIndex = 0);
|
|
31
|
+
let escapedVal = '';
|
|
32
|
+
let match;
|
|
33
|
+
// eslint-disable-next-line
|
|
34
|
+
while ((match = charsRegex.exec(val))) {
|
|
35
|
+
escapedVal += val.slice(chunkIndex, match.index) + charsMap[match[0]];
|
|
36
|
+
chunkIndex = charsRegex.lastIndex;
|
|
37
|
+
}
|
|
38
|
+
if (chunkIndex === 0) {
|
|
39
|
+
// Nothing was escaped
|
|
40
|
+
return `'${val}'`;
|
|
41
|
+
}
|
|
42
|
+
if (chunkIndex < val.length) {
|
|
43
|
+
return `'${escapedVal}${val.slice(chunkIndex)}'`;
|
|
44
|
+
}
|
|
45
|
+
return `'${escapedVal}'`;
|
|
46
|
+
};
|
|
47
|
+
const bufferToString = (buffer) => `X${escapeString(buffer.toString('hex'))}`;
|
|
48
|
+
const arrayToList = (array, finalEscape, ctx) => {
|
|
49
|
+
let sql = '';
|
|
50
|
+
for (let i = 0; i < array.length; i++) {
|
|
51
|
+
const val = array[i];
|
|
52
|
+
if (Array.isArray(val)) {
|
|
53
|
+
sql +=
|
|
54
|
+
// eslint-disable-next-line
|
|
55
|
+
(i === 0 ? '' : ', ') +
|
|
56
|
+
'(' +
|
|
57
|
+
arrayToList(val, finalEscape, ctx) +
|
|
58
|
+
')';
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
sql += (i === 0 ? '' : ', ') + finalEscape(val, ctx);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return sql;
|
|
65
|
+
};
|
|
66
|
+
const zeroPad = (number, length) => {
|
|
67
|
+
number = number.toString();
|
|
68
|
+
while (number.length < length) {
|
|
69
|
+
number = `0${number}`;
|
|
70
|
+
}
|
|
71
|
+
return number;
|
|
72
|
+
};
|
|
73
|
+
const convertTimezone = (tz) => {
|
|
74
|
+
if (tz === 'Z') {
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
const m = tz.match(/([+\-\s])(\d\d):?(\d\d)?/);
|
|
78
|
+
if (m) {
|
|
79
|
+
return (
|
|
80
|
+
// eslint-disable-next-line
|
|
81
|
+
(m[1] == '-' ? -1 : 1) *
|
|
82
|
+
(parseInt(m[2], 10) + (m[3] ? parseInt(m[3], 10) : 0) / 60) *
|
|
83
|
+
60);
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
};
|
|
87
|
+
const dateToString = (date, finalEscape, ctx = {}) => {
|
|
88
|
+
// @ts-ignore
|
|
89
|
+
const timeZone = ctx.timeZone || 'local';
|
|
90
|
+
const dt = new Date(date);
|
|
91
|
+
let year;
|
|
92
|
+
let month;
|
|
93
|
+
let day;
|
|
94
|
+
let hour;
|
|
95
|
+
let minute;
|
|
96
|
+
let second;
|
|
97
|
+
let millisecond;
|
|
98
|
+
if (timeZone === 'local') {
|
|
99
|
+
year = dt.getFullYear();
|
|
100
|
+
month = dt.getMonth() + 1;
|
|
101
|
+
day = dt.getDate();
|
|
102
|
+
hour = dt.getHours();
|
|
103
|
+
minute = dt.getMinutes();
|
|
104
|
+
second = dt.getSeconds();
|
|
105
|
+
millisecond = dt.getMilliseconds();
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const tz = convertTimezone(timeZone);
|
|
109
|
+
if (tz !== false && tz !== 0) {
|
|
110
|
+
dt.setTime(dt.getTime() + tz * 60000);
|
|
111
|
+
}
|
|
112
|
+
year = dt.getUTCFullYear();
|
|
113
|
+
month = dt.getUTCMonth() + 1;
|
|
114
|
+
day = dt.getUTCDate();
|
|
115
|
+
hour = dt.getUTCHours();
|
|
116
|
+
minute = dt.getUTCMinutes();
|
|
117
|
+
second = dt.getUTCSeconds();
|
|
118
|
+
millisecond = dt.getUTCMilliseconds();
|
|
119
|
+
}
|
|
120
|
+
// YYYY-MM-DD HH:mm:ss.mmm
|
|
121
|
+
return `${zeroPad(year, 4)}-${zeroPad(month, 2)}-${zeroPad(day, 2)} ${zeroPad(hour, 2)}:${zeroPad(minute, 2)}:${zeroPad(second, 2)}.${zeroPad(millisecond, 3)}`;
|
|
122
|
+
};
|
|
123
|
+
export const makeEscape = (config = {}) => {
|
|
124
|
+
const finalEscapeDate = config.escapeDate || dateToString;
|
|
125
|
+
const finalEscapeArray = config.escapeArray || arrayToList;
|
|
126
|
+
const finalEscapeBuffer = config.escapeBuffer || bufferToString;
|
|
127
|
+
const finalEscapeString = config.escapeString || escapeString;
|
|
128
|
+
const finalEscapeObject = config.escapeObject || escapeObject;
|
|
129
|
+
const finalWrap = config.wrap || wrapEscape;
|
|
130
|
+
function escapeFn(val, finalEscape, ctx) {
|
|
131
|
+
if (val === undefined || val === null) {
|
|
132
|
+
return 'NULL';
|
|
133
|
+
}
|
|
134
|
+
// eslint-disable-next-line
|
|
135
|
+
switch (typeof val) {
|
|
136
|
+
case 'boolean':
|
|
137
|
+
return val ? 'true' : 'false';
|
|
138
|
+
case 'number':
|
|
139
|
+
// eslint-disable-next-line
|
|
140
|
+
return val + '';
|
|
141
|
+
case 'object':
|
|
142
|
+
if (val instanceof Date) {
|
|
143
|
+
val = finalEscapeDate(val, finalEscape, ctx);
|
|
144
|
+
}
|
|
145
|
+
else if (Array.isArray(val)) {
|
|
146
|
+
return finalEscapeArray(val, finalEscape, ctx);
|
|
147
|
+
}
|
|
148
|
+
else if (Buffer.isBuffer(val)) {
|
|
149
|
+
return finalEscapeBuffer(val, finalEscape, ctx);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
return finalEscapeObject(val, finalEscape, ctx);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return finalEscapeString(val, finalEscape, ctx);
|
|
156
|
+
}
|
|
157
|
+
return finalWrap ? finalWrap(escapeFn) : escapeFn;
|
|
158
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Andrew Zolotukhin <andrew_zol@cleverbrush.com>",
|
|
3
|
+
"bugs": {
|
|
4
|
+
"url": "https://github.com/cleverbrush/framework/issues",
|
|
5
|
+
"email": "andrew_zol@cleverbrush.com"
|
|
6
|
+
},
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"@clickhouse/client": "^1.0.1"
|
|
9
|
+
},
|
|
10
|
+
"peerDependencies": {
|
|
11
|
+
"knex": ">=3.1.0"
|
|
12
|
+
},
|
|
13
|
+
"description": "Knex Query Builder for ClickHouse",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "CHANGE ME",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"knex",
|
|
20
|
+
"query builder",
|
|
21
|
+
"clickhouse",
|
|
22
|
+
"sql"
|
|
23
|
+
],
|
|
24
|
+
"license": "BSD 3-Clause",
|
|
25
|
+
"main": "./dist/index.js",
|
|
26
|
+
"name": "@cleverbrush/knex-clickhouse",
|
|
27
|
+
"readme": "https://github.com/cleverbrush/framework/tree/master/libs/knex-clickhouse#readme",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "github:cleverbrush/framework"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"watch": "tsc --watch",
|
|
34
|
+
"build": "tsc"
|
|
35
|
+
},
|
|
36
|
+
"type": "module",
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"version": "1.1.6"
|
|
39
|
+
}
|