@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 ADDED
@@ -0,0 +1,3 @@
1
+ # Knex client for ClickHouse.
2
+
3
+ This is a library to use [knex](https://knexjs.org/) query builder against ClickHouse. It uses `@clickhouse/client` library to connect to ClickHouse.
@@ -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
+ }