@malloydata/db-databricks 0.0.351
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/dist/databricks_connection.d.ts +37 -0
- package/dist/databricks_connection.js +268 -0
- package/dist/databricks_connection.js.map +1 -0
- package/dist/databricks_connection.spec.d.ts +1 -0
- package/dist/databricks_connection.spec.js +176 -0
- package/dist/databricks_connection.spec.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/package.json +28 -0
- package/src/databricks_connection.spec.ts +237 -0
- package/src/databricks_connection.ts +356 -0
- package/src/index.ts +91 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Connection, MalloyQueryData, PersistSQLResults, QueryRunStats, SQLSourceDef, TableSourceDef, SQLSourceRequest, QueryOptionsReader, RunSQLOptions } from '@malloydata/malloy';
|
|
2
|
+
import { BaseConnection } from '@malloydata/malloy/connection';
|
|
3
|
+
export interface DatabricksConfiguration {
|
|
4
|
+
host: string;
|
|
5
|
+
path: string;
|
|
6
|
+
token?: string;
|
|
7
|
+
oauthClientId?: string;
|
|
8
|
+
oauthClientSecret?: string;
|
|
9
|
+
defaultCatalog?: string;
|
|
10
|
+
defaultSchema?: string;
|
|
11
|
+
setupSQL?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class DatabricksConnection extends BaseConnection implements Connection, PersistSQLResults {
|
|
14
|
+
private readonly dialect;
|
|
15
|
+
private client?;
|
|
16
|
+
private session?;
|
|
17
|
+
private connectPromise?;
|
|
18
|
+
config: DatabricksConfiguration;
|
|
19
|
+
queryOptions: QueryOptionsReader | undefined;
|
|
20
|
+
name: string;
|
|
21
|
+
get dialectName(): string;
|
|
22
|
+
constructor(name: string, config: DatabricksConfiguration, queryOptions?: QueryOptionsReader);
|
|
23
|
+
private ensureConnected;
|
|
24
|
+
private doConnect;
|
|
25
|
+
private executeRaw;
|
|
26
|
+
manifestTemporaryTable(sqlCommand: string): Promise<string>;
|
|
27
|
+
test(): Promise<void>;
|
|
28
|
+
runSQL(sql: string, options?: RunSQLOptions): Promise<MalloyQueryData>;
|
|
29
|
+
getDigest(): string;
|
|
30
|
+
canPersist(): this is PersistSQLResults;
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
estimateQueryCost(_sqlCommand: string): Promise<QueryRunStats>;
|
|
33
|
+
fetchTableSchema(tableName: string, tablePath: string): Promise<TableSourceDef | string>;
|
|
34
|
+
fetchSelectSchema(sqlRef: SQLSourceRequest): Promise<SQLSourceDef | string>;
|
|
35
|
+
private fillStructDefFromDescribe;
|
|
36
|
+
runRawSQL(sql: string): Promise<MalloyQueryData>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Contributors to the Malloy project
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.DatabricksConnection = void 0;
|
|
8
|
+
const malloy_1 = require("@malloydata/malloy");
|
|
9
|
+
const connection_1 = require("@malloydata/malloy/connection");
|
|
10
|
+
const sql_1 = require("@databricks/sql");
|
|
11
|
+
// Suppress noisy SDK logging by default
|
|
12
|
+
const quietLogger = new sql_1.DBSQLLogger({ level: sql_1.LogLevel.error });
|
|
13
|
+
class DatabricksTypeParser extends malloy_1.TinyParser {
|
|
14
|
+
constructor(typeStr, dialect) {
|
|
15
|
+
super(typeStr, {
|
|
16
|
+
space: /^\s+/,
|
|
17
|
+
char: /^[<>:,()]/,
|
|
18
|
+
id: /^\w+/,
|
|
19
|
+
});
|
|
20
|
+
this.dialect = dialect;
|
|
21
|
+
}
|
|
22
|
+
typeDef() {
|
|
23
|
+
const typToken = this.next();
|
|
24
|
+
if (typToken.type === 'eof') {
|
|
25
|
+
throw this.parseError('Unexpected EOF parsing type');
|
|
26
|
+
}
|
|
27
|
+
const typText = typToken.text.toLowerCase();
|
|
28
|
+
if (typText === 'struct' && this.peek().text === '<') {
|
|
29
|
+
this.next('<');
|
|
30
|
+
const fields = [];
|
|
31
|
+
for (;;) {
|
|
32
|
+
const name = this.next('id');
|
|
33
|
+
this.next(':');
|
|
34
|
+
const fieldType = this.typeDef();
|
|
35
|
+
fields.push((0, malloy_1.mkFieldDef)(fieldType, name.text));
|
|
36
|
+
const sep = this.next();
|
|
37
|
+
if (sep.text === '>')
|
|
38
|
+
break;
|
|
39
|
+
if (sep.text === ',')
|
|
40
|
+
continue;
|
|
41
|
+
throw this.parseError(`Expected '>' or ',', got '${sep.text}'`);
|
|
42
|
+
}
|
|
43
|
+
return { type: 'record', fields };
|
|
44
|
+
}
|
|
45
|
+
if (typText === 'array' && this.peek().text === '<') {
|
|
46
|
+
this.next('<');
|
|
47
|
+
const elType = this.typeDef();
|
|
48
|
+
this.next('>');
|
|
49
|
+
return elType.type === 'record'
|
|
50
|
+
? {
|
|
51
|
+
type: 'array',
|
|
52
|
+
elementTypeDef: { type: 'record_element' },
|
|
53
|
+
fields: elType.fields,
|
|
54
|
+
}
|
|
55
|
+
: { type: 'array', elementTypeDef: elType };
|
|
56
|
+
}
|
|
57
|
+
if (typText === 'map' && this.peek().text === '<') {
|
|
58
|
+
this.next('<');
|
|
59
|
+
this.typeDef(); // key type
|
|
60
|
+
this.next(',');
|
|
61
|
+
this.typeDef(); // value type
|
|
62
|
+
this.next('>');
|
|
63
|
+
return { type: 'sql native' };
|
|
64
|
+
}
|
|
65
|
+
// Atomic type — parse parameters for DECIMAL, skip for others
|
|
66
|
+
if (typToken.type === 'id') {
|
|
67
|
+
if (typText === 'decimal' && this.peek().text === '(') {
|
|
68
|
+
this.next('(');
|
|
69
|
+
this.next('id'); // precision
|
|
70
|
+
let numberType = 'integer';
|
|
71
|
+
if (this.peek().text === ',') {
|
|
72
|
+
this.next(',');
|
|
73
|
+
const scale = this.next('id');
|
|
74
|
+
if (scale.text !== '0')
|
|
75
|
+
numberType = 'float';
|
|
76
|
+
}
|
|
77
|
+
this.next(')');
|
|
78
|
+
return { type: 'number', numberType };
|
|
79
|
+
}
|
|
80
|
+
if (this.peek().text === '(') {
|
|
81
|
+
this.next('(');
|
|
82
|
+
let depth = 1;
|
|
83
|
+
while (depth > 0) {
|
|
84
|
+
const t = this.next();
|
|
85
|
+
if (t.text === '(')
|
|
86
|
+
depth++;
|
|
87
|
+
else if (t.text === ')')
|
|
88
|
+
depth--;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return this.dialect.sqlTypeToMalloyType(typText);
|
|
92
|
+
}
|
|
93
|
+
throw this.parseError(`Unexpected '${typToken.text}' while parsing type`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
class DatabricksConnection extends connection_1.BaseConnection {
|
|
97
|
+
get dialectName() {
|
|
98
|
+
return this.dialect.name;
|
|
99
|
+
}
|
|
100
|
+
constructor(name, config, queryOptions) {
|
|
101
|
+
super();
|
|
102
|
+
this.dialect = new malloy_1.DatabricksDialect();
|
|
103
|
+
this.config = config;
|
|
104
|
+
this.queryOptions = queryOptions;
|
|
105
|
+
this.name = name;
|
|
106
|
+
}
|
|
107
|
+
async ensureConnected() {
|
|
108
|
+
if (this.session) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (this.connectPromise) {
|
|
112
|
+
return this.connectPromise;
|
|
113
|
+
}
|
|
114
|
+
this.connectPromise = this.doConnect();
|
|
115
|
+
return this.connectPromise;
|
|
116
|
+
}
|
|
117
|
+
async doConnect() {
|
|
118
|
+
this.client = new sql_1.DBSQLClient({ logger: quietLogger });
|
|
119
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
120
|
+
const connectOptions = {
|
|
121
|
+
host: this.config.host,
|
|
122
|
+
path: this.config.path,
|
|
123
|
+
};
|
|
124
|
+
if (this.config.oauthClientId && this.config.oauthClientSecret) {
|
|
125
|
+
connectOptions.authType = 'databricks-oauth';
|
|
126
|
+
connectOptions.oauthClientId = this.config.oauthClientId;
|
|
127
|
+
connectOptions.oauthClientSecret = this.config.oauthClientSecret;
|
|
128
|
+
}
|
|
129
|
+
else if (this.config.token) {
|
|
130
|
+
connectOptions.token = this.config.token;
|
|
131
|
+
}
|
|
132
|
+
await this.client.connect(connectOptions);
|
|
133
|
+
this.session = await this.client.openSession();
|
|
134
|
+
// Malloy timestamps are UTC wallclock
|
|
135
|
+
await this.executeRaw("SET TIME ZONE 'UTC'");
|
|
136
|
+
// Set catalog and schema if configured
|
|
137
|
+
if (this.config.defaultCatalog) {
|
|
138
|
+
await this.executeRaw(`USE CATALOG ${this.config.defaultCatalog}`);
|
|
139
|
+
}
|
|
140
|
+
if (this.config.defaultSchema) {
|
|
141
|
+
await this.executeRaw(`USE SCHEMA ${this.config.defaultSchema}`);
|
|
142
|
+
}
|
|
143
|
+
// Run user-provided setup SQL
|
|
144
|
+
if (this.config.setupSQL) {
|
|
145
|
+
for (const stmt of this.config.setupSQL.split(';\n')) {
|
|
146
|
+
const trimmed = stmt.trim();
|
|
147
|
+
if (trimmed) {
|
|
148
|
+
await this.executeRaw(trimmed);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
154
|
+
async executeRaw(sql) {
|
|
155
|
+
await this.ensureConnected();
|
|
156
|
+
const operation = await this.session.executeStatement(sql, {
|
|
157
|
+
runAsync: true,
|
|
158
|
+
});
|
|
159
|
+
const result = await operation.fetchAll();
|
|
160
|
+
await operation.close();
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
async manifestTemporaryTable(sqlCommand) {
|
|
164
|
+
const hash = (0, malloy_1.makeDigest)(sqlCommand);
|
|
165
|
+
const tableName = `tt${hash.slice(0, this.dialect.maxIdentifierLength - 2)}`;
|
|
166
|
+
const cmd = `CREATE TABLE IF NOT EXISTS ${tableName} AS (${sqlCommand})`;
|
|
167
|
+
await this.executeRaw(cmd);
|
|
168
|
+
return tableName;
|
|
169
|
+
}
|
|
170
|
+
async test() {
|
|
171
|
+
await this.runRawSQL('SELECT 1');
|
|
172
|
+
}
|
|
173
|
+
async runSQL(sql, options) {
|
|
174
|
+
const result = await this.runRawSQL(sql);
|
|
175
|
+
if ((options === null || options === void 0 ? void 0 : options.rowLimit) && result.rows.length > options.rowLimit) {
|
|
176
|
+
return {
|
|
177
|
+
rows: result.rows.slice(0, options.rowLimit),
|
|
178
|
+
totalRows: result.totalRows,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
return result;
|
|
182
|
+
}
|
|
183
|
+
getDigest() {
|
|
184
|
+
const { host, path, defaultCatalog, defaultSchema } = this.config;
|
|
185
|
+
return (0, malloy_1.makeDigest)('databricks', host, path, defaultCatalog, defaultSchema, this.config.setupSQL);
|
|
186
|
+
}
|
|
187
|
+
canPersist() {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
async close() {
|
|
191
|
+
if (this.session) {
|
|
192
|
+
await this.session.close();
|
|
193
|
+
this.session = undefined;
|
|
194
|
+
}
|
|
195
|
+
if (this.client) {
|
|
196
|
+
await this.client.close();
|
|
197
|
+
this.client = undefined;
|
|
198
|
+
}
|
|
199
|
+
this.connectPromise = undefined;
|
|
200
|
+
}
|
|
201
|
+
async estimateQueryCost(_sqlCommand) {
|
|
202
|
+
return {};
|
|
203
|
+
}
|
|
204
|
+
async fetchTableSchema(tableName, tablePath) {
|
|
205
|
+
const structDef = {
|
|
206
|
+
type: 'table',
|
|
207
|
+
name: tableName,
|
|
208
|
+
tablePath,
|
|
209
|
+
dialect: this.dialectName,
|
|
210
|
+
connection: this.name,
|
|
211
|
+
fields: [],
|
|
212
|
+
};
|
|
213
|
+
try {
|
|
214
|
+
const quotedPath = this.dialect.quoteTablePath(tablePath);
|
|
215
|
+
const result = await this.runRawSQL(`DESCRIBE TABLE ${quotedPath}`);
|
|
216
|
+
this.fillStructDefFromDescribe(result, structDef);
|
|
217
|
+
return structDef;
|
|
218
|
+
}
|
|
219
|
+
catch (e) {
|
|
220
|
+
return `Error fetching schema for ${tablePath}: ${e.message}`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async fetchSelectSchema(sqlRef) {
|
|
224
|
+
const structDef = {
|
|
225
|
+
type: 'sql_select',
|
|
226
|
+
...sqlRef,
|
|
227
|
+
dialect: this.dialectName,
|
|
228
|
+
fields: [],
|
|
229
|
+
name: (0, malloy_1.sqlKey)(sqlRef.connection, sqlRef.selectStr),
|
|
230
|
+
};
|
|
231
|
+
try {
|
|
232
|
+
// Use DESCRIBE on a subquery via a temporary view
|
|
233
|
+
const tempViewName = `_malloy_tmp_${(0, malloy_1.makeDigest)(sqlRef.selectStr).slice(0, 20)}`;
|
|
234
|
+
await this.executeRaw(`CREATE OR REPLACE TEMPORARY VIEW ${tempViewName} AS (${sqlRef.selectStr})`);
|
|
235
|
+
const result = await this.runRawSQL(`DESCRIBE TABLE ${tempViewName}`);
|
|
236
|
+
this.fillStructDefFromDescribe(result, structDef);
|
|
237
|
+
await this.executeRaw(`DROP VIEW IF EXISTS ${tempViewName}`);
|
|
238
|
+
return structDef;
|
|
239
|
+
}
|
|
240
|
+
catch (e) {
|
|
241
|
+
return `Error fetching schema for SQL block: ${e.message}`;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
fillStructDefFromDescribe(result, structDef) {
|
|
245
|
+
for (const row of result.rows) {
|
|
246
|
+
const colName = row['col_name'];
|
|
247
|
+
const dataType = row['data_type'];
|
|
248
|
+
// DESCRIBE TABLE includes partition info and blank separators; skip them
|
|
249
|
+
if (!colName || colName.startsWith('#') || colName.trim() === '') {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
const parser = new DatabricksTypeParser(dataType, this.dialect);
|
|
253
|
+
const malloyType = parser.typeDef();
|
|
254
|
+
structDef.fields.push((0, malloy_1.mkFieldDef)(malloyType, colName));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async runRawSQL(sql) {
|
|
258
|
+
try {
|
|
259
|
+
const rows = (await this.executeRaw(sql));
|
|
260
|
+
return { rows, totalRows: rows.length };
|
|
261
|
+
}
|
|
262
|
+
catch (e) {
|
|
263
|
+
throw new Error(`Databricks SQL error: ${e.message}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
exports.DatabricksConnection = DatabricksConnection;
|
|
268
|
+
//# sourceMappingURL=databricks_connection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"databricks_connection.js","sourceRoot":"","sources":["../src/databricks_connection.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAkBH,+CAM4B;AAC5B,8DAA6D;AAC7D,yCAAmE;AAEnE,wCAAwC;AACxC,MAAM,WAAW,GAAG,IAAI,iBAAW,CAAC,EAAC,KAAK,EAAE,cAAQ,CAAC,KAAK,EAAC,CAAC,CAAC;AAE7D,MAAM,oBAAqB,SAAQ,mBAAU;IAC3C,YACE,OAAe,EACE,OAA0B;QAE3C,KAAK,CAAC,OAAO,EAAE;YACb,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,WAAW;YACjB,EAAE,EAAE,MAAM;SACX,CAAC,CAAC;QANc,YAAO,GAAP,OAAO,CAAmB;IAO7C,CAAC;IAED,OAAO;QACL,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,QAAQ,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;YAC5B,MAAM,IAAI,CAAC,UAAU,CAAC,6BAA6B,CAAC,CAAC;QACvD,CAAC;QACD,MAAM,OAAO,GAAG,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QAE5C,IAAI,OAAO,KAAK,QAAQ,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YACrD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACf,MAAM,MAAM,GAAe,EAAE,CAAC;YAC9B,SAAS,CAAC;gBACR,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACf,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;gBACjC,MAAM,CAAC,IAAI,CAAC,IAAA,mBAAU,EAAC,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC9C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;gBACxB,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG;oBAAE,MAAM;gBAC5B,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG;oBAAE,SAAS;gBAC/B,MAAM,IAAI,CAAC,UAAU,CAAC,6BAA6B,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC;YAClE,CAAC;YACD,OAAO,EAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAkB,CAAC;QACnD,CAAC;QAED,IAAI,OAAO,KAAK,OAAO,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YACpD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACf,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;YAC9B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACf,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ;gBAC7B,CAAC,CAAC;oBACE,IAAI,EAAE,OAAO;oBACb,cAAc,EAAE,EAAC,IAAI,EAAE,gBAAgB,EAAC;oBACxC,MAAM,EAAE,MAAM,CAAC,MAAM;iBACtB;gBACH,CAAC,CAAC,EAAC,IAAI,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,EAAC,CAAC;QAC9C,CAAC;QAED,IAAI,OAAO,KAAK,KAAK,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;YAClD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACf,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,WAAW;YAC3B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACf,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,aAAa;YAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YACf,OAAO,EAAC,IAAI,EAAE,YAAY,EAAC,CAAC;QAC9B,CAAC;QAED,8DAA8D;QAC9D,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YAC3B,IAAI,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;gBACtD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACf,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY;gBAC7B,IAAI,UAAU,GAAwB,SAAS,CAAC;gBAChD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;oBAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBACf,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,GAAG;wBAAE,UAAU,GAAG,OAAO,CAAC;gBAC/C,CAAC;gBACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACf,OAAO,EAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAC,CAAC;YACtC,CAAC;YACD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,KAAK,GAAG,EAAE,CAAC;gBAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBACf,IAAI,KAAK,GAAG,CAAC,CAAC;gBACd,OAAO,KAAK,GAAG,CAAC,EAAE,CAAC;oBACjB,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;oBACtB,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG;wBAAE,KAAK,EAAE,CAAC;yBACvB,IAAI,CAAC,CAAC,IAAI,KAAK,GAAG;wBAAE,KAAK,EAAE,CAAC;gBACnC,CAAC;YACH,CAAC;YACD,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;QACnD,CAAC;QAED,MAAM,IAAI,CAAC,UAAU,CAAC,eAAe,QAAQ,CAAC,IAAI,sBAAsB,CAAC,CAAC;IAC5E,CAAC;CACF;AAaD,MAAa,oBACX,SAAQ,2BAAc;IAYtB,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED,YACE,IAAY,EACZ,MAA+B,EAC/B,YAAiC;QAEjC,KAAK,EAAE,CAAC;QAlBO,YAAO,GAAG,IAAI,0BAAiB,EAAE,CAAC;QAmBjD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAEO,KAAK,CAAC,eAAe;QAC3B,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,cAAc,CAAC;QAC7B,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;QACvC,OAAO,IAAI,CAAC,cAAc,CAAC;IAC7B,CAAC;IAEO,KAAK,CAAC,SAAS;QACrB,IAAI,CAAC,MAAM,GAAG,IAAI,iBAAW,CAAC,EAAC,MAAM,EAAE,WAAW,EAAC,CAAC,CAAC;QAErD,8DAA8D;QAC9D,MAAM,cAAc,GAAQ;YAC1B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;YACtB,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI;SACvB,CAAC;QAEF,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,CAAC;YAC/D,cAAc,CAAC,QAAQ,GAAG,kBAAkB,CAAC;YAC7C,cAAc,CAAC,aAAa,GAAG,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC;YACzD,cAAc,CAAC,iBAAiB,GAAG,IAAI,CAAC,MAAM,CAAC,iBAAiB,CAAC;QACnE,CAAC;aAAM,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAC7B,cAAc,CAAC,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QAC3C,CAAC;QAED,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;QAC1C,IAAI,CAAC,OAAO,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;QAE/C,sCAAsC;QACtC,MAAM,IAAI,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAC;QAE7C,uCAAuC;QACvC,IAAI,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC;YAC/B,MAAM,IAAI,CAAC,UAAU,CAAC,eAAe,IAAI,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,CAAC;QACrE,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;YAC9B,MAAM,IAAI,CAAC,UAAU,CAAC,cAAc,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;QACnE,CAAC;QAED,8BAA8B;QAC9B,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;YACzB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC;gBACrD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;gBAC5B,IAAI,OAAO,EAAE,CAAC;oBACZ,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;gBACjC,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IAED,8DAA8D;IACtD,KAAK,CAAC,UAAU,CAAC,GAAW;QAClC,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;QAC7B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,GAAG,EAAE;YACzD,QAAQ,EAAE,IAAI;SACf,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,QAAQ,EAAE,CAAC;QAC1C,MAAM,SAAS,CAAC,KAAK,EAAE,CAAC;QACxB,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,KAAK,CAAC,sBAAsB,CAAC,UAAkB;QAC7C,MAAM,IAAI,GAAG,IAAA,mBAAU,EAAC,UAAU,CAAC,CAAC;QACpC,MAAM,SAAS,GAAG,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,mBAAmB,GAAG,CAAC,CAAC,EAAE,CAAC;QAC7E,MAAM,GAAG,GAAG,8BAA8B,SAAS,QAAQ,UAAU,GAAG,CAAC;QACzE,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QAC3B,OAAO,SAAS,CAAC;IACnB,CAAC;IAEM,KAAK,CAAC,IAAI;QACf,MAAM,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IACnC,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,GAAW,EAAE,OAAuB;QAC/C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QACzC,IAAI,CAAA,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,QAAQ,KAAI,MAAM,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC/D,OAAO;gBACL,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC;gBAC5C,SAAS,EAAE,MAAM,CAAC,SAAS;aAC5B,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAEM,SAAS;QACd,MAAM,EAAC,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,aAAa,EAAC,GAAG,IAAI,CAAC,MAAM,CAAC;QAChE,OAAO,IAAA,mBAAU,EACf,YAAY,EACZ,IAAI,EACJ,IAAI,EACJ,cAAc,EACd,aAAa,EACb,IAAI,CAAC,MAAM,CAAC,QAAQ,CACrB,CAAC;IACJ,CAAC;IAED,UAAU;QACR,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,KAAK;QACT,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YAC3B,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;QAC3B,CAAC;QACD,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAC1B,IAAI,CAAC,MAAM,GAAG,SAAS,CAAC;QAC1B,CAAC;QACD,IAAI,CAAC,cAAc,GAAG,SAAS,CAAC;IAClC,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,WAAmB;QACzC,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,KAAK,CAAC,gBAAgB,CACpB,SAAiB,EACjB,SAAiB;QAEjB,MAAM,SAAS,GAAmB;YAChC,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,SAAS;YACf,SAAS;YACT,OAAO,EAAE,IAAI,CAAC,WAAW;YACzB,UAAU,EAAE,IAAI,CAAC,IAAI;YACrB,MAAM,EAAE,EAAE;SACX,CAAC;QAEF,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;YAC1D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,kBAAkB,UAAU,EAAE,CAAC,CAAC;YACpE,IAAI,CAAC,yBAAyB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YAClD,OAAO,SAAS,CAAC;QACnB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,6BAA6B,SAAS,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;QAChE,CAAC;IACH,CAAC;IAED,KAAK,CAAC,iBAAiB,CACrB,MAAwB;QAExB,MAAM,SAAS,GAAiB;YAC9B,IAAI,EAAE,YAAY;YAClB,GAAG,MAAM;YACT,OAAO,EAAE,IAAI,CAAC,WAAW;YACzB,MAAM,EAAE,EAAE;YACV,IAAI,EAAE,IAAA,eAAM,EAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC;SAClD,CAAC;QAEF,IAAI,CAAC;YACH,kDAAkD;YAClD,MAAM,YAAY,GAAG,eAAe,IAAA,mBAAU,EAAC,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;YAChF,MAAM,IAAI,CAAC,UAAU,CACnB,oCAAoC,YAAY,QAAQ,MAAM,CAAC,SAAS,GAAG,CAC5E,CAAC;YACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,kBAAkB,YAAY,EAAE,CAAC,CAAC;YACtE,IAAI,CAAC,yBAAyB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YAClD,MAAM,IAAI,CAAC,UAAU,CAAC,uBAAuB,YAAY,EAAE,CAAC,CAAC;YAC7D,OAAO,SAAS,CAAC;QACnB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,wCAAwC,CAAC,CAAC,OAAO,EAAE,CAAC;QAC7D,CAAC;IACH,CAAC;IAEO,yBAAyB,CAC/B,MAAuB,EACvB,SAAoB;QAEpB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAC9B,MAAM,OAAO,GAAG,GAAG,CAAC,UAAU,CAAW,CAAC;YAC1C,MAAM,QAAQ,GAAG,GAAG,CAAC,WAAW,CAAW,CAAC;YAE5C,yEAAyE;YACzE,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;gBACjE,SAAS;YACX,CAAC;YAED,MAAM,MAAM,GAAG,IAAI,oBAAoB,CAAC,QAAQ,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;YAChE,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;YACpC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,IAAA,mBAAU,EAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC;QACzD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,GAAW;QACzB,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAc,CAAC;YACvD,OAAO,EAAC,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,EAAC,CAAC;QACxC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;CACF;AA/ND,oDA+NC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@malloydata/malloy/test/matchers';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Contributors to the Malloy project
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
var _a, _b, _c, _d;
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
const _1 = require(".");
|
|
9
|
+
const test_1 = require("@malloydata/malloy/test");
|
|
10
|
+
require("@malloydata/malloy/test/matchers");
|
|
11
|
+
const host = (_a = process.env['DATABRICKS_HOST']) !== null && _a !== void 0 ? _a : '';
|
|
12
|
+
const token = (_b = process.env['DATABRICKS_TOKEN']) !== null && _b !== void 0 ? _b : '';
|
|
13
|
+
const warehouseId = (_c = process.env['DATABRICKS_WAREHOUSE_ID']) !== null && _c !== void 0 ? _c : '';
|
|
14
|
+
const defaultCatalog = (_d = process.env['DATABRICKS_CATALOG']) !== null && _d !== void 0 ? _d : '';
|
|
15
|
+
const path = process.env['DATABRICKS_PATH'] ||
|
|
16
|
+
(warehouseId ? `/sql/1.0/warehouses/${warehouseId}` : '');
|
|
17
|
+
const dbr_connection = new _1.DatabricksConnection('databricks', {
|
|
18
|
+
host,
|
|
19
|
+
path,
|
|
20
|
+
token,
|
|
21
|
+
defaultCatalog,
|
|
22
|
+
});
|
|
23
|
+
afterAll(async () => {
|
|
24
|
+
await dbr_connection.close();
|
|
25
|
+
});
|
|
26
|
+
async function schemaFor(selectStr) {
|
|
27
|
+
const res = await dbr_connection.fetchSchemaForSQLStruct({ selectStr, connection: 'databricks' }, {});
|
|
28
|
+
if ('error' in res && res.error) {
|
|
29
|
+
throw new Error(res.error);
|
|
30
|
+
}
|
|
31
|
+
return res.structDef.fields;
|
|
32
|
+
}
|
|
33
|
+
describe('basic connectivity', () => {
|
|
34
|
+
it('runs SELECT 1', async () => {
|
|
35
|
+
const res = await dbr_connection.runSQL('SELECT 1 as t');
|
|
36
|
+
expect(res.rows[0]['t']).toBe(1);
|
|
37
|
+
});
|
|
38
|
+
it('reads schema of a table', async () => {
|
|
39
|
+
const result = await dbr_connection.fetchTableSchema('alltypes', 'malloytest.alltypes');
|
|
40
|
+
expect(typeof result).not.toBe('string');
|
|
41
|
+
if (typeof result !== 'string') {
|
|
42
|
+
const fieldNames = result.fields.map(f => f.name);
|
|
43
|
+
expect(fieldNames).toContain('t_int64');
|
|
44
|
+
expect(fieldNames).toContain('string');
|
|
45
|
+
expect(fieldNames).toContain('t_date');
|
|
46
|
+
expect(fieldNames).toContain('t_timestamp');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it('reads schema of a SQL statement (fetchSchemaForSQLStruct)', async () => {
|
|
50
|
+
const fields = await schemaFor('SELECT * FROM malloytest.alltypes');
|
|
51
|
+
const fieldNames = fields.map(f => f.name);
|
|
52
|
+
expect(fieldNames).toContain('t_int64');
|
|
53
|
+
expect(fieldNames).toContain('string');
|
|
54
|
+
expect(fieldNames).toContain('t_date');
|
|
55
|
+
expect(fieldNames).toContain('t_timestamp');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('schema recognition — atomic types', () => {
|
|
59
|
+
it.each([
|
|
60
|
+
['INT', 'integer'],
|
|
61
|
+
['BIGINT', 'bigint'],
|
|
62
|
+
['DOUBLE', 'float'],
|
|
63
|
+
['FLOAT', 'float'],
|
|
64
|
+
['DECIMAL(10,2)', 'float'],
|
|
65
|
+
['DECIMAL(18,0)', 'integer'],
|
|
66
|
+
])('maps %s to number/%s', async (sqlType, numberType) => {
|
|
67
|
+
const fields = await schemaFor(`SELECT CAST(1 AS ${sqlType}) as v`);
|
|
68
|
+
expect(fields[0]).toEqual({
|
|
69
|
+
name: 'v',
|
|
70
|
+
type: 'number',
|
|
71
|
+
numberType,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
it.each(['STRING', 'VARCHAR(100)'])('maps %s to string', async (sqlType) => {
|
|
75
|
+
const fields = await schemaFor(`SELECT CAST('x' AS ${sqlType}) as v`);
|
|
76
|
+
expect(fields[0]).toEqual({ name: 'v', type: 'string' });
|
|
77
|
+
});
|
|
78
|
+
it('maps BOOLEAN to boolean', async () => {
|
|
79
|
+
const fields = await schemaFor('SELECT CAST(true AS BOOLEAN) as v');
|
|
80
|
+
expect(fields[0]).toEqual({ name: 'v', type: 'boolean' });
|
|
81
|
+
});
|
|
82
|
+
it('maps DATE to date', async () => {
|
|
83
|
+
const fields = await schemaFor('SELECT CAST(CURRENT_DATE() AS DATE) as v');
|
|
84
|
+
expect(fields[0]).toEqual({ name: 'v', type: 'date' });
|
|
85
|
+
});
|
|
86
|
+
it('maps TIMESTAMP to timestamp', async () => {
|
|
87
|
+
const fields = await schemaFor('SELECT CAST(CURRENT_TIMESTAMP() AS TIMESTAMP) as v');
|
|
88
|
+
expect(fields[0]).toEqual({ name: 'v', type: 'timestamp' });
|
|
89
|
+
});
|
|
90
|
+
it('maps TIMESTAMP_NTZ to sql native', async () => {
|
|
91
|
+
const fields = await schemaFor('SELECT CAST(CURRENT_TIMESTAMP() AS TIMESTAMP_NTZ) as v');
|
|
92
|
+
expect(fields[0]).toEqual({
|
|
93
|
+
name: 'v',
|
|
94
|
+
type: 'sql native',
|
|
95
|
+
rawType: 'timestamp_ntz',
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('schema recognition — complex types', () => {
|
|
100
|
+
it('parses ARRAY<INT>', async () => {
|
|
101
|
+
const fields = await schemaFor('SELECT ARRAY(1, 2, 3) as v');
|
|
102
|
+
expect(fields[0]).toMatchObject({
|
|
103
|
+
name: 'v',
|
|
104
|
+
type: 'array',
|
|
105
|
+
elementTypeDef: { type: 'number', numberType: 'integer' },
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
it('parses STRUCT', async () => {
|
|
109
|
+
const fields = await schemaFor("SELECT NAMED_STRUCT('a', 1.0D, 'b', 2, 'c', 'hello') as v");
|
|
110
|
+
expect(fields[0]).toEqual({
|
|
111
|
+
name: 'v',
|
|
112
|
+
type: 'record',
|
|
113
|
+
join: 'one',
|
|
114
|
+
fields: [
|
|
115
|
+
{ name: 'a', type: 'number', numberType: 'float' },
|
|
116
|
+
{ name: 'b', type: 'number', numberType: 'integer' },
|
|
117
|
+
{ name: 'c', type: 'string' },
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
it('parses ARRAY<STRUCT<...>> (repeated record)', async () => {
|
|
122
|
+
const fields = await schemaFor("SELECT ARRAY(NAMED_STRUCT('a', 1.0D, 'b', 2, 'c', 'hello')) as v");
|
|
123
|
+
expect(fields[0]).toEqual({
|
|
124
|
+
name: 'v',
|
|
125
|
+
type: 'array',
|
|
126
|
+
join: 'many',
|
|
127
|
+
elementTypeDef: { type: 'record_element' },
|
|
128
|
+
fields: [
|
|
129
|
+
{ name: 'a', type: 'number', numberType: 'float' },
|
|
130
|
+
{ name: 'b', type: 'number', numberType: 'integer' },
|
|
131
|
+
{ name: 'c', type: 'string' },
|
|
132
|
+
],
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
describe('data hydration — atomic types', () => {
|
|
137
|
+
const runtime = (0, test_1.createTestRuntime)(dbr_connection);
|
|
138
|
+
const testModel = (0, test_1.mkTestModel)(runtime, {});
|
|
139
|
+
it('reads integer', async () => {
|
|
140
|
+
await expect('run: databricks.sql("SELECT 42 as v")').toMatchResult(testModel, { v: 42 });
|
|
141
|
+
});
|
|
142
|
+
it('reads float', async () => {
|
|
143
|
+
await expect('run: databricks.sql("SELECT 3.14 as v")').toMatchResult(testModel, { v: 3.14 });
|
|
144
|
+
});
|
|
145
|
+
it('reads string', async () => {
|
|
146
|
+
await expect('run: databricks.sql("SELECT \'hello\' as v")').toMatchResult(testModel, { v: 'hello' });
|
|
147
|
+
});
|
|
148
|
+
it('reads boolean', async () => {
|
|
149
|
+
await expect('run: databricks.sql("SELECT true as v")').toMatchResult(testModel, { v: true });
|
|
150
|
+
});
|
|
151
|
+
it('reads date', async () => {
|
|
152
|
+
await expect('run: databricks.sql("SELECT DATE \'2024-01-15\' as v")').toMatchResult(testModel, { v: '2024-01-15T00:00:00.000Z' });
|
|
153
|
+
});
|
|
154
|
+
it('reads timestamp', async () => {
|
|
155
|
+
await expect('run: databricks.sql("SELECT TIMESTAMP \'2024-01-15 10:30:00\' as v")').toMatchResult(testModel, { v: '2024-01-15T10:30:00.000Z' });
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
describe('data hydration — complex types', () => {
|
|
159
|
+
const runtime = (0, test_1.createTestRuntime)(dbr_connection);
|
|
160
|
+
const testModel = (0, test_1.mkTestModel)(runtime, {});
|
|
161
|
+
it('reads array of integers', async () => {
|
|
162
|
+
await expect('run: databricks.sql("SELECT ARRAY(1, 2, 3) as v")').toMatchResult(testModel, { v: [1, 2, 3] });
|
|
163
|
+
});
|
|
164
|
+
it('reads struct', async () => {
|
|
165
|
+
await expect("run: databricks.sql(\"SELECT NAMED_STRUCT('a', 1.0D, 'b', 'hello') as v\")").toMatchResult(testModel, { v: { a: 1.0, b: 'hello' } });
|
|
166
|
+
});
|
|
167
|
+
it('reads array of structs', async () => {
|
|
168
|
+
await expect("run: databricks.sql(\"SELECT ARRAY(NAMED_STRUCT('x', 1, 'y', 'a'), NAMED_STRUCT('x', 2, 'y', 'b')) as v\")").toMatchResult(testModel, {
|
|
169
|
+
v: [
|
|
170
|
+
{ x: 1, y: 'a' },
|
|
171
|
+
{ x: 2, y: 'b' },
|
|
172
|
+
],
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
//# sourceMappingURL=databricks_connection.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"databricks_connection.spec.js","sourceRoot":"","sources":["../src/databricks_connection.spec.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DatabricksConnection } from './databricks_connection';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright Contributors to the Malloy project
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.DatabricksConnection = void 0;
|
|
8
|
+
var databricks_connection_1 = require("./databricks_connection");
|
|
9
|
+
Object.defineProperty(exports, "DatabricksConnection", { enumerable: true, get: function () { return databricks_connection_1.DatabricksConnection; } });
|
|
10
|
+
const malloy_1 = require("@malloydata/malloy");
|
|
11
|
+
const databricks_connection_2 = require("./databricks_connection");
|
|
12
|
+
(0, malloy_1.registerConnectionType)('databricks', {
|
|
13
|
+
displayName: 'Databricks',
|
|
14
|
+
factory: async (config) => {
|
|
15
|
+
const host = typeof config['host'] === 'string' ? config['host'] : '';
|
|
16
|
+
const path = typeof config['path'] === 'string' ? config['path'] : '';
|
|
17
|
+
return new databricks_connection_2.DatabricksConnection(config.name, {
|
|
18
|
+
host,
|
|
19
|
+
path,
|
|
20
|
+
token: typeof config['token'] === 'string' ? config['token'] : undefined,
|
|
21
|
+
oauthClientId: typeof config['oauthClientId'] === 'string'
|
|
22
|
+
? config['oauthClientId']
|
|
23
|
+
: undefined,
|
|
24
|
+
oauthClientSecret: typeof config['oauthClientSecret'] === 'string'
|
|
25
|
+
? config['oauthClientSecret']
|
|
26
|
+
: undefined,
|
|
27
|
+
defaultCatalog: typeof config['defaultCatalog'] === 'string'
|
|
28
|
+
? config['defaultCatalog']
|
|
29
|
+
: undefined,
|
|
30
|
+
defaultSchema: typeof config['defaultSchema'] === 'string'
|
|
31
|
+
? config['defaultSchema']
|
|
32
|
+
: undefined,
|
|
33
|
+
setupSQL: typeof config['setupSQL'] === 'string' ? config['setupSQL'] : undefined,
|
|
34
|
+
});
|
|
35
|
+
},
|
|
36
|
+
properties: [
|
|
37
|
+
{ name: 'host', displayName: 'Host', type: 'string' },
|
|
38
|
+
{
|
|
39
|
+
name: 'path',
|
|
40
|
+
displayName: 'HTTP Path',
|
|
41
|
+
type: 'string',
|
|
42
|
+
description: 'SQL warehouse HTTP path',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'token',
|
|
46
|
+
displayName: 'Access Token',
|
|
47
|
+
type: 'secret',
|
|
48
|
+
optional: true,
|
|
49
|
+
description: 'Personal access token',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'oauthClientId',
|
|
53
|
+
displayName: 'OAuth Client ID',
|
|
54
|
+
type: 'string',
|
|
55
|
+
optional: true,
|
|
56
|
+
description: 'OAuth M2M client ID',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
name: 'oauthClientSecret',
|
|
60
|
+
displayName: 'OAuth Client Secret',
|
|
61
|
+
type: 'secret',
|
|
62
|
+
optional: true,
|
|
63
|
+
description: 'OAuth M2M client secret',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'defaultCatalog',
|
|
67
|
+
displayName: 'Default Catalog',
|
|
68
|
+
type: 'string',
|
|
69
|
+
optional: true,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'defaultSchema',
|
|
73
|
+
displayName: 'Default Schema',
|
|
74
|
+
type: 'string',
|
|
75
|
+
optional: true,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'setupSQL',
|
|
79
|
+
displayName: 'Setup SQL',
|
|
80
|
+
type: 'text',
|
|
81
|
+
optional: true,
|
|
82
|
+
description: 'SQL statements to run when the connection is established',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@malloydata/db-databricks",
|
|
3
|
+
"version": "0.0.351",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/malloydata/malloy#readme",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/malloydata/malloy"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"lint": "eslint '**/*.ts{,x}'",
|
|
17
|
+
"lint-fix": "eslint '**/*.ts{,x}' --fix",
|
|
18
|
+
"test": "jest --config=../../jest.config.js",
|
|
19
|
+
"build": "tsc --build",
|
|
20
|
+
"clean": "tsc --build --clean && rm -f tsconfig.tsbuildinfo",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@malloydata/malloy": "0.0.351",
|
|
25
|
+
"@types/node": "^22.7.4",
|
|
26
|
+
"@databricks/sql": "^1.8.4"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Contributors to the Malloy project
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {DatabricksConnection} from '.';
|
|
7
|
+
import {createTestRuntime, mkTestModel} from '@malloydata/malloy/test';
|
|
8
|
+
import '@malloydata/malloy/test/matchers';
|
|
9
|
+
|
|
10
|
+
const host = process.env['DATABRICKS_HOST'] ?? '';
|
|
11
|
+
const token = process.env['DATABRICKS_TOKEN'] ?? '';
|
|
12
|
+
const warehouseId = process.env['DATABRICKS_WAREHOUSE_ID'] ?? '';
|
|
13
|
+
const defaultCatalog = process.env['DATABRICKS_CATALOG'] ?? '';
|
|
14
|
+
const path =
|
|
15
|
+
process.env['DATABRICKS_PATH'] ||
|
|
16
|
+
(warehouseId ? `/sql/1.0/warehouses/${warehouseId}` : '');
|
|
17
|
+
|
|
18
|
+
const dbr_connection = new DatabricksConnection('databricks', {
|
|
19
|
+
host,
|
|
20
|
+
path,
|
|
21
|
+
token,
|
|
22
|
+
defaultCatalog,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterAll(async () => {
|
|
26
|
+
await dbr_connection.close();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
async function schemaFor(selectStr: string) {
|
|
30
|
+
const res = await dbr_connection.fetchSchemaForSQLStruct(
|
|
31
|
+
{selectStr, connection: 'databricks'},
|
|
32
|
+
{}
|
|
33
|
+
);
|
|
34
|
+
if ('error' in res && res.error) {
|
|
35
|
+
throw new Error(res.error);
|
|
36
|
+
}
|
|
37
|
+
return res.structDef!.fields;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('basic connectivity', () => {
|
|
41
|
+
it('runs SELECT 1', async () => {
|
|
42
|
+
const res = await dbr_connection.runSQL('SELECT 1 as t');
|
|
43
|
+
expect(res.rows[0]['t']).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('reads schema of a table', async () => {
|
|
47
|
+
const result = await dbr_connection.fetchTableSchema(
|
|
48
|
+
'alltypes',
|
|
49
|
+
'malloytest.alltypes'
|
|
50
|
+
);
|
|
51
|
+
expect(typeof result).not.toBe('string');
|
|
52
|
+
if (typeof result !== 'string') {
|
|
53
|
+
const fieldNames = result.fields.map(f => f.name);
|
|
54
|
+
expect(fieldNames).toContain('t_int64');
|
|
55
|
+
expect(fieldNames).toContain('string');
|
|
56
|
+
expect(fieldNames).toContain('t_date');
|
|
57
|
+
expect(fieldNames).toContain('t_timestamp');
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('reads schema of a SQL statement (fetchSchemaForSQLStruct)', async () => {
|
|
62
|
+
const fields = await schemaFor('SELECT * FROM malloytest.alltypes');
|
|
63
|
+
const fieldNames = fields.map(f => f.name);
|
|
64
|
+
expect(fieldNames).toContain('t_int64');
|
|
65
|
+
expect(fieldNames).toContain('string');
|
|
66
|
+
expect(fieldNames).toContain('t_date');
|
|
67
|
+
expect(fieldNames).toContain('t_timestamp');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('schema recognition — atomic types', () => {
|
|
72
|
+
it.each([
|
|
73
|
+
['INT', 'integer'],
|
|
74
|
+
['BIGINT', 'bigint'],
|
|
75
|
+
['DOUBLE', 'float'],
|
|
76
|
+
['FLOAT', 'float'],
|
|
77
|
+
['DECIMAL(10,2)', 'float'],
|
|
78
|
+
['DECIMAL(18,0)', 'integer'],
|
|
79
|
+
])('maps %s to number/%s', async (sqlType, numberType) => {
|
|
80
|
+
const fields = await schemaFor(`SELECT CAST(1 AS ${sqlType}) as v`);
|
|
81
|
+
expect(fields[0]).toEqual({
|
|
82
|
+
name: 'v',
|
|
83
|
+
type: 'number',
|
|
84
|
+
numberType,
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it.each(['STRING', 'VARCHAR(100)'])('maps %s to string', async sqlType => {
|
|
89
|
+
const fields = await schemaFor(`SELECT CAST('x' AS ${sqlType}) as v`);
|
|
90
|
+
expect(fields[0]).toEqual({name: 'v', type: 'string'});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('maps BOOLEAN to boolean', async () => {
|
|
94
|
+
const fields = await schemaFor('SELECT CAST(true AS BOOLEAN) as v');
|
|
95
|
+
expect(fields[0]).toEqual({name: 'v', type: 'boolean'});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('maps DATE to date', async () => {
|
|
99
|
+
const fields = await schemaFor('SELECT CAST(CURRENT_DATE() AS DATE) as v');
|
|
100
|
+
expect(fields[0]).toEqual({name: 'v', type: 'date'});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('maps TIMESTAMP to timestamp', async () => {
|
|
104
|
+
const fields = await schemaFor(
|
|
105
|
+
'SELECT CAST(CURRENT_TIMESTAMP() AS TIMESTAMP) as v'
|
|
106
|
+
);
|
|
107
|
+
expect(fields[0]).toEqual({name: 'v', type: 'timestamp'});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('maps TIMESTAMP_NTZ to sql native', async () => {
|
|
111
|
+
const fields = await schemaFor(
|
|
112
|
+
'SELECT CAST(CURRENT_TIMESTAMP() AS TIMESTAMP_NTZ) as v'
|
|
113
|
+
);
|
|
114
|
+
expect(fields[0]).toEqual({
|
|
115
|
+
name: 'v',
|
|
116
|
+
type: 'sql native',
|
|
117
|
+
rawType: 'timestamp_ntz',
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('schema recognition — complex types', () => {
|
|
123
|
+
it('parses ARRAY<INT>', async () => {
|
|
124
|
+
const fields = await schemaFor('SELECT ARRAY(1, 2, 3) as v');
|
|
125
|
+
expect(fields[0]).toMatchObject({
|
|
126
|
+
name: 'v',
|
|
127
|
+
type: 'array',
|
|
128
|
+
elementTypeDef: {type: 'number', numberType: 'integer'},
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('parses STRUCT', async () => {
|
|
133
|
+
const fields = await schemaFor(
|
|
134
|
+
"SELECT NAMED_STRUCT('a', 1.0D, 'b', 2, 'c', 'hello') as v"
|
|
135
|
+
);
|
|
136
|
+
expect(fields[0]).toEqual({
|
|
137
|
+
name: 'v',
|
|
138
|
+
type: 'record',
|
|
139
|
+
join: 'one',
|
|
140
|
+
fields: [
|
|
141
|
+
{name: 'a', type: 'number', numberType: 'float'},
|
|
142
|
+
{name: 'b', type: 'number', numberType: 'integer'},
|
|
143
|
+
{name: 'c', type: 'string'},
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('parses ARRAY<STRUCT<...>> (repeated record)', async () => {
|
|
149
|
+
const fields = await schemaFor(
|
|
150
|
+
"SELECT ARRAY(NAMED_STRUCT('a', 1.0D, 'b', 2, 'c', 'hello')) as v"
|
|
151
|
+
);
|
|
152
|
+
expect(fields[0]).toEqual({
|
|
153
|
+
name: 'v',
|
|
154
|
+
type: 'array',
|
|
155
|
+
join: 'many',
|
|
156
|
+
elementTypeDef: {type: 'record_element'},
|
|
157
|
+
fields: [
|
|
158
|
+
{name: 'a', type: 'number', numberType: 'float'},
|
|
159
|
+
{name: 'b', type: 'number', numberType: 'integer'},
|
|
160
|
+
{name: 'c', type: 'string'},
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
describe('data hydration — atomic types', () => {
|
|
167
|
+
const runtime = createTestRuntime(dbr_connection);
|
|
168
|
+
const testModel = mkTestModel(runtime, {});
|
|
169
|
+
|
|
170
|
+
it('reads integer', async () => {
|
|
171
|
+
await expect('run: databricks.sql("SELECT 42 as v")').toMatchResult(
|
|
172
|
+
testModel,
|
|
173
|
+
{v: 42}
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('reads float', async () => {
|
|
178
|
+
await expect('run: databricks.sql("SELECT 3.14 as v")').toMatchResult(
|
|
179
|
+
testModel,
|
|
180
|
+
{v: 3.14}
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('reads string', async () => {
|
|
185
|
+
await expect('run: databricks.sql("SELECT \'hello\' as v")').toMatchResult(
|
|
186
|
+
testModel,
|
|
187
|
+
{v: 'hello'}
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('reads boolean', async () => {
|
|
192
|
+
await expect('run: databricks.sql("SELECT true as v")').toMatchResult(
|
|
193
|
+
testModel,
|
|
194
|
+
{v: true}
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('reads date', async () => {
|
|
199
|
+
await expect(
|
|
200
|
+
'run: databricks.sql("SELECT DATE \'2024-01-15\' as v")'
|
|
201
|
+
).toMatchResult(testModel, {v: '2024-01-15T00:00:00.000Z'});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('reads timestamp', async () => {
|
|
205
|
+
await expect(
|
|
206
|
+
'run: databricks.sql("SELECT TIMESTAMP \'2024-01-15 10:30:00\' as v")'
|
|
207
|
+
).toMatchResult(testModel, {v: '2024-01-15T10:30:00.000Z'});
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('data hydration — complex types', () => {
|
|
212
|
+
const runtime = createTestRuntime(dbr_connection);
|
|
213
|
+
const testModel = mkTestModel(runtime, {});
|
|
214
|
+
|
|
215
|
+
it('reads array of integers', async () => {
|
|
216
|
+
await expect(
|
|
217
|
+
'run: databricks.sql("SELECT ARRAY(1, 2, 3) as v")'
|
|
218
|
+
).toMatchResult(testModel, {v: [1, 2, 3]});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('reads struct', async () => {
|
|
222
|
+
await expect(
|
|
223
|
+
"run: databricks.sql(\"SELECT NAMED_STRUCT('a', 1.0D, 'b', 'hello') as v\")"
|
|
224
|
+
).toMatchResult(testModel, {v: {a: 1.0, b: 'hello'}});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('reads array of structs', async () => {
|
|
228
|
+
await expect(
|
|
229
|
+
"run: databricks.sql(\"SELECT ARRAY(NAMED_STRUCT('x', 1, 'y', 'a'), NAMED_STRUCT('x', 2, 'y', 'b')) as v\")"
|
|
230
|
+
).toMatchResult(testModel, {
|
|
231
|
+
v: [
|
|
232
|
+
{x: 1, y: 'a'},
|
|
233
|
+
{x: 2, y: 'b'},
|
|
234
|
+
],
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
});
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Contributors to the Malloy project
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
Connection,
|
|
8
|
+
MalloyQueryData,
|
|
9
|
+
PersistSQLResults,
|
|
10
|
+
QueryRunStats,
|
|
11
|
+
QueryData,
|
|
12
|
+
SQLSourceDef,
|
|
13
|
+
TableSourceDef,
|
|
14
|
+
SQLSourceRequest,
|
|
15
|
+
QueryOptionsReader,
|
|
16
|
+
RunSQLOptions,
|
|
17
|
+
StructDef,
|
|
18
|
+
AtomicTypeDef,
|
|
19
|
+
FieldDef,
|
|
20
|
+
RecordTypeDef,
|
|
21
|
+
} from '@malloydata/malloy';
|
|
22
|
+
import {
|
|
23
|
+
DatabricksDialect,
|
|
24
|
+
sqlKey,
|
|
25
|
+
makeDigest,
|
|
26
|
+
TinyParser,
|
|
27
|
+
mkFieldDef,
|
|
28
|
+
} from '@malloydata/malloy';
|
|
29
|
+
import {BaseConnection} from '@malloydata/malloy/connection';
|
|
30
|
+
import {DBSQLClient, DBSQLLogger, LogLevel} from '@databricks/sql';
|
|
31
|
+
|
|
32
|
+
// Suppress noisy SDK logging by default
|
|
33
|
+
const quietLogger = new DBSQLLogger({level: LogLevel.error});
|
|
34
|
+
|
|
35
|
+
class DatabricksTypeParser extends TinyParser {
|
|
36
|
+
constructor(
|
|
37
|
+
typeStr: string,
|
|
38
|
+
private readonly dialect: DatabricksDialect
|
|
39
|
+
) {
|
|
40
|
+
super(typeStr, {
|
|
41
|
+
space: /^\s+/,
|
|
42
|
+
char: /^[<>:,()]/,
|
|
43
|
+
id: /^\w+/,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
typeDef(): AtomicTypeDef {
|
|
48
|
+
const typToken = this.next();
|
|
49
|
+
if (typToken.type === 'eof') {
|
|
50
|
+
throw this.parseError('Unexpected EOF parsing type');
|
|
51
|
+
}
|
|
52
|
+
const typText = typToken.text.toLowerCase();
|
|
53
|
+
|
|
54
|
+
if (typText === 'struct' && this.peek().text === '<') {
|
|
55
|
+
this.next('<');
|
|
56
|
+
const fields: FieldDef[] = [];
|
|
57
|
+
for (;;) {
|
|
58
|
+
const name = this.next('id');
|
|
59
|
+
this.next(':');
|
|
60
|
+
const fieldType = this.typeDef();
|
|
61
|
+
fields.push(mkFieldDef(fieldType, name.text));
|
|
62
|
+
const sep = this.next();
|
|
63
|
+
if (sep.text === '>') break;
|
|
64
|
+
if (sep.text === ',') continue;
|
|
65
|
+
throw this.parseError(`Expected '>' or ',', got '${sep.text}'`);
|
|
66
|
+
}
|
|
67
|
+
return {type: 'record', fields} as RecordTypeDef;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typText === 'array' && this.peek().text === '<') {
|
|
71
|
+
this.next('<');
|
|
72
|
+
const elType = this.typeDef();
|
|
73
|
+
this.next('>');
|
|
74
|
+
return elType.type === 'record'
|
|
75
|
+
? {
|
|
76
|
+
type: 'array',
|
|
77
|
+
elementTypeDef: {type: 'record_element'},
|
|
78
|
+
fields: elType.fields,
|
|
79
|
+
}
|
|
80
|
+
: {type: 'array', elementTypeDef: elType};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typText === 'map' && this.peek().text === '<') {
|
|
84
|
+
this.next('<');
|
|
85
|
+
this.typeDef(); // key type
|
|
86
|
+
this.next(',');
|
|
87
|
+
this.typeDef(); // value type
|
|
88
|
+
this.next('>');
|
|
89
|
+
return {type: 'sql native'};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Atomic type — parse parameters for DECIMAL, skip for others
|
|
93
|
+
if (typToken.type === 'id') {
|
|
94
|
+
if (typText === 'decimal' && this.peek().text === '(') {
|
|
95
|
+
this.next('(');
|
|
96
|
+
this.next('id'); // precision
|
|
97
|
+
let numberType: 'integer' | 'float' = 'integer';
|
|
98
|
+
if (this.peek().text === ',') {
|
|
99
|
+
this.next(',');
|
|
100
|
+
const scale = this.next('id');
|
|
101
|
+
if (scale.text !== '0') numberType = 'float';
|
|
102
|
+
}
|
|
103
|
+
this.next(')');
|
|
104
|
+
return {type: 'number', numberType};
|
|
105
|
+
}
|
|
106
|
+
if (this.peek().text === '(') {
|
|
107
|
+
this.next('(');
|
|
108
|
+
let depth = 1;
|
|
109
|
+
while (depth > 0) {
|
|
110
|
+
const t = this.next();
|
|
111
|
+
if (t.text === '(') depth++;
|
|
112
|
+
else if (t.text === ')') depth--;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return this.dialect.sqlTypeToMalloyType(typText);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
throw this.parseError(`Unexpected '${typToken.text}' while parsing type`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface DatabricksConfiguration {
|
|
123
|
+
host: string;
|
|
124
|
+
path: string;
|
|
125
|
+
token?: string;
|
|
126
|
+
oauthClientId?: string;
|
|
127
|
+
oauthClientSecret?: string;
|
|
128
|
+
defaultCatalog?: string;
|
|
129
|
+
defaultSchema?: string;
|
|
130
|
+
setupSQL?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export class DatabricksConnection
|
|
134
|
+
extends BaseConnection
|
|
135
|
+
implements Connection, PersistSQLResults
|
|
136
|
+
{
|
|
137
|
+
private readonly dialect = new DatabricksDialect();
|
|
138
|
+
private client?: DBSQLClient;
|
|
139
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
140
|
+
private session?: any;
|
|
141
|
+
private connectPromise?: Promise<void>;
|
|
142
|
+
config: DatabricksConfiguration;
|
|
143
|
+
queryOptions: QueryOptionsReader | undefined;
|
|
144
|
+
public name: string;
|
|
145
|
+
|
|
146
|
+
get dialectName(): string {
|
|
147
|
+
return this.dialect.name;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
constructor(
|
|
151
|
+
name: string,
|
|
152
|
+
config: DatabricksConfiguration,
|
|
153
|
+
queryOptions?: QueryOptionsReader
|
|
154
|
+
) {
|
|
155
|
+
super();
|
|
156
|
+
this.config = config;
|
|
157
|
+
this.queryOptions = queryOptions;
|
|
158
|
+
this.name = name;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async ensureConnected(): Promise<void> {
|
|
162
|
+
if (this.session) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (this.connectPromise) {
|
|
166
|
+
return this.connectPromise;
|
|
167
|
+
}
|
|
168
|
+
this.connectPromise = this.doConnect();
|
|
169
|
+
return this.connectPromise;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private async doConnect(): Promise<void> {
|
|
173
|
+
this.client = new DBSQLClient({logger: quietLogger});
|
|
174
|
+
|
|
175
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
176
|
+
const connectOptions: any = {
|
|
177
|
+
host: this.config.host,
|
|
178
|
+
path: this.config.path,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (this.config.oauthClientId && this.config.oauthClientSecret) {
|
|
182
|
+
connectOptions.authType = 'databricks-oauth';
|
|
183
|
+
connectOptions.oauthClientId = this.config.oauthClientId;
|
|
184
|
+
connectOptions.oauthClientSecret = this.config.oauthClientSecret;
|
|
185
|
+
} else if (this.config.token) {
|
|
186
|
+
connectOptions.token = this.config.token;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
await this.client.connect(connectOptions);
|
|
190
|
+
this.session = await this.client.openSession();
|
|
191
|
+
|
|
192
|
+
// Malloy timestamps are UTC wallclock
|
|
193
|
+
await this.executeRaw("SET TIME ZONE 'UTC'");
|
|
194
|
+
|
|
195
|
+
// Set catalog and schema if configured
|
|
196
|
+
if (this.config.defaultCatalog) {
|
|
197
|
+
await this.executeRaw(`USE CATALOG ${this.config.defaultCatalog}`);
|
|
198
|
+
}
|
|
199
|
+
if (this.config.defaultSchema) {
|
|
200
|
+
await this.executeRaw(`USE SCHEMA ${this.config.defaultSchema}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Run user-provided setup SQL
|
|
204
|
+
if (this.config.setupSQL) {
|
|
205
|
+
for (const stmt of this.config.setupSQL.split(';\n')) {
|
|
206
|
+
const trimmed = stmt.trim();
|
|
207
|
+
if (trimmed) {
|
|
208
|
+
await this.executeRaw(trimmed);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
215
|
+
private async executeRaw(sql: string): Promise<any[]> {
|
|
216
|
+
await this.ensureConnected();
|
|
217
|
+
const operation = await this.session.executeStatement(sql, {
|
|
218
|
+
runAsync: true,
|
|
219
|
+
});
|
|
220
|
+
const result = await operation.fetchAll();
|
|
221
|
+
await operation.close();
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async manifestTemporaryTable(sqlCommand: string): Promise<string> {
|
|
226
|
+
const hash = makeDigest(sqlCommand);
|
|
227
|
+
const tableName = `tt${hash.slice(0, this.dialect.maxIdentifierLength - 2)}`;
|
|
228
|
+
const cmd = `CREATE TABLE IF NOT EXISTS ${tableName} AS (${sqlCommand})`;
|
|
229
|
+
await this.executeRaw(cmd);
|
|
230
|
+
return tableName;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public async test(): Promise<void> {
|
|
234
|
+
await this.runRawSQL('SELECT 1');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async runSQL(sql: string, options?: RunSQLOptions): Promise<MalloyQueryData> {
|
|
238
|
+
const result = await this.runRawSQL(sql);
|
|
239
|
+
if (options?.rowLimit && result.rows.length > options.rowLimit) {
|
|
240
|
+
return {
|
|
241
|
+
rows: result.rows.slice(0, options.rowLimit),
|
|
242
|
+
totalRows: result.totalRows,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public getDigest(): string {
|
|
249
|
+
const {host, path, defaultCatalog, defaultSchema} = this.config;
|
|
250
|
+
return makeDigest(
|
|
251
|
+
'databricks',
|
|
252
|
+
host,
|
|
253
|
+
path,
|
|
254
|
+
defaultCatalog,
|
|
255
|
+
defaultSchema,
|
|
256
|
+
this.config.setupSQL
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
canPersist(): this is PersistSQLResults {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async close(): Promise<void> {
|
|
265
|
+
if (this.session) {
|
|
266
|
+
await this.session.close();
|
|
267
|
+
this.session = undefined;
|
|
268
|
+
}
|
|
269
|
+
if (this.client) {
|
|
270
|
+
await this.client.close();
|
|
271
|
+
this.client = undefined;
|
|
272
|
+
}
|
|
273
|
+
this.connectPromise = undefined;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async estimateQueryCost(_sqlCommand: string): Promise<QueryRunStats> {
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async fetchTableSchema(
|
|
281
|
+
tableName: string,
|
|
282
|
+
tablePath: string
|
|
283
|
+
): Promise<TableSourceDef | string> {
|
|
284
|
+
const structDef: TableSourceDef = {
|
|
285
|
+
type: 'table',
|
|
286
|
+
name: tableName,
|
|
287
|
+
tablePath,
|
|
288
|
+
dialect: this.dialectName,
|
|
289
|
+
connection: this.name,
|
|
290
|
+
fields: [],
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const quotedPath = this.dialect.quoteTablePath(tablePath);
|
|
295
|
+
const result = await this.runRawSQL(`DESCRIBE TABLE ${quotedPath}`);
|
|
296
|
+
this.fillStructDefFromDescribe(result, structDef);
|
|
297
|
+
return structDef;
|
|
298
|
+
} catch (e) {
|
|
299
|
+
return `Error fetching schema for ${tablePath}: ${e.message}`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async fetchSelectSchema(
|
|
304
|
+
sqlRef: SQLSourceRequest
|
|
305
|
+
): Promise<SQLSourceDef | string> {
|
|
306
|
+
const structDef: SQLSourceDef = {
|
|
307
|
+
type: 'sql_select',
|
|
308
|
+
...sqlRef,
|
|
309
|
+
dialect: this.dialectName,
|
|
310
|
+
fields: [],
|
|
311
|
+
name: sqlKey(sqlRef.connection, sqlRef.selectStr),
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
// Use DESCRIBE on a subquery via a temporary view
|
|
316
|
+
const tempViewName = `_malloy_tmp_${makeDigest(sqlRef.selectStr).slice(0, 20)}`;
|
|
317
|
+
await this.executeRaw(
|
|
318
|
+
`CREATE OR REPLACE TEMPORARY VIEW ${tempViewName} AS (${sqlRef.selectStr})`
|
|
319
|
+
);
|
|
320
|
+
const result = await this.runRawSQL(`DESCRIBE TABLE ${tempViewName}`);
|
|
321
|
+
this.fillStructDefFromDescribe(result, structDef);
|
|
322
|
+
await this.executeRaw(`DROP VIEW IF EXISTS ${tempViewName}`);
|
|
323
|
+
return structDef;
|
|
324
|
+
} catch (e) {
|
|
325
|
+
return `Error fetching schema for SQL block: ${e.message}`;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private fillStructDefFromDescribe(
|
|
330
|
+
result: MalloyQueryData,
|
|
331
|
+
structDef: StructDef
|
|
332
|
+
): void {
|
|
333
|
+
for (const row of result.rows) {
|
|
334
|
+
const colName = row['col_name'] as string;
|
|
335
|
+
const dataType = row['data_type'] as string;
|
|
336
|
+
|
|
337
|
+
// DESCRIBE TABLE includes partition info and blank separators; skip them
|
|
338
|
+
if (!colName || colName.startsWith('#') || colName.trim() === '') {
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const parser = new DatabricksTypeParser(dataType, this.dialect);
|
|
343
|
+
const malloyType = parser.typeDef();
|
|
344
|
+
structDef.fields.push(mkFieldDef(malloyType, colName));
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async runRawSQL(sql: string): Promise<MalloyQueryData> {
|
|
349
|
+
try {
|
|
350
|
+
const rows = (await this.executeRaw(sql)) as QueryData;
|
|
351
|
+
return {rows, totalRows: rows.length};
|
|
352
|
+
} catch (e) {
|
|
353
|
+
throw new Error(`Databricks SQL error: ${e.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright Contributors to the Malloy project
|
|
3
|
+
* SPDX-License-Identifier: MIT
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {DatabricksConnection} from './databricks_connection';
|
|
7
|
+
|
|
8
|
+
import {registerConnectionType} from '@malloydata/malloy';
|
|
9
|
+
import type {ConnectionConfig} from '@malloydata/malloy';
|
|
10
|
+
import {DatabricksConnection} from './databricks_connection';
|
|
11
|
+
|
|
12
|
+
registerConnectionType('databricks', {
|
|
13
|
+
displayName: 'Databricks',
|
|
14
|
+
factory: async (config: ConnectionConfig) => {
|
|
15
|
+
const host = typeof config['host'] === 'string' ? config['host'] : '';
|
|
16
|
+
const path = typeof config['path'] === 'string' ? config['path'] : '';
|
|
17
|
+
|
|
18
|
+
return new DatabricksConnection(config.name, {
|
|
19
|
+
host,
|
|
20
|
+
path,
|
|
21
|
+
token: typeof config['token'] === 'string' ? config['token'] : undefined,
|
|
22
|
+
oauthClientId:
|
|
23
|
+
typeof config['oauthClientId'] === 'string'
|
|
24
|
+
? config['oauthClientId']
|
|
25
|
+
: undefined,
|
|
26
|
+
oauthClientSecret:
|
|
27
|
+
typeof config['oauthClientSecret'] === 'string'
|
|
28
|
+
? config['oauthClientSecret']
|
|
29
|
+
: undefined,
|
|
30
|
+
defaultCatalog:
|
|
31
|
+
typeof config['defaultCatalog'] === 'string'
|
|
32
|
+
? config['defaultCatalog']
|
|
33
|
+
: undefined,
|
|
34
|
+
defaultSchema:
|
|
35
|
+
typeof config['defaultSchema'] === 'string'
|
|
36
|
+
? config['defaultSchema']
|
|
37
|
+
: undefined,
|
|
38
|
+
setupSQL:
|
|
39
|
+
typeof config['setupSQL'] === 'string' ? config['setupSQL'] : undefined,
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
properties: [
|
|
43
|
+
{name: 'host', displayName: 'Host', type: 'string'},
|
|
44
|
+
{
|
|
45
|
+
name: 'path',
|
|
46
|
+
displayName: 'HTTP Path',
|
|
47
|
+
type: 'string',
|
|
48
|
+
description: 'SQL warehouse HTTP path',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'token',
|
|
52
|
+
displayName: 'Access Token',
|
|
53
|
+
type: 'secret',
|
|
54
|
+
optional: true,
|
|
55
|
+
description: 'Personal access token',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'oauthClientId',
|
|
59
|
+
displayName: 'OAuth Client ID',
|
|
60
|
+
type: 'string',
|
|
61
|
+
optional: true,
|
|
62
|
+
description: 'OAuth M2M client ID',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'oauthClientSecret',
|
|
66
|
+
displayName: 'OAuth Client Secret',
|
|
67
|
+
type: 'secret',
|
|
68
|
+
optional: true,
|
|
69
|
+
description: 'OAuth M2M client secret',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'defaultCatalog',
|
|
73
|
+
displayName: 'Default Catalog',
|
|
74
|
+
type: 'string',
|
|
75
|
+
optional: true,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'defaultSchema',
|
|
79
|
+
displayName: 'Default Schema',
|
|
80
|
+
type: 'string',
|
|
81
|
+
optional: true,
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'setupSQL',
|
|
85
|
+
displayName: 'Setup SQL',
|
|
86
|
+
type: 'text',
|
|
87
|
+
optional: true,
|
|
88
|
+
description: 'SQL statements to run when the connection is established',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
});
|