dobo-clickhouse 2.2.1

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.
@@ -0,0 +1,45 @@
1
+ {
2
+ "plugins": ["plugins/markdown", "jsdoc-plugin-intersection"],
3
+ "opts": {
4
+ "encoding": "utf8",
5
+ "recurse": true,
6
+ "verbose": true,
7
+ "destination": "./docs",
8
+ "template": "node_modules/clean-jsdoc-theme",
9
+ "readme": "./docs/static/home.md",
10
+ "theme_opts": {
11
+ "default_theme": "light",
12
+ "display-module-header": true,
13
+ "title": "DoboRedis API",
14
+ "homepageTitle": "DoboRedis API",
15
+ "sections": ["Classes", "Events", "Modules", "Global"],
16
+ "menu": [{
17
+ "title": "NPM",
18
+ "link": "https://www.npmjs.com/package/dobo-couchdb"
19
+ }, {
20
+ "title": "Github",
21
+ "link": "https://github.com/ardhi/dobo-couchdb"
22
+ }, {
23
+ "title": "Dobo",
24
+ "link": "https://dobo.bajo.app/"
25
+ }, {
26
+ "title": "Bajo",
27
+ "link": "https://bajo.app/"
28
+ }]
29
+ }
30
+ },
31
+ "source": {
32
+ "include": ["."],
33
+ "includePattern": ".+\\.js(doc|x)?$",
34
+ "exclude": ["node_modules", "docs", "test"]
35
+ },
36
+ "markdown": {
37
+ "hardwrap": false,
38
+ "idInHeadings": true
39
+ },
40
+ "sourceType": "module",
41
+ "templates": {
42
+ "cleverLinks": false,
43
+ "monospaceLinks": false
44
+ }
45
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-2025 Ardhi Lukianto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # dobo-clickhouse
2
+
3
+ ![GitHub package.json version](https://img.shields.io/github/package-json/v/ardhi/dobo-clickhouse) ![NPM Version](https://img.shields.io/npm/v/dobo-clickhouse)
4
+
5
+ > <br />**Attention**: I do NOT accept any pull request at the moment, thanks! ([Why?](wiki/CONTRIBUTING.md))<br /><br />
6
+
7
+ Clickhouse driver for [Dobo](https://github.com/ardhi/dobo)
8
+
9
+ ## Installation
10
+
11
+ First, go to your ```{appDir}``` and run the following command in your terminal:
12
+
13
+ ```bash
14
+ $ npm install dobo dobo-clickhouse
15
+ ```
16
+
17
+ And enable the plugin by adding ```dobo```, ```dobo-knex``` and ```dobo-clickhouse``` to the plugin list in either the ```$dataDir/config/.plugins``` file or the ```bajo.plugins``` array within your ```package.json``` file
18
+
19
+ ## Documentations
20
+
21
+ - [Config Object](wiki/CONFIG.md)
22
+ - [API](https://ardhi.github.io/dobo-clickhouse)
23
+ - [Contributing](wiki/CONTRIBUTING.md)
24
+
25
+ ## Hire Me
26
+
27
+ If you have a Bajo Framework-based project and need a professional service or assistance, please <a href="https://github.com/ardhi#professional-service">click here</a>. I'd be happy to work on it at a competitive price and with fast turnaround!
28
+
29
+ ## Support Me
30
+
31
+ Please support me using the channels below. Your donation will motivate me to work faster and more diligently on future development.
32
+
33
+ <a href="https://github.com/sponsors/ardhi">
34
+ <img src="https://img.shields.io/badge/Github-slategrey?style=flat&logo=github" height="50">
35
+ </a>
36
+ <a href="https://www.patreon.com/bajoframework">
37
+ <img src="https://img.shields.io/badge/Patreon-f2c3b2?style=flat&logo=patreon" height="50">
38
+ </a>
39
+ <a href="https://www.paypal.com/ncp/payment/EWLERL7SCUU64">
40
+ <img src="https://img.shields.io/badge/Paypal-blue?style=flat&logo=paypal" height="50">
41
+ </a>
42
+
43
+ <p>
44
+ <div><img alt="bc1qwtv78cwp9ef8hnqaw84fxg5856l0pggqe32g6f" src="docs/static/bitcoin.jpeg" width="150" height="150" /><br>Bitcoin</div>
45
+ </p>
46
+
47
+ ## License
48
+
49
+ [MIT](LICENSE)
@@ -0,0 +1,59 @@
1
+ import clientFactory from '../../../lib/dialect/client.js'
2
+
3
+ async function clickhouseDriverFactory () {
4
+ const { DoboKnexDriver } = this.app.baseClass
5
+
6
+ class DoboClickhouseDriver extends DoboKnexDriver {
7
+ constructor (plugin, name, options) {
8
+ super(plugin, name, options)
9
+ this.idField = {
10
+ name: 'id',
11
+ type: 'string',
12
+ required: true,
13
+ autoInc: true,
14
+ index: 'primary'
15
+ }
16
+ this.idGenerator = 'uuidv7'
17
+ this.defaultEngine = 'MergeTree'
18
+ this.support.returning = false
19
+ this.support.uniqueIndex = false
20
+ this.support.nullableField = false
21
+ }
22
+
23
+ async sanitizeConnection (item) {
24
+ await super.sanitizeConnection(item)
25
+ item.port = item.port ?? 8123
26
+ item.user = item.user ?? 'default'
27
+ item.host = item.host ?? '127.0.0.1'
28
+ item.database = item.database ?? 'default'
29
+ }
30
+
31
+ async connect (connection, noRebuild) {
32
+ const { importPkg } = this.app.bajo
33
+ const knex = await importPkg('doboKnex:knex')
34
+ const client = await clientFactory.call(this.plugin)
35
+ const { user, password, host, port, database } = connection.options
36
+ connection.client = knex({
37
+ client,
38
+ connection: () => {
39
+ return `clickhouse://${user}:${password}@${host}:${port}/${database}`
40
+ },
41
+ ...this.options
42
+ })
43
+ }
44
+
45
+ async updateRecord (model, id, body = {}, options = {}) {
46
+ const oldData = options._data
47
+ const client = model.connection.client
48
+ const result = await client(model.collName).where('id', id).update(body, this._getReturningFields(model, options))
49
+ if (options.noResult) return
50
+ if (this.support.returning) return { data: result[0], oldData }
51
+ const resp = await this.getRecord(model, id)
52
+ return { data: resp.data, oldData }
53
+ }
54
+ }
55
+
56
+ return DoboClickhouseDriver
57
+ }
58
+
59
+ export default clickhouseDriverFactory
package/index.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Plugin factory
3
+ *
4
+ * @param {string} pkgName - NPM package name
5
+ * @returns {class}
6
+ */
7
+ async function factory (pkgName) {
8
+ const me = this
9
+
10
+ /**
11
+ * DoboClickhouse class
12
+ *
13
+ * @class
14
+ */
15
+ class DoboClickhouse extends this.app.baseClass.Base {
16
+ constructor () {
17
+ super(pkgName, me.app)
18
+ this.config = {
19
+ connections: []
20
+ }
21
+ }
22
+
23
+ exit = () => {
24
+ }
25
+ }
26
+
27
+ return DoboClickhouse
28
+ }
29
+
30
+ export default factory
@@ -0,0 +1,239 @@
1
+ // From: https://github.com/DnAp/KnexClickhouseDialect/blob/master/src/client.js
2
+ import ClickHouse from 'clickhouse'
3
+ import _qc from './query/compiler.js'
4
+ import _sc from './schema/compiler.js'
5
+ import _tc from './schema/tablecompiler.js'
6
+ import _cc from './schema/columncompiler.js'
7
+ import TransactionClickHouse from './transaction.js'
8
+ import sqlString from 'sqlstring'
9
+
10
+ function ltrimSlashes (v) {
11
+ return v.replace(/^\//g, '')
12
+ }
13
+
14
+ async function clientFactory () {
15
+ const { importModule } = this.app.bajo
16
+ const { isString, isFunction, assign, map } = this.app.lib._
17
+ const makeEscape = (await importModule('doboKnex:node_modules/knex/lib/util/string.js')).makeEscape
18
+ const TableBuilder = await importModule('doboKnex:node_modules/knex/lib/schema/tablebuilder.js')
19
+ const Client = await importModule('doboKnex:node_modules/knex/lib/client.js')
20
+ const QueryCompiler = await _qc.call(this)
21
+ const SchemaCompilerClickHouse = await _sc.call(this)
22
+ const TableCompilerClickHouse = await _tc.call(this)
23
+ const ColumnCompilerClickHouse = await _cc.call(this)
24
+
25
+ class ClickhouseClient extends Client {
26
+ dialect = 'clickhouse'
27
+ driverName = 'clickhouse'
28
+ _escapeBinding = makeEscape()
29
+ canCancelQuery = true
30
+ _migrationLockTableName = '`knex_migrations_lock`'
31
+
32
+ constructor (config = {}) {
33
+ super(config)
34
+ this.initializeDriver()
35
+ this.initializePool()
36
+ if (config.migrations && config.migrations.tableName) {
37
+ const migrationCfg = config.migrations
38
+ if (migrationCfg.schemaName) {
39
+ this._migrationLockTableName = '`' + migrationCfg.schemaName + '`.`' + migrationCfg.tableName + '_lock`'
40
+ } else {
41
+ this._migrationLockTableName = '`' + migrationCfg.tableName + '_lock`'
42
+ }
43
+ }
44
+ }
45
+
46
+ _driver () {
47
+ return ClickHouse
48
+ }
49
+
50
+ tableBuilder (type, tableName, tableNameLike, fn) {
51
+ const builder = new TableBuilder(this, type, tableName, tableNameLike, fn)
52
+ builder.engine = function (val) {
53
+ this._single.engine = val
54
+ }
55
+ return builder
56
+ }
57
+
58
+ queryCompiler (...args) {
59
+ return new QueryCompiler(this, ...args)
60
+ }
61
+
62
+ schemaCompiler (builder) {
63
+ return new SchemaCompilerClickHouse(this, builder)
64
+ }
65
+
66
+ tableCompiler (tableBuilder) {
67
+ return new TableCompilerClickHouse(this, tableBuilder)
68
+ }
69
+
70
+ columnCompiler () {
71
+ return new ColumnCompilerClickHouse(this, ...arguments)
72
+ }
73
+
74
+ async transaction () {
75
+ return new TransactionClickHouse(this, ...arguments)
76
+ }
77
+
78
+ wrapIdentifierImpl (value) {
79
+ return value !== '*' ? `\`${value.replace(/`/g, '``')}\`` : '*'
80
+ }
81
+
82
+ // Get a raw connection, called by the `pool` whenever a new
83
+ // connection needs to be added to the pool.
84
+ async acquireRawConnection () {
85
+ const config = await this.getConfiguration()
86
+ return new (this.driver.ClickHouse)(config)
87
+ }
88
+
89
+ async getConfiguration () {
90
+ let config = this.config.connection
91
+ if (isFunction(config)) {
92
+ config = await config()
93
+ }
94
+ if (isString(config)) {
95
+ const url = new URL(config)
96
+ config = {
97
+ url: url.hostname,
98
+ port: url.port ? url.port : 8123,
99
+ user: url.username,
100
+ password: url.password,
101
+ database: ltrimSlashes(url.pathname),
102
+ debug: false,
103
+ basicAuth: null,
104
+ isUseGzip: false,
105
+ usePost: true,
106
+ config: {
107
+ session_timeout: 60,
108
+ output_format_json_quote_64bit_integers: 0,
109
+ enable_http_compression: 0
110
+ }
111
+ }
112
+ }
113
+
114
+ return config
115
+ }
116
+
117
+ // Used to explicitly close a connection, called internally by the pool
118
+ // when a connection times out or the pool is shutdown.
119
+ destroyRawConnection (connection) {
120
+ connection.destroy()
121
+ }
122
+
123
+ // eslint-disable-next-line no-unused-vars
124
+ validateConnection (connection) {
125
+ return true
126
+ }
127
+
128
+ // Grab a connection, run the query via the MySQL streaming interface,
129
+ // and pass that through to the stream we've sent back to the client.
130
+ _stream (connection, obj, stream, options) {
131
+ options = options || {}
132
+ const queryOptions = assign({ sql: obj.sql }, obj.options)
133
+ return new Promise((resolve, reject) => {
134
+ stream.on('error', reject)
135
+ stream.on('end', resolve)
136
+ const queryStream = connection
137
+ .query(queryOptions, obj.bindings)
138
+ .stream(options)
139
+
140
+ queryStream.on('error', (err) => {
141
+ reject(err)
142
+ stream.emit('error', err)
143
+ })
144
+
145
+ queryStream.pipe(stream)
146
+ })
147
+ }
148
+
149
+ /**
150
+ * Runs the query on the specified connection, providing the bindingsand any other necessary prep work.
151
+ */
152
+ async _query (connection, obj) {
153
+ if (!obj || typeof obj === 'string') obj = { sql: obj }
154
+ // dirty hack for knex-migrator
155
+ if (['insert', 'update'].includes(obj.method) && obj.sql.indexOf(this._migrationLockTableName) > -1) {
156
+ obj.response = [[], []]
157
+ return obj
158
+ }
159
+ return new Promise((resolve, reject) => {
160
+ if (!obj.sql) {
161
+ resolve()
162
+ return
163
+ }
164
+
165
+ const queryOptions = assign({ sql: obj.sql }, obj.options)
166
+ const query = this._applyBindings(queryOptions.sql, obj.bindings)
167
+
168
+ connection.query(query, (err, rows, fields) => {
169
+ if (err) return reject(err)
170
+ obj.response = [rows, fields]
171
+ resolve(obj)
172
+ })
173
+ })
174
+ }
175
+
176
+ _applyBindings (sql, bindings) {
177
+ for (let i = 0; i < bindings.length; i++) {
178
+ if (bindings[i] instanceof Date) {
179
+ bindings[i] = bindings[i].toISOString()
180
+ .replace(/T/, ' ')
181
+ .replace(/\..+/, '')
182
+ }
183
+ }
184
+ return sqlString.format(sql, bindings)
185
+ }
186
+
187
+ // Process the response as returned from the query.
188
+ processResponse (obj, runner) {
189
+ if (obj == null) return
190
+ const { response } = obj
191
+ const { method } = obj
192
+ const rows = response[0]
193
+ const fields = response[1]
194
+ if (obj.output) return obj.output.call(runner, rows, fields)
195
+ switch (method) {
196
+ case 'select':
197
+ case 'pluck':
198
+ case 'first': {
199
+ if (method === 'pluck') return map(rows, obj.pluck)
200
+ return method === 'first' ? rows[0] : rows
201
+ }
202
+ case 'insert':
203
+ return [rows.insertId]
204
+ case 'del':
205
+ case 'update':
206
+ case 'counter':
207
+ return rows.affectedRows
208
+ default:
209
+ return response
210
+ }
211
+ }
212
+
213
+ cancelQuery (connectionToKill) {
214
+ const acquiringConn = this.acquireConnection()
215
+
216
+ // Error out if we can't acquire connection in time.
217
+ // Purposely not putting timeout on `KILL QUERY` execution because erroring
218
+ // early there would release the `connectionToKill` back to the pool with
219
+ // a `KILL QUERY` command yet to finish.
220
+ return acquiringConn
221
+ .timeout(100)
222
+ .then((conn) => this.query(conn, {
223
+ method: 'raw',
224
+ sql: 'KILL QUERY ?',
225
+ bindings: [connectionToKill.threadId],
226
+ options: {}
227
+ }))
228
+ .finally(() => {
229
+ // NOT returning this promise because we want to release the connection
230
+ // in a non-blocking fashion
231
+ acquiringConn.then((conn) => this.releaseConnection(conn))
232
+ })
233
+ }
234
+ }
235
+
236
+ return ClickhouseClient
237
+ }
238
+
239
+ export default clientFactory
@@ -0,0 +1,76 @@
1
+ // From: https://github.com/DnAp/KnexClickhouseDialect/blob/master/src/query/compiler.js
2
+
3
+ async function factory () {
4
+ const { importModule } = this.app.bajo
5
+ const { identity } = this.app.lib._
6
+ const QueryCompiler = await importModule('doboKnex:node_modules/knex/lib/query/querycompiler.js')
7
+
8
+ class QueryCompilerClickhouse extends QueryCompiler {
9
+ _emptyInsertValue = '() values ()'
10
+
11
+ constructor (client, builder, bindings) {
12
+ super(client, builder, bindings)
13
+ const { returning } = this.single
14
+
15
+ if (returning) {
16
+ this.client.logger.warn(
17
+ '.returning() is not supported by Clickhouse and will not have any effect.'
18
+ )
19
+ }
20
+ }
21
+
22
+ // Update method, including joins, wheres, order & limits.
23
+ /*
24
+ update () {
25
+ const { tableName } = this
26
+ const updateData = this._prepUpdate(this.single.update)
27
+ const wheres = this.where()
28
+ return `ALTER TABLE ${tableName} UPDATE ` +
29
+ updateData.join(', ') +
30
+ (wheres ? ` ${wheres}` : '') + ' SETTINGS mutations_sync = 1'
31
+ }
32
+ */
33
+
34
+ // Compiles a `columnInfo` query.
35
+ columnInfo () {
36
+ const column = this.single.columnInfo
37
+
38
+ // The user may have specified a custom wrapIdentifier function in the config. We
39
+ // need to run the identifiers through that function, but not format them as
40
+ // identifiers otherwise.
41
+ const table = this.client.customWrapIdentifier(this.single.table, identity)
42
+
43
+ return {
44
+ sql:
45
+ 'select * from information_schema.columns where table_name = ? and table_schema = ?',
46
+ bindings: [table, this.client.database()],
47
+ output (resp) {
48
+ const out = resp.reduce(function r (columns, val) {
49
+ columns[val.COLUMN_NAME] = {
50
+ defaultValue: val.COLUMN_DEFAULT,
51
+ type: val.DATA_TYPE,
52
+ maxLength: val.CHARACTER_MAXIMUM_LENGTH,
53
+ nullable: val.IS_NULLABLE === 'YES'
54
+ }
55
+ return columns
56
+ }, {})
57
+ return (column && out[column]) || out
58
+ }
59
+ }
60
+ }
61
+
62
+ limit () {
63
+ // Workaround for offset only.
64
+ // see: http://stackoverflow.com/questions/255517/mysql-offset-infinite-rows
65
+ if (this.single.offset && !this.single.limit && this.single.limit !== 0) return 'limit 18446744073709551615'
66
+
67
+ return super.limit()
68
+ }
69
+ }
70
+
71
+ // Set the QueryBuilder & QueryCompiler on the client object,
72
+ // in case anyone wants to modify things to suit their own purposes.
73
+ return QueryCompilerClickhouse
74
+ }
75
+
76
+ export default factory
@@ -0,0 +1,112 @@
1
+ // From: https://github.com/DnAp/KnexClickhouseDialect/blob/master/src/schema/columncompiler.js
2
+
3
+ async function factory () {
4
+ const { importModule } = this.app.bajo
5
+
6
+ const ColumnCompiler = await importModule('doboKnex:node_modules/knex/lib/schema/columncompiler.js')
7
+ const { toNumber } = await importModule('doboKnex:node_modules/knex/lib/util/helpers.js')
8
+ const Raw = await importModule('doboKnex:node_modules/knex/lib/raw.js')
9
+
10
+ class ColumnCompilerClickHouse extends ColumnCompiler {
11
+ modifiers = ['defaultTo']
12
+
13
+ increments = 'UUID default generateUUIDv7()'
14
+
15
+ bigincrements = 'UUID default generateUUIDv7()'
16
+
17
+ smallint = 'Int8'
18
+
19
+ mediumint = 'Int16'
20
+
21
+ integer = 'Int32'
22
+
23
+ bigint = 'Int64'
24
+
25
+ text = 'String'
26
+
27
+ varchar = 'String'
28
+
29
+ datetime = 'datetime'
30
+
31
+ timestamp = 'datetime'
32
+
33
+ time = 'time'
34
+
35
+ /*
36
+ double (precision, scale) {
37
+ return `Decimal32(${toNumber(precision, 8)}, ${toNumber(scale, 2)})`
38
+ }
39
+ */
40
+
41
+ float = 'Float32'
42
+
43
+ double = 'Float64'
44
+
45
+ enu (allowed) {
46
+ // todo
47
+ // let enumData = [];
48
+ // allowed.forEach((v, k) => {
49
+ // enumData += '';
50
+ // });
51
+ return `enum('${allowed.join('\', \'')}')`
52
+ }
53
+
54
+ bit (length) {
55
+ return length ? `bit(${toNumber(length)})` : 'bit'
56
+ }
57
+
58
+ binary (length) {
59
+ return length ? `varbinary(${toNumber(length)})` : 'blob'
60
+ }
61
+
62
+ json () {
63
+ return 'json'
64
+ }
65
+
66
+ jsonb () {
67
+ return 'json'
68
+ }
69
+
70
+ // Modifiers
71
+ // ------
72
+
73
+ defaultTo (value) {
74
+ if (value === null || value === undefined) {
75
+ return ''
76
+ }
77
+ if (value instanceof Raw) {
78
+ value = value.toQuery()
79
+ } else if (this.type === 'bool' || this.type === 'UInt8') {
80
+ if (value === 'false') value = 0
81
+ value = value ? 1 : 0
82
+ } else {
83
+ value = this.client._escapeBinding(value.toString())
84
+ }
85
+ return 'default ' + value
86
+ }
87
+
88
+ unsigned () {
89
+ return ''
90
+ }
91
+
92
+ comment () {
93
+ return ''
94
+ }
95
+
96
+ first () {
97
+ return ''
98
+ }
99
+
100
+ after (column) {
101
+ return `after ${this.formatter.wrap(column)}`
102
+ }
103
+
104
+ collate (collation) {
105
+ return collation && `collate '${collation}'`
106
+ }
107
+ }
108
+
109
+ return ColumnCompilerClickHouse
110
+ }
111
+
112
+ export default factory
@@ -0,0 +1,47 @@
1
+ // From: https://github.com/DnAp/KnexClickhouseDialect/blob/master/src/schema/compiler.js
2
+
3
+ async function factory () {
4
+ const { importModule } = this.app.bajo
5
+ const SchemaCompiler = await importModule('doboKnex:node_modules/knex/lib/schema/compiler.js')
6
+
7
+ class SchemaCompilerClickHouse extends SchemaCompiler {
8
+ // Rename a table on the schema.
9
+ renameTable (tableName, to) {
10
+ this.pushQuery(
11
+ `rename table ${this.formatter.wrap(tableName)} to ${this.formatter.wrap(to)}`
12
+ )
13
+ }
14
+
15
+ // Check whether a table exists on the query.
16
+ hasTable (tableName) {
17
+ const sql = 'select name from system.tables where name = ? and database = currentDatabase()'
18
+ const bindings = [tableName]
19
+
20
+ this.pushQuery({
21
+ sql,
22
+ bindings,
23
+ output: function output (resp) {
24
+ return resp.length > 0
25
+ }
26
+ })
27
+ }
28
+
29
+ // Check whether a column exists on the schema.
30
+ hasColumn (tableName, column) {
31
+ const sql = 'SELECT name FROM system.columns where table = ? and name = ? and database = currentDatabase()'
32
+ const bindings = [tableName, column]
33
+
34
+ this.pushQuery({
35
+ sql,
36
+ bindings,
37
+ output: function output (resp) {
38
+ return resp.length > 0
39
+ }
40
+ })
41
+ }
42
+ }
43
+
44
+ return SchemaCompilerClickHouse
45
+ }
46
+
47
+ export default factory
@@ -0,0 +1,248 @@
1
+ // From: https://github.com/DnAp/KnexClickhouseDialect/blob/master/src/schema/tablecompiler.js
2
+
3
+ async function factory () {
4
+ const { importModule } = this.app.bajo
5
+ const { isString, isPlainObject } = this.app.lib._
6
+ const TableCompiler = await importModule('doboKnex:node_modules/knex/lib/schema/tablecompiler.js')
7
+ // Table Compiler
8
+ // ------
9
+
10
+ class TableCompilerClickHouse extends TableCompiler {
11
+ addColumnsPrefix = 'add '
12
+ alterColumnsPrefix = 'modify '
13
+ dropColumnPrefix = 'drop '
14
+
15
+ createQuery (columns, ifNot) {
16
+ const createStatement = ifNot ? 'create table if not exists ' : 'create table '
17
+ let sql = createStatement + this.tableName() + ' (' + columns.sql.join(', ') + ')'
18
+
19
+ const engine = this.single.engine || 'TinyLog'
20
+
21
+ if (engine) sql += ` engine = ${engine}`
22
+
23
+ // hack: find primary key statement and put it here
24
+ const primary = this.tableBuilder._statements.find(stmt => stmt.grouping === 'alterTable' && stmt.method === 'primary')
25
+ if (primary && !['TinyLog'].includes(engine)) {
26
+ sql += ` primary key (${this.formatter.columnize(primary.args[0])})`
27
+ }
28
+
29
+ if (this.single.comment) {
30
+ const comment = this.single.comment || ''
31
+ if (comment.length > 60) this.client.logger.warn('The max length for a table comment is 60 characters')
32
+ sql += ` comment = '${comment}'`
33
+ }
34
+
35
+ sql += ' SETTINGS enable_block_number_column = 1, enable_block_offset_column = 1'
36
+ this.pushQuery(sql)
37
+ }
38
+
39
+ // Compiles the comment on the table.
40
+ comment (comment) {
41
+ this.pushQuery(`alter table ${this.tableName()} comment = '${comment}'`)
42
+ }
43
+
44
+ changeType () {
45
+ // alter table + table + ' modify ' + wrapped + '// type';
46
+ }
47
+
48
+ // Renames a column on the table.
49
+ renameColumn (from, to) {
50
+ const compiler = this
51
+ const table = this.tableName()
52
+ const wrapped = this.formatter.wrap(from) + ' ' + this.formatter.wrap(to)
53
+
54
+ this.pushQuery({
55
+ sql:
56
+ `show fields from ${table} where field = ` +
57
+ this.formatter.parameter(from),
58
+ output (resp) {
59
+ const column = resp[0]
60
+ const runner = this
61
+ return compiler.getFKRefs(runner)
62
+ .then(([refs]) => new Promise((resolve, reject) => {
63
+ try {
64
+ if (!refs.length) {
65
+ resolve()
66
+ }
67
+ resolve(compiler.dropFKRefs(runner, refs))
68
+ } catch (e) {
69
+ reject(e)
70
+ }
71
+ })
72
+ .then(function f () {
73
+ let sql = `alter table ${table} change ${wrapped} ${column.Type}`
74
+
75
+ if (String(column.Null)
76
+ .toUpperCase() !== 'YES') {
77
+ sql += ' NOT NULL'
78
+ } else {
79
+ // This doesn't matter for most cases except Timestamp, where this is important
80
+ sql += ' NULL'
81
+ }
82
+ if (column.Default) {
83
+ sql += ` DEFAULT '${column.Default}'`
84
+ }
85
+
86
+ return runner.query({
87
+ sql
88
+ })
89
+ })
90
+ .then(function f () {
91
+ if (!refs.length) {
92
+ return undefined
93
+ }
94
+ return compiler.createFKRefs(
95
+ runner,
96
+ refs.map(function m (ref) {
97
+ if (ref.REFERENCED_COLUMN_NAME === from) {
98
+ ref.REFERENCED_COLUMN_NAME = to
99
+ }
100
+ if (ref.COLUMN_NAME === from) {
101
+ ref.COLUMN_NAME = to
102
+ }
103
+ return ref
104
+ })
105
+ )
106
+ }))
107
+ }
108
+ })
109
+ }
110
+
111
+ getFKRefs (runner) {
112
+ // todo wtf?
113
+ const formatter = this.client.formatter(this.tableBuilder)
114
+ const sql = `SELECT KCU.CONSTRAINT_NAME, KCU.TABLE_NAME, KCU.COLUMN_NAME,
115
+ KCU.REFERENCED_TABLE_NAME, KCU.REFERENCED_COLUMN_NAME,
116
+ RC.UPDATE_RULE,
117
+ RC.DELETE_RULE FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC
118
+ USING(CONSTRAINT_NAME)WHERE KCU.REFERENCED_TABLE_NAME = ${formatter.parameter(this.tableNameRaw)} AND KCU.CONSTRAINT_SCHEMA = ${formatter.parameter(this.client.database())} AND RC.CONSTRAINT_SCHEMA = ${formatter.parameter(this.client.database())}`
119
+
120
+ return runner.query({
121
+ sql,
122
+ bindings: formatter.bindings
123
+ })
124
+ }
125
+
126
+ dropFKRefs (runner, refs) {
127
+ const formatter = this.client.formatter(this.tableBuilder)
128
+
129
+ return Promise.all(
130
+ refs.map(function f (ref) {
131
+ const constraintName = formatter.wrap(ref.CONSTRAINT_NAME)
132
+ const tableName = formatter.wrap(ref.TABLE_NAME)
133
+ return runner.query({
134
+ sql: `alter table ${tableName} drop foreign key ${constraintName}`
135
+ })
136
+ })
137
+ )
138
+ }
139
+
140
+ createFKRefs (runner, refs) {
141
+ const formatter = this.client.formatter(this.tableBuilder)
142
+
143
+ return Promise.all(
144
+ refs.map(function f (ref) {
145
+ const tableName = formatter.wrap(ref.TABLE_NAME)
146
+ const keyName = formatter.wrap(ref.CONSTRAINT_NAME)
147
+ const column = formatter.columnize(ref.COLUMN_NAME)
148
+ const references = formatter.columnize(ref.REFERENCED_COLUMN_NAME)
149
+ const inTable = formatter.wrap(ref.REFERENCED_TABLE_NAME)
150
+ const onUpdate = ` ON UPDATE ${ref.UPDATE_RULE}`
151
+ const onDelete = ` ON DELETE ${ref.DELETE_RULE}`
152
+
153
+ return runner.query({
154
+ sql:
155
+ `alter table ${tableName} add constraint ${keyName} ` +
156
+ 'foreign key (' +
157
+ column +
158
+ ') references ' +
159
+ inTable +
160
+ ' (' +
161
+ references +
162
+ ')' +
163
+ onUpdate +
164
+ onDelete
165
+ })
166
+ })
167
+ )
168
+ }
169
+
170
+ index (columns, indexName, options) {
171
+ let indexType
172
+
173
+ if (isString(options)) {
174
+ indexType = options
175
+ } else if (isPlainObject(options)) {
176
+ ({ indexType } = options)
177
+ }
178
+ if (!indexType) indexType = 'bloom_filter'
179
+
180
+ indexName = indexName
181
+ ? this.formatter.wrap(indexName)
182
+ : this._indexCommand('index', this.tableNameRaw, columns)
183
+ this.pushQuery(
184
+ `alter table ${this.tableName()} add index ${indexName}(${this.formatter.columnize(columns)}) type ${indexType}`
185
+ )
186
+ }
187
+
188
+ primary (columns, constraintName) {
189
+ // these won't work since primary key needs to be defined inside create table
190
+ /*
191
+ constraintName = constraintName
192
+ ? this.formatter.wrap(constraintName)
193
+ : this.formatter.wrap(`${this.tableNameRaw}_pkey`)
194
+ this.pushQuery(
195
+ `alter table ${this.tableName()} add primary key ${constraintName}(${this.formatter.columnize(
196
+ columns
197
+ )})`
198
+ )
199
+ */
200
+ }
201
+
202
+ unique (columns, indexName) {
203
+ indexName = indexName
204
+ ? this.formatter.wrap(indexName)
205
+ : this._indexCommand('unique', this.tableNameRaw, columns)
206
+ this.pushQuery(
207
+ `alter table ${this.tableName()} add unique ${indexName}(${this.formatter.columnize(
208
+ columns
209
+ )})`
210
+ )
211
+ }
212
+
213
+ // Compile a drop index command.
214
+ dropIndex (columns, indexName) {
215
+ indexName = indexName
216
+ ? this.formatter.wrap(indexName)
217
+ : this._indexCommand('index', this.tableNameRaw, columns)
218
+ this.pushQuery(`alter table ${this.tableName()} drop index ${indexName}`)
219
+ }
220
+
221
+ // Compile a drop foreign key command.
222
+ dropForeign (columns, indexName) {
223
+ indexName = indexName
224
+ ? this.formatter.wrap(indexName)
225
+ : this._indexCommand('foreign', this.tableNameRaw, columns)
226
+ this.pushQuery(
227
+ `alter table ${this.tableName()} drop foreign key ${indexName}`
228
+ )
229
+ }
230
+
231
+ // Compile a drop primary key command.
232
+ dropPrimary () {
233
+ // this.pushQuery(`alter table ${this.tableName()} drop primary key`)
234
+ }
235
+
236
+ // Compile a drop unique key command.
237
+ dropUnique (column, indexName) {
238
+ indexName = indexName
239
+ ? this.formatter.wrap(indexName)
240
+ : this._indexCommand('unique', this.tableNameRaw, column)
241
+ this.pushQuery(`alter table ${this.tableName()} drop index ${indexName}`)
242
+ }
243
+ }
244
+
245
+ return TableCompilerClickHouse
246
+ }
247
+
248
+ export default factory
@@ -0,0 +1,30 @@
1
+ // From: https://github.com/DnAp/KnexClickhouseDialect/blob/master/src/transaction.js
2
+
3
+ /**
4
+ * @implements {Knex.Transaction}
5
+ */
6
+ class TransactionClickHouse {
7
+ executionPromise = Promise.resolve(undefined)
8
+
9
+ commit (value) {
10
+ return undefined
11
+ }
12
+
13
+ isCompleted () {
14
+ return true
15
+ }
16
+
17
+ query (conn, sql, status, value) {
18
+ return undefined
19
+ }
20
+
21
+ rollback () {
22
+ return undefined
23
+ }
24
+
25
+ savepoint (transactionScope) {
26
+ return undefined
27
+ }
28
+ }
29
+
30
+ export default TransactionClickHouse
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "dobo-clickhouse",
3
+ "version": "2.2.1",
4
+ "description": "Clickhouse driver for Dobo",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build-doc": "jsdoc -c .jsdoc.conf.json",
8
+ "test": "mocha"
9
+ },
10
+ "type": "module",
11
+ "bajo": {
12
+ "type": "plugin",
13
+ "alias": "dbclickhouse",
14
+ "dependencies": ["dobo", "dobo-knex"]
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/ardhi/dobo-clickhouse.git"
19
+ },
20
+ "keywords": [
21
+ "clickhouse",
22
+ "dobo",
23
+ "db",
24
+ "driver",
25
+ "bajo",
26
+ "framework",
27
+ "modular"
28
+ ],
29
+ "author": "Ardhi Lukianto <ardhi@lukianto.com>",
30
+ "license": "MIT",
31
+ "bugs": {
32
+ "url": "https://github.com/ardhi/dobo-clickhouse/issues"
33
+ },
34
+ "homepage": "https://github.com/ardhi/dobo-clickhouse#readme",
35
+ "devDependencies": {
36
+ "clean-jsdoc-theme": "^4.3.0",
37
+ "jsdoc-plugin-intersection": "^1.0.4"
38
+ },
39
+ "dependencies": {
40
+ "clickhouse": "^2.6.0",
41
+ "sqlstring": "^2.3.3"
42
+ }
43
+ }
@@ -0,0 +1,6 @@
1
+ # Changes
2
+
3
+ ## 2026-01-19
4
+
5
+ - [2.2.0] First published version
6
+