@malloydata/db-duckdb 0.0.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.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # Malloy
2
+
3
+ Malloy is an experimental language for describing data relationships and transformations. It is both a semantic modeling language and a querying language that runs queries against a relational database. Malloy currently connects to BigQuery and Postgres, and natively supports DuckDB. We've built a Visual Studio Code extension to facilitate building Malloy data models, querying and transforming data, and creating simple visualizations and dashboards.
4
+
5
+ ## This package
6
+
7
+ This package facilitates using the `malloydata/malloy` library with DuckDB - see [here](https://github.com/looker-open-source/malloy/blob/main/packages/malloy/README.md) for additional information.
@@ -0,0 +1,60 @@
1
+ /// <reference path="../src/duckdb.d.ts" />
2
+ import { Connection, MalloyQueryData, PersistSQLResults, PooledConnection, SQLBlock, StructDef, QueryDataRow } from "@malloydata/malloy";
3
+ import { FetchSchemaAndRunSimultaneously, FetchSchemaAndRunStreamSimultaneously, StreamingConnection } from "@malloydata/malloy/src/runtime_types";
4
+ import { Database, Row } from "duckdb";
5
+ import { RunSQLOptions } from "@malloydata/malloy/src/malloy";
6
+ export declare class DuckDBConnection implements Connection, PersistSQLResults, StreamingConnection {
7
+ readonly name: string;
8
+ private workingDirectory;
9
+ protected connection: import("duckdb").Connection;
10
+ protected database: Database;
11
+ protected isSetup: boolean;
12
+ constructor(name: string, databasePath?: string, workingDirectory?: string);
13
+ get dialectName(): string;
14
+ isPool(): this is PooledConnection;
15
+ canPersist(): this is PersistSQLResults;
16
+ protected setup(): Promise<void>;
17
+ protected runDuckDBQuery(sql: string): Promise<{
18
+ rows: Row[];
19
+ totalRows: number;
20
+ }>;
21
+ runRawSQL(sql: string): Promise<{
22
+ rows: Row[];
23
+ totalRows: number;
24
+ }>;
25
+ runSQL(sql: string, options?: RunSQLOptions): Promise<MalloyQueryData>;
26
+ runSQLStream(sql: string, _options?: RunSQLOptions): AsyncIterableIterator<QueryDataRow>;
27
+ runSQLBlockAndFetchResultSchema(sqlBlock: SQLBlock): Promise<{
28
+ data: MalloyQueryData;
29
+ schema: StructDef;
30
+ }>;
31
+ private getSQLBlockSchema;
32
+ /**
33
+ * Split's a structs columns declaration into individual columns
34
+ * to be fed back into fillStructDefFromTypeMap(). Handles commas
35
+ * within nested STRUCT() declarations.
36
+ *
37
+ * (https://github.com/looker-open-source/malloy/issues/635)
38
+ *
39
+ * @param s struct's column declaration
40
+ * @returns Array of column type declarations
41
+ */
42
+ private splitColumns;
43
+ private stringToTypeMap;
44
+ private fillStructDefFromTypeMap;
45
+ private schemaFromQuery;
46
+ fetchSchemaForSQLBlocks(sqlRefs: SQLBlock[]): Promise<{
47
+ schemas: Record<string, StructDef>;
48
+ errors: Record<string, string>;
49
+ }>;
50
+ fetchSchemaForTables(tables: string[]): Promise<{
51
+ schemas: Record<string, StructDef>;
52
+ errors: Record<string, string>;
53
+ }>;
54
+ private getTableSchema;
55
+ canFetchSchemaAndRunSimultaneously(): this is FetchSchemaAndRunSimultaneously;
56
+ canStream(): this is StreamingConnection;
57
+ canFetchSchemaAndRunStreamSimultaneously(): this is FetchSchemaAndRunStreamSimultaneously;
58
+ test(): Promise<void>;
59
+ manifestTemporaryTable(sqlCommand: string): Promise<string>;
60
+ }
@@ -0,0 +1,361 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.DuckDBConnection = void 0;
27
+ /*
28
+ * Copyright 2021 Google LLC
29
+ *
30
+ * This program is free software; you can redistribute it and/or
31
+ * modify it under the terms of the GNU General Public License
32
+ * version 2 as published by the Free Software Foundation.
33
+ *
34
+ * This program is distributed in the hope that it will be useful,
35
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
36
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
37
+ * GNU General Public License for more details.
38
+ */
39
+ const crypto = __importStar(require("crypto"));
40
+ const malloy_1 = require("@malloydata/malloy");
41
+ const duckdb_1 = require("duckdb");
42
+ const duckDBToMalloyTypes = {
43
+ BIGINT: "number",
44
+ DOUBLE: "number",
45
+ VARCHAR: "string",
46
+ DATE: "date",
47
+ TIMESTAMP: "timestamp",
48
+ TIME: "string",
49
+ DECIMAL: "number",
50
+ BOOLEAN: "boolean",
51
+ INTEGER: "number",
52
+ };
53
+ class DuckDBConnection {
54
+ constructor(name, databasePath = "test/data/duckdb/duckdb_test.db", workingDirectory = "/") {
55
+ this.name = name;
56
+ this.workingDirectory = workingDirectory;
57
+ this.isSetup = false;
58
+ this.database = new duckdb_1.Database(databasePath, duckdb_1.OPEN_READWRITE, // databasePath === ":memory:" ? OPEN_READWRITE : OPEN_READONLY,
59
+ (err) => {
60
+ if (err) {
61
+ return console.error(err);
62
+ }
63
+ });
64
+ this.connection = this.database.connect();
65
+ }
66
+ get dialectName() {
67
+ return "duckdb";
68
+ }
69
+ isPool() {
70
+ return false;
71
+ }
72
+ canPersist() {
73
+ return true;
74
+ }
75
+ async setup() {
76
+ if (!this.isSetup) {
77
+ if (this.workingDirectory) {
78
+ this.runDuckDBQuery(`SET FILE_SEARCH_PATH='${this.workingDirectory}'`);
79
+ }
80
+ // TODO: This is where we will load extensions once we figure
81
+ // out how to better support them.
82
+ // await this.runDuckDBQuery("INSTALL 'json'");
83
+ // await this.runDuckDBQuery("LOAD 'json'");
84
+ // await this.runDuckDBQuery("INSTALL 'httpfs'");
85
+ // await this.runDuckDBQuery("LOAD 'httpfs'");
86
+ // await this.runDuckDBQuery("DROP MACRO sum_distinct");
87
+ // try {
88
+ // await this.runDuckDBQuery(
89
+ // `
90
+ // create macro sum_distinct(l) as (
91
+ // select sum(x.val) as value FROM (select unnest(l)) x
92
+ // )
93
+ // `
94
+ // );
95
+ // } catch (e) {}
96
+ }
97
+ this.isSetup = true;
98
+ }
99
+ async runDuckDBQuery(sql) {
100
+ return new Promise((resolve, reject) => {
101
+ this.connection.all(sql, (err, result) => {
102
+ if (err) {
103
+ reject(err);
104
+ }
105
+ else
106
+ resolve({
107
+ rows: result,
108
+ totalRows: result.length,
109
+ });
110
+ });
111
+ });
112
+ }
113
+ async runRawSQL(sql) {
114
+ await this.setup();
115
+ return this.runDuckDBQuery(sql);
116
+ }
117
+ async runSQL(sql, options = {}) {
118
+ var _a;
119
+ const rowLimit = (_a = options.rowLimit) !== null && _a !== void 0 ? _a : 10;
120
+ const statements = sql.split("-- hack: split on this");
121
+ while (statements.length > 1) {
122
+ await this.runRawSQL(statements[0]);
123
+ statements.shift();
124
+ }
125
+ const retVal = await this.runRawSQL(statements[0]);
126
+ let result = retVal.rows;
127
+ if (result.length > rowLimit) {
128
+ result = result.slice(0, rowLimit);
129
+ }
130
+ return { rows: result, totalRows: result.length };
131
+ }
132
+ async *runSQLStream(sql, _options = {}) {
133
+ await this.setup();
134
+ const statements = sql.split("-- hack: split on this");
135
+ while (statements.length > 1) {
136
+ await this.runDuckDBQuery(statements[0]);
137
+ statements.shift();
138
+ }
139
+ for await (const row of this.connection.stream(statements[0])) {
140
+ yield row;
141
+ }
142
+ }
143
+ async runSQLBlockAndFetchResultSchema(sqlBlock) {
144
+ const data = await this.runSQL(sqlBlock.select);
145
+ const schema = (await this.fetchSchemaForSQLBlocks([sqlBlock])).schemas[sqlBlock.name];
146
+ return { data, schema };
147
+ }
148
+ async getSQLBlockSchema(sqlRef) {
149
+ const structDef = {
150
+ type: "struct",
151
+ dialect: "duckdb",
152
+ name: sqlRef.name,
153
+ structSource: {
154
+ type: "sql",
155
+ method: "subquery",
156
+ sqlBlock: sqlRef,
157
+ },
158
+ structRelationship: {
159
+ type: "basetable",
160
+ connectionName: this.name,
161
+ },
162
+ fields: [],
163
+ };
164
+ await this.schemaFromQuery(`DESCRIBE SELECT * FROM (${sqlRef.select})`, structDef);
165
+ return structDef;
166
+ }
167
+ /**
168
+ * Split's a structs columns declaration into individual columns
169
+ * to be fed back into fillStructDefFromTypeMap(). Handles commas
170
+ * within nested STRUCT() declarations.
171
+ *
172
+ * (https://github.com/looker-open-source/malloy/issues/635)
173
+ *
174
+ * @param s struct's column declaration
175
+ * @returns Array of column type declarations
176
+ */
177
+ splitColumns(s) {
178
+ const columns = [];
179
+ let parens = 0;
180
+ let column = "";
181
+ let eatSpaces = true;
182
+ for (let idx = 0; idx < s.length; idx++) {
183
+ const c = s.charAt(idx);
184
+ if (eatSpaces && c === " ") {
185
+ // Eat space
186
+ }
187
+ else {
188
+ eatSpaces = false;
189
+ if (!parens && c === ",") {
190
+ columns.push(column);
191
+ column = "";
192
+ eatSpaces = true;
193
+ }
194
+ else {
195
+ column += c;
196
+ }
197
+ if (c === "(") {
198
+ parens += 1;
199
+ }
200
+ else if (c === ")") {
201
+ parens -= 1;
202
+ }
203
+ }
204
+ }
205
+ columns.push(column);
206
+ return columns;
207
+ }
208
+ stringToTypeMap(s) {
209
+ const ret = {};
210
+ const columns = this.splitColumns(s);
211
+ for (const c of columns) {
212
+ //const [name, type] = c.split(" ", 1);
213
+ const columnMatch = c.match(/^(?<name>[^\s]+) (?<type>.*)$/);
214
+ if (columnMatch && columnMatch.groups) {
215
+ ret[columnMatch.groups["name"]] = columnMatch.groups["type"];
216
+ }
217
+ else {
218
+ throw new Error(`Badly form Structure definition ${s}`);
219
+ }
220
+ }
221
+ return ret;
222
+ }
223
+ fillStructDefFromTypeMap(structDef, typeMap) {
224
+ for (const name in typeMap) {
225
+ let duckDBType = typeMap[name];
226
+ // Remove DECIMAL(x,y) precision to simplify lookup
227
+ duckDBType = duckDBType.replace(/^DECIMAL\(\d+,\d+\)/g, "DECIMAL");
228
+ let malloyType = duckDBToMalloyTypes[duckDBType];
229
+ const arrayMatch = duckDBType.match(/(?<duckDBType>.*)\[\]$/);
230
+ if (arrayMatch && arrayMatch.groups) {
231
+ duckDBType = arrayMatch.groups["duckDBType"];
232
+ }
233
+ const structMatch = duckDBType.match(/^STRUCT\((?<fields>.*)\)$/);
234
+ if (structMatch && structMatch.groups) {
235
+ const newTypeMap = this.stringToTypeMap(structMatch.groups["fields"]);
236
+ const innerStructDef = {
237
+ type: "struct",
238
+ name,
239
+ dialect: this.dialectName,
240
+ structSource: { type: arrayMatch ? "nested" : "inline" },
241
+ structRelationship: {
242
+ type: arrayMatch ? "nested" : "inline",
243
+ field: name,
244
+ isArray: false,
245
+ },
246
+ fields: [],
247
+ };
248
+ this.fillStructDefFromTypeMap(innerStructDef, newTypeMap);
249
+ structDef.fields.push(innerStructDef);
250
+ }
251
+ else {
252
+ if (arrayMatch) {
253
+ malloyType = duckDBToMalloyTypes[duckDBType];
254
+ const innerStructDef = {
255
+ type: "struct",
256
+ name,
257
+ dialect: this.dialectName,
258
+ structSource: { type: "nested" },
259
+ structRelationship: { type: "nested", field: name, isArray: true },
260
+ fields: [{ type: malloyType, name: "value" }],
261
+ };
262
+ structDef.fields.push(innerStructDef);
263
+ }
264
+ else {
265
+ if (malloyType !== undefined) {
266
+ structDef.fields.push({
267
+ type: malloyType,
268
+ name,
269
+ });
270
+ }
271
+ else {
272
+ throw new Error(`unknown duckdb type ${duckDBType}`);
273
+ }
274
+ }
275
+ }
276
+ }
277
+ }
278
+ async schemaFromQuery(infoQuery, structDef) {
279
+ const typeMap = {};
280
+ const result = await this.runRawSQL(infoQuery);
281
+ for (const row of result.rows) {
282
+ typeMap[row["column_name"]] = row["column_type"];
283
+ }
284
+ this.fillStructDefFromTypeMap(structDef, typeMap);
285
+ }
286
+ async fetchSchemaForSQLBlocks(sqlRefs) {
287
+ const schemas = {};
288
+ const errors = {};
289
+ for (const sqlRef of sqlRefs) {
290
+ try {
291
+ schemas[sqlRef.name] = await this.getSQLBlockSchema(sqlRef);
292
+ }
293
+ catch (error) {
294
+ errors[sqlRef.name] = error;
295
+ }
296
+ }
297
+ return { schemas, errors };
298
+ }
299
+ async fetchSchemaForTables(tables) {
300
+ const schemas = {};
301
+ const errors = {};
302
+ for (const tableURL of tables) {
303
+ try {
304
+ schemas[tableURL] = await this.getTableSchema(tableURL);
305
+ }
306
+ catch (error) {
307
+ errors[tableURL] = error.toString();
308
+ }
309
+ }
310
+ return { schemas, errors };
311
+ }
312
+ async getTableSchema(tableURL) {
313
+ const { tablePath: tableName } = (0, malloy_1.parseTableURL)(tableURL);
314
+ const structDef = {
315
+ type: "struct",
316
+ name: tableName,
317
+ dialect: "duckdb",
318
+ structSource: { type: "table" },
319
+ structRelationship: {
320
+ type: "basetable",
321
+ connectionName: this.name,
322
+ },
323
+ fields: [],
324
+ };
325
+ // const { tablePath: tableName } = parseTableURL(tableURL);
326
+ // const [schema, table] = tableName.split(".");
327
+ // if (table === undefined) {
328
+ // throw new Error("Default schema not yet supported in DuckDB");
329
+ // }
330
+ // const infoQuery = `
331
+ // SELECT column_name, data_type FROM information_schema.columns
332
+ // WHERE table_name = '${table}'
333
+ // AND table_schema = '${schema}'
334
+ // `;
335
+ const infoQuery = `DESCRIBE SELECT * FROM ${tableName.match(/\//) ? `'${tableName}'` : tableName};`;
336
+ await this.schemaFromQuery(infoQuery, structDef);
337
+ return structDef;
338
+ }
339
+ canFetchSchemaAndRunSimultaneously() {
340
+ return false;
341
+ }
342
+ canStream() {
343
+ return true;
344
+ }
345
+ canFetchSchemaAndRunStreamSimultaneously() {
346
+ return false;
347
+ }
348
+ async test() {
349
+ await this.runRawSQL("SELECT 1");
350
+ }
351
+ async manifestTemporaryTable(sqlCommand) {
352
+ const hash = crypto.createHash("md5").update(sqlCommand).digest("hex");
353
+ const tableName = `tt${hash}`;
354
+ const cmd = `CREATE TEMPORARY TABLE IF NOT EXISTS ${tableName} AS (${sqlCommand});`;
355
+ // console.log(cmd);
356
+ await this.runRawSQL(cmd);
357
+ return tableName;
358
+ }
359
+ }
360
+ exports.DuckDBConnection = DuckDBConnection;
361
+ //# sourceMappingURL=duckdb_connection.js.map
@@ -0,0 +1 @@
1
+ export { DuckDBConnection } from "./duckdb_connection";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ /*
3
+ * Copyright 2021 Google LLC
4
+ *
5
+ * This program is free software; you can redistribute it and/or
6
+ * modify it under the terms of the GNU General Public License
7
+ * version 2 as published by the Free Software Foundation.
8
+ *
9
+ * This program is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DuckDBConnection = void 0;
16
+ var duckdb_connection_1 = require("./duckdb_connection");
17
+ Object.defineProperty(exports, "DuckDBConnection", { enumerable: true, get: function () { return duckdb_connection_1.DuckDBConnection; } });
18
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@malloydata/db-duckdb",
3
+ "version": "0.0.1",
4
+ "license": "GPL-2.0",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "lint": "eslint '**/*.ts{,x}'",
9
+ "lint-fix": "eslint '**/*.ts{,x}' --fix",
10
+ "test": "jest --config=../../jest.config.js",
11
+ "build": "tsc --build",
12
+ "malloyc": "ts-node ../../scripts/malloy-to-json"
13
+ },
14
+ "dependencies": {
15
+ "duckdb": "0.4.1-dev1094.0"
16
+ }
17
+ }