@cubejs-backend/dremio-driver 1.4.2 → 1.4.4
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/CHANGELOG.md +11 -0
- package/driver/DremioDriver.js +4 -8
- package/driver/DremioQuery.js +13 -1
- package/package.json +9 -9
- package/test/DremioQuery.test.ts +45 -0
- package/test/unit/params-escaping.test.ts +88 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.4.4](https://github.com/cube-js/cube/compare/v1.4.3...v1.4.4) (2026-07-28)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- **pinot/dremio/ksql/databriks/hive/jdbc-driver:** Сorrect SQL parameter escaping ([#11374](https://github.com/cube-js/cube/issues/11374)) ([e31ccf0](https://github.com/cube-js/cube/commit/e31ccf0e040bf6df0a0898797552302352b0aa99))
|
|
11
|
+
- **schema-compiler:** Escape LIKE wildcards on Tesseract for all dialects ([0432799](https://github.com/cube-js/cube/commit/04327997e013d11432b007710db51913875680ab)), closes [#11345](https://github.com/cube-js/cube/issues/11345) [#11345](https://github.com/cube-js/cube/issues/11345) [#10862](https://github.com/cube-js/cube/issues/10862) [#11345](https://github.com/cube-js/cube/issues/11345) [#11345](https://github.com/cube-js/cube/issues/11345) [#11345](https://github.com/cube-js/cube/issues/11345) [#11345](https://github.com/cube-js/cube/issues/11345) [opensearch-project/sql#779](https://github.com/opensearch-project/sql/issues/779) [#11345](https://github.com/cube-js/cube/issues/11345)
|
|
12
|
+
|
|
13
|
+
## [1.4.3](https://github.com/cube-js/cube/compare/v1.4.2...v1.4.3) (2026-06-08)
|
|
14
|
+
|
|
15
|
+
**Note:** Version bump only for package @cubejs-backend/dremio-driver
|
|
16
|
+
|
|
6
17
|
## [1.4.2](https://github.com/cube-js/cube/compare/v1.4.1...v1.4.2) (2025-12-16)
|
|
7
18
|
|
|
8
19
|
**Note:** Version bump only for package @cubejs-backend/dremio-driver
|
package/driver/DremioDriver.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
const {
|
|
8
8
|
getEnv,
|
|
9
9
|
assertDataSource,
|
|
10
|
+
formatAnsi,
|
|
10
11
|
pausePromise,
|
|
11
12
|
} = require('@cubejs-backend/shared');
|
|
12
13
|
const axios = require('axios');
|
|
13
|
-
const SqlString = require('sqlstring');
|
|
14
14
|
const { BaseDriver } = require('@cubejs-backend/base-driver');
|
|
15
15
|
const DremioQuery = require('./DremioQuery');
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ const DremioQuery = require('./DremioQuery');
|
|
|
18
18
|
// @see https://docs.dremio.com/rest-api/jobs/get-job.html
|
|
19
19
|
const DREMIO_JOB_LIMIT = 500;
|
|
20
20
|
|
|
21
|
-
const applyParams = (query, params) =>
|
|
21
|
+
const applyParams = (query, params) => formatAnsi(query, params);
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Dremio driver class.
|
|
@@ -207,12 +207,7 @@ class DremioDriver extends BaseDriver {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
async query(query, values) {
|
|
210
|
-
const queryString = applyParams(
|
|
211
|
-
query,
|
|
212
|
-
(values || []).map(s => (typeof s === 'string' ? {
|
|
213
|
-
toSqlString: () => SqlString.escape(s).replace(/\\\\([_%])/g, '\\$1').replace(/\\'/g, '\'\'')
|
|
214
|
-
} : s))
|
|
215
|
-
);
|
|
210
|
+
const queryString = applyParams(query, values || []);
|
|
216
211
|
|
|
217
212
|
await this.getToken();
|
|
218
213
|
const jobId = await this.executeQuery(queryString);
|
|
@@ -280,3 +275,4 @@ class DremioDriver extends BaseDriver {
|
|
|
280
275
|
}
|
|
281
276
|
|
|
282
277
|
module.exports = DremioDriver;
|
|
278
|
+
module.exports.applyParams = applyParams;
|
package/driver/DremioQuery.js
CHANGED
|
@@ -13,10 +13,18 @@ const GRANULARITY_TO_INTERVAL = {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
class DremioFilter extends BaseFilter {
|
|
16
|
+
/**
|
|
17
|
+
* ILIKE(<expression>, <pattern> [, <escape_character>]) → boolean
|
|
18
|
+
* Dremio's ILIKE is a function, not an operator, so `NOT` negates the call from the outside
|
|
19
|
+
* and the escape character is its optional third argument — without it
|
|
20
|
+
* BaseFilter.escapeWildcardChars has no effect, as ILIKE has no default escape character.
|
|
21
|
+
* The 3-arg form is documented for Dremio Cloud and for Software down to 24.3.x.
|
|
22
|
+
* @see https://docs.dremio.com/cloud/reference/sql/sql-functions/functions/ILIKE/
|
|
23
|
+
*/
|
|
16
24
|
likeIgnoreCase(column, not, param, type) {
|
|
17
25
|
const p = (!type || type === 'contains' || type === 'ends') ? '%' : '';
|
|
18
26
|
const s = (!type || type === 'contains' || type === 'starts') ? '%' : '';
|
|
19
|
-
return
|
|
27
|
+
return `${not ? 'NOT ' : ''}ILIKE(${column}, CONCAT('${p}', ${this.allocateParam(param)}, '${s}'), '\\')`;
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
castParameter() {
|
|
@@ -165,6 +173,10 @@ class DremioQuery extends BaseQuery {
|
|
|
165
173
|
templates.functions.DATEDIFF = 'DATE_DIFF(DATE, DATE_TRUNC(\'{{ date_part }}\', {{ args[1] }}), DATE_TRUNC(\'{{ date_part }}\', {{ args[2] }}))';
|
|
166
174
|
templates.expressions.interval_single_date_part = 'CAST({{ num }} as INTERVAL {{ date_part }})';
|
|
167
175
|
templates.quotes.identifiers = '"';
|
|
176
|
+
// Dremio's ILIKE is a function whose optional third argument is the escape character, so
|
|
177
|
+
// `filters.like_escape_char` cannot be emitted as an ESCAPE clause. Mirror
|
|
178
|
+
// DremioFilter.likeIgnoreCase instead.
|
|
179
|
+
templates.tesseract.ilike = '{% if negated %}NOT {% endif %}ILIKE({{ expr }}, {{ pattern }}, \'\\\')';
|
|
168
180
|
return templates;
|
|
169
181
|
}
|
|
170
182
|
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@cubejs-backend/dremio-driver",
|
|
3
3
|
"description": "Cube.js Dremio driver",
|
|
4
4
|
"author": "Cube Dev, Inc.",
|
|
5
|
-
"version": "1.4.
|
|
5
|
+
"version": "1.4.4",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "https://github.com/cube-js/cube.git",
|
|
@@ -16,21 +16,21 @@
|
|
|
16
16
|
"tsc": "tsc",
|
|
17
17
|
"watch": "tsc -w",
|
|
18
18
|
"test": "yarn integration",
|
|
19
|
+
"unit": "jest --verbose dist/test/unit",
|
|
19
20
|
"integration": "npm run integration:dremio",
|
|
20
21
|
"integration:dremio": "jest --verbose dist/test",
|
|
21
22
|
"lint": "eslint driver/*.js",
|
|
22
23
|
"lint:fix": "eslint driver/*.js"
|
|
23
24
|
},
|
|
24
25
|
"dependencies": {
|
|
25
|
-
"@cubejs-backend/base-driver": "1.4.
|
|
26
|
-
"@cubejs-backend/schema-compiler": "1.4.
|
|
27
|
-
"@cubejs-backend/shared": "1.4.
|
|
28
|
-
"axios": "^1.8.3"
|
|
29
|
-
"sqlstring": "^2.3.1"
|
|
26
|
+
"@cubejs-backend/base-driver": "1.4.4",
|
|
27
|
+
"@cubejs-backend/schema-compiler": "1.4.4",
|
|
28
|
+
"@cubejs-backend/shared": "1.4.4",
|
|
29
|
+
"axios": "^1.8.3"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@cubejs-backend/linter": "1.4.
|
|
33
|
-
"@cubejs-backend/testing-shared": "1.4.
|
|
32
|
+
"@cubejs-backend/linter": "1.4.4",
|
|
33
|
+
"@cubejs-backend/testing-shared": "1.4.4",
|
|
34
34
|
"jest": "^29"
|
|
35
35
|
},
|
|
36
36
|
"license": "Apache-2.0",
|
|
@@ -40,5 +40,5 @@
|
|
|
40
40
|
"eslintConfig": {
|
|
41
41
|
"extends": "../cubejs-linter"
|
|
42
42
|
},
|
|
43
|
-
"gitHead": "
|
|
43
|
+
"gitHead": "1675e538f739774b1335e2e0baed8b29040b9c4d"
|
|
44
44
|
}
|
package/test/DremioQuery.test.ts
CHANGED
|
@@ -118,4 +118,49 @@ cube(\`sales\`, {
|
|
|
118
118
|
'("sales".sales_datetime >= TO_TIMESTAMP(?, \'YYYY-MM-DD"T"HH24:MI:SS.FFF\') AND "sales".sales_datetime <= TO_TIMESTAMP(?, \'YYYY-MM-DD"T"HH24:MI:SS.FFF\'))'
|
|
119
119
|
);
|
|
120
120
|
}));
|
|
121
|
+
|
|
122
|
+
const likeQuery = (operator: string, value: string) => new DremioQuery(
|
|
123
|
+
{ joinGraph, cubeEvaluator, compiler },
|
|
124
|
+
{
|
|
125
|
+
measures: ['sales.count'],
|
|
126
|
+
filters: [
|
|
127
|
+
{
|
|
128
|
+
member: 'sales.category',
|
|
129
|
+
operator,
|
|
130
|
+
values: [value],
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Dremio's ILIKE is a function: `NOT` cannot sit inside the argument list and the escape
|
|
137
|
+
// character is the third argument, without which the `\` BaseFilter.escapeWildcardChars binds
|
|
138
|
+
// would stay a literal backslash
|
|
139
|
+
it('should call ILIKE as a function with an escape character', () => compiler.compile().then(() => {
|
|
140
|
+
const [sql, params] = likeQuery('contains', 'demo').buildSqlAndParams();
|
|
141
|
+
|
|
142
|
+
expect(sql).toContain('ILIKE("sales".category, CONCAT(\'%\', ?, \'%\'), \'\\\')');
|
|
143
|
+
expect(params).toEqual(['demo']);
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
it('should negate the whole ILIKE call', () => compiler.compile().then(() => {
|
|
147
|
+
const [sql] = likeQuery('notContains', 'demo').buildSqlAndParams();
|
|
148
|
+
|
|
149
|
+
expect(sql).toContain('NOT ILIKE("sales".category, CONCAT(\'%\', ?, \'%\'), \'\\\')');
|
|
150
|
+
}));
|
|
151
|
+
|
|
152
|
+
it('should escape LIKE wildcards in filter parameters', () => compiler.compile().then(() => {
|
|
153
|
+
expect(likeQuery('contains', 'a_b%').buildSqlAndParams()[1]).toEqual(['a\\_b\\%']);
|
|
154
|
+
expect(likeQuery('startsWith', '100%').buildSqlAndParams()[1]).toEqual(['100\\%']);
|
|
155
|
+
expect(likeQuery('endsWith', 'c:\\users').buildSqlAndParams()[1]).toEqual(['c:\\\\users']);
|
|
156
|
+
}));
|
|
157
|
+
|
|
158
|
+
it('should pass the escape character to ILIKE for the native planner too', () => compiler.compile().then(() => {
|
|
159
|
+
const templates = likeQuery('contains', 'demo').sqlTemplates();
|
|
160
|
+
|
|
161
|
+
expect(templates.filters.like_escape_char).toEqual('\\');
|
|
162
|
+
expect(templates.tesseract.ilike).toEqual(
|
|
163
|
+
'{% if negated %}NOT {% endif %}ILIKE({{ expr }}, {{ pattern }}, \'\\\')'
|
|
164
|
+
);
|
|
165
|
+
}));
|
|
121
166
|
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* eslint-disable quotes */
|
|
2
|
+
const { applyParams } = require('../../../driver/DremioDriver');
|
|
3
|
+
|
|
4
|
+
// Dremio is a Calcite-based, standard-SQL engine: a quote inside a string
|
|
5
|
+
// literal is escaped by doubling it and a backslash is plain data.
|
|
6
|
+
describe('DremioDriver SQL parameter escaping', () => {
|
|
7
|
+
it('preserves LIKE escape sequences emitted by the schema compiler', () => {
|
|
8
|
+
const sql = applyParams(
|
|
9
|
+
`SELECT * FROM orders WHERE LOWER(name) LIKE '%' || LOWER(?) || '%' ESCAPE '\\'`,
|
|
10
|
+
['new\\_order\\%'],
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
expect(sql).toBe(
|
|
14
|
+
`SELECT * FROM orders WHERE LOWER(name) LIKE '%' || LOWER('new\\_order\\%') || '%' ESCAPE '\\'`
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('does not double literal backslashes in LIKE parameters', () => {
|
|
19
|
+
const sql = applyParams(
|
|
20
|
+
`SELECT * FROM orders WHERE LOWER(name) LIKE '%' || LOWER(?) || '%' ESCAPE '\\'`,
|
|
21
|
+
['folder\\\\name'],
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
expect(sql).toBe(
|
|
25
|
+
`SELECT * FROM orders WHERE LOWER(name) LIKE '%' || LOWER('folder\\\\name') || '%' ESCAPE '\\'`
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('doubles quotes so a value cannot break out of the literal', () => {
|
|
30
|
+
const sql = applyParams(
|
|
31
|
+
'SELECT * FROM orders WHERE name = ?',
|
|
32
|
+
[`o'reilly'); DROP TABLE orders; --`],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(sql).toBe(
|
|
36
|
+
`SELECT * FROM orders WHERE name = 'o''reilly''); DROP TABLE orders; --'`
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('keeps the literal closed for a backslash-then-quote payload', () => {
|
|
41
|
+
// In a backslash-escaping dialect `\'` would smuggle a quote through; on
|
|
42
|
+
// Dremio the backslash is data and only the quote needs doubling.
|
|
43
|
+
const sql = applyParams(
|
|
44
|
+
'SELECT * FROM orders WHERE name = ?',
|
|
45
|
+
[`foo\\' OR 1=1 --`],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(sql).toBe(`SELECT * FROM orders WHERE name = 'foo\\'' OR 1=1 --'`);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('keeps the literal closed for a value ending in a backslash', () => {
|
|
52
|
+
const sql = applyParams(
|
|
53
|
+
'SELECT * FROM orders WHERE name = ? AND status = ?',
|
|
54
|
+
['payload\\', 'new'],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
expect(sql).toBe(`SELECT * FROM orders WHERE name = 'payload\\' AND status = 'new'`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('keeps a literal percent sign in an equality parameter verbatim', () => {
|
|
61
|
+
const sql = applyParams(
|
|
62
|
+
'SELECT * FROM orders WHERE discount_label = ?',
|
|
63
|
+
['100% cotton'],
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(sql).toBe(`SELECT * FROM orders WHERE discount_label = '100% cotton'`);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('escapes every element of an array parameter', () => {
|
|
70
|
+
const sql = applyParams(
|
|
71
|
+
'SELECT * FROM orders WHERE status IN (?)',
|
|
72
|
+
[[`it's`, 'b']],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(sql).toBe(`SELECT * FROM orders WHERE status IN ('it''s', 'b')`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('substitutes multiple placeholders in order', () => {
|
|
79
|
+
const sql = applyParams(
|
|
80
|
+
`SELECT * FROM orders WHERE LOWER(name) LIKE '%' || LOWER(?) || '%' ESCAPE '\\' AND status = ? AND amount > ?`,
|
|
81
|
+
['pending\\_review', 'new', 100],
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(sql).toBe(
|
|
85
|
+
`SELECT * FROM orders WHERE LOWER(name) LIKE '%' || LOWER('pending\\_review') || '%' ESCAPE '\\' AND status = 'new' AND amount > 100`
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|