Skip to content

Commit 89d74fd

Browse files
committed
Add GizmoSQL support to @malloydata/db-duckdb
Adds GizmoSQLConnection class following the established pattern for DuckDB variants (similar to MotherDuck and WASM implementations). - GizmoSQLConnection extends DuckDBCommon for DuckDB SQL dialect support - Python ADBC bridge handles Arrow Flight SQL protocol communication - SQL injection protection for catalog identifiers - Efficient data transfer via Apache Arrow IPC format GizmoSQL is DuckDB served over Arrow Flight SQL, making db-duckdb the appropriate package for this connection type.
1 parent 13ac0d7 commit 89d74fd

File tree

15 files changed

+186
-837
lines changed

15 files changed

+186
-837
lines changed

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
"packages/malloy-db-bigquery",
1414
"packages/malloy-db-trino",
1515
"packages/malloy-db-duckdb",
16-
"packages/malloy-db-gizmosql",
1716
"packages/malloy-db-mysql",
1817
"packages/malloy-db-postgres",
1918
"packages/malloy-db-snowflake",
File renamed without changes.
File renamed without changes.
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/*
2+
* Copyright 2025 Google LLC
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining
5+
* a copy of this software and associated documentation files
6+
* (the "Software"), to deal in the Software without restriction,
7+
* including without limitation the rights to use, copy, modify, merge,
8+
* publish, distribute, sublicense, and/or sell copies of the Software,
9+
* and to permit persons to whom the Software is furnished to do so,
10+
* subject to the following conditions:
11+
*
12+
* The above copyright notice and this permission notice shall be
13+
* included in all copies or substantial portions of the Software.
14+
*
15+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16+
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17+
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18+
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19+
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20+
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21+
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22+
*/
23+
24+
import crypto from 'crypto';
25+
import type {
26+
ConnectionConfig,
27+
QueryDataRow,
28+
QueryOptionsReader,
29+
RunSQLOptions,
30+
} from '@malloydata/malloy';
31+
import {DuckDBCommon} from './duckdb_common';
32+
import {PythonBridgeClient} from './python_bridge_client';
33+
import type {Table} from 'apache-arrow';
34+
35+
export interface GizmoSQLConnectionOptions extends ConnectionConfig {
36+
gizmosqlUri: string;
37+
gizmosqlUsername: string;
38+
gizmosqlPassword: string;
39+
gizmosqlCatalog?: string;
40+
pythonPath?: string;
41+
}
42+
43+
export class GizmoSQLConnection extends DuckDBCommon {
44+
public readonly name: string;
45+
private client: PythonBridgeClient | null = null;
46+
private uri: string;
47+
private username: string;
48+
private password: string;
49+
private catalog: string;
50+
private pythonPath?: string;
51+
private isSetup: Promise<void> | undefined;
52+
private setupError: Error | undefined;
53+
54+
constructor(
55+
options: GizmoSQLConnectionOptions,
56+
queryOptions?: QueryOptionsReader
57+
) {
58+
super(queryOptions);
59+
this.name = options.name;
60+
this.uri = options.gizmosqlUri;
61+
this.username = options.gizmosqlUsername;
62+
this.password = options.gizmosqlPassword;
63+
this.catalog = options.gizmosqlCatalog || 'main';
64+
this.pythonPath = options.pythonPath;
65+
}
66+
67+
private async getClient(): Promise<PythonBridgeClient> {
68+
if (!this.client) {
69+
this.client = new PythonBridgeClient({
70+
uri: this.uri,
71+
username: this.username,
72+
password: this.password,
73+
catalog: this.catalog,
74+
pythonPath: this.pythonPath,
75+
});
76+
77+
await this.client.connect();
78+
}
79+
return this.client;
80+
}
81+
82+
protected async setup(): Promise<void> {
83+
if (this.setupError) {
84+
throw this.setupError;
85+
}
86+
87+
const doSetup = async () => {
88+
try {
89+
await this.getClient();
90+
} catch (error) {
91+
this.setupError = error as Error;
92+
throw error;
93+
}
94+
};
95+
96+
if (!this.isSetup) {
97+
this.isSetup = doSetup();
98+
}
99+
await this.isSetup;
100+
}
101+
102+
protected async runDuckDBQuery(
103+
sql: string
104+
): Promise<{rows: QueryDataRow[]; totalRows: number}> {
105+
const client = await this.getClient();
106+
107+
try {
108+
const table: Table = await client.query(sql);
109+
110+
// Convert Arrow Table to QueryDataRow[]
111+
const rows: QueryDataRow[] = [];
112+
for (let i = 0; i < table.numRows; i++) {
113+
const row: QueryDataRow = {};
114+
for (const field of table.schema.fields) {
115+
const column = table.getChild(field.name);
116+
if (column) {
117+
row[field.name] = column.get(i);
118+
}
119+
}
120+
rows.push(row);
121+
}
122+
123+
return {rows, totalRows: rows.length};
124+
} catch (error) {
125+
const message = error instanceof Error ? error.message : String(error);
126+
throw new Error(`GizmoSQL query failed: ${message}`);
127+
}
128+
}
129+
130+
public async *runSQLStream(
131+
sql: string,
132+
{rowLimit, abortSignal}: RunSQLOptions = {}
133+
): AsyncIterableIterator<QueryDataRow> {
134+
const defaultOptions = this.readQueryOptions();
135+
rowLimit ??= defaultOptions.rowLimit;
136+
await this.setup();
137+
138+
const statements = sql.split('-- hack: split on this');
139+
140+
while (statements.length > 1) {
141+
await this.runDuckDBQuery(statements[0]);
142+
statements.shift();
143+
}
144+
145+
const result = await this.runDuckDBQuery(statements[0]);
146+
let index = 0;
147+
148+
for (const row of result.rows) {
149+
if (
150+
(rowLimit !== undefined && index >= rowLimit) ||
151+
abortSignal?.aborted
152+
) {
153+
break;
154+
}
155+
index++;
156+
yield row;
157+
}
158+
}
159+
160+
async createHash(sqlCommand: string): Promise<string> {
161+
return crypto.createHash('md5').update(sqlCommand).digest('hex');
162+
}
163+
164+
public async close(): Promise<void> {
165+
if (this.client) {
166+
this.client.close();
167+
this.client = null;
168+
}
169+
}
170+
}

packages/malloy-db-duckdb/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@
2222
*/
2323

2424
export {DuckDBConnection} from './duckdb_connection';
25+
export {
26+
GizmoSQLConnection,
27+
type GizmoSQLConnectionOptions,
28+
} from './gizmosql_connection';

packages/malloy-db-gizmosql/src/python_bridge_client.ts renamed to packages/malloy-db-duckdb/src/python_bridge_client.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright 2025 Google LLC
33
*
44
* Python bridge client for GizmoSQL
5-
* Uses Python ADBC driver which properly handles GizmoSQL's incomplete Flight SQL implementation
5+
* Uses Python ADBC driver which properly handles GizmoSQL's Flight SQL implementation
66
*/
77

88
import {spawn} from 'child_process';
@@ -26,6 +26,7 @@ export class PythonBridgeClient {
2626

2727
/**
2828
* Execute SQL query via Python ADBC bridge
29+
* Returns Apache Arrow Table
2930
*/
3031
async query(sql: string): Promise<Table> {
3132
const request = {
@@ -55,7 +56,10 @@ export class PythonBridgeClient {
5556

5657
python.on('close', (code) => {
5758
if (code !== 0) {
58-
reject(new Error(`Python bridge failed (code ${code}): ${stderr}`));
59+
const error = new Error(
60+
`Python bridge failed (code ${code}): ${stderr}`
61+
);
62+
reject(error);
5963
return;
6064
}
6165

@@ -75,7 +79,9 @@ export class PythonBridgeClient {
7579

7680
resolve(table);
7781
} catch (error) {
78-
reject(new Error(`Failed to parse bridge response: ${error}`));
82+
const message =
83+
error instanceof Error ? error.message : String(error);
84+
reject(new Error(`Failed to parse bridge response: ${message}`));
7985
}
8086
});
8187

@@ -94,6 +100,6 @@ export class PythonBridgeClient {
94100
}
95101

96102
close(): void {
97-
// No cleanup needed
103+
// No cleanup needed for stateless client
98104
}
99105
}

packages/malloy-db-duckdb/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
{
1010
"path": "../malloy"
1111
}
12-
]
12+
],
13+
"include": ["src/**/*.ts"]
1314
}

packages/malloy-db-gizmosql/package.json

Lines changed: 0 additions & 32 deletions
This file was deleted.

0 commit comments

Comments
 (0)