@sqlrooms/duckdb 0.0.0
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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-dev.log +58 -0
- package/.turbo/turbo-lint.log +4 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE.md +9 -0
- package/dist/duckdb copy.d.ts +28 -0
- package/dist/duckdb copy.d.ts.map +1 -0
- package/dist/duckdb copy.js +140 -0
- package/dist/duckdb.d.ts +15 -0
- package/dist/duckdb.d.ts.map +1 -0
- package/dist/duckdb.js +156 -0
- package/dist/exportToCsv.d.ts +2 -0
- package/dist/exportToCsv.d.ts.map +1 -0
- package/dist/exportToCsv.js +71 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types.d.ts +11 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/dist/useDuckConn.d.ts +42 -0
- package/dist/useDuckConn.d.ts.map +1 -0
- package/dist/useDuckConn.js +244 -0
- package/dist/useDuckDb.d.ts +42 -0
- package/dist/useDuckDb.d.ts.map +1 -0
- package/dist/useDuckDb.js +231 -0
- package/eslint.config.js +4 -0
- package/package.json +22 -0
- package/src/duckdb.ts +198 -0
- package/src/exportToCsv.ts +101 -0
- package/src/index.ts +4 -0
- package/src/types.ts +11 -0
- package/src/useDuckDb.ts +296 -0
- package/tsconfig.json +8 -0
package/src/useDuckDb.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import * as duckdb from '@duckdb/duckdb-wasm';
|
|
2
|
+
import * as arrow from 'apache-arrow';
|
|
3
|
+
import {DataTable} from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated DuckConn is deprecated, use DuckDb instead
|
|
7
|
+
*/
|
|
8
|
+
export type DuckConn = DuckDb;
|
|
9
|
+
|
|
10
|
+
export type DuckDb = {
|
|
11
|
+
db: duckdb.AsyncDuckDB;
|
|
12
|
+
conn: duckdb.AsyncDuckDBConnection;
|
|
13
|
+
worker: Worker;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ENABLE_DUCK_LOGGING = false;
|
|
17
|
+
|
|
18
|
+
const SilentLogger = {
|
|
19
|
+
log: () => {
|
|
20
|
+
/* do nothing */
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// TODO: shut DB down at some point
|
|
25
|
+
|
|
26
|
+
let duckConn: DuckDb;
|
|
27
|
+
let initialize: Promise<DuckDb> | undefined;
|
|
28
|
+
|
|
29
|
+
export class DuckQueryError extends Error {
|
|
30
|
+
readonly cause: unknown;
|
|
31
|
+
readonly query: string | undefined;
|
|
32
|
+
readonly queryCallStack: string | undefined;
|
|
33
|
+
constructor(err: unknown, query: string, stack: string | undefined) {
|
|
34
|
+
super(
|
|
35
|
+
`DB query failed: ${
|
|
36
|
+
err instanceof Error ? err.message : err
|
|
37
|
+
}\n\nFull query:\n\n${query}\n\nQuery call stack:\n\n${stack}\n\n`,
|
|
38
|
+
);
|
|
39
|
+
this.cause = err;
|
|
40
|
+
this.query = query;
|
|
41
|
+
this.queryCallStack = stack;
|
|
42
|
+
Object.setPrototypeOf(this, DuckQueryError.prototype);
|
|
43
|
+
}
|
|
44
|
+
getMessageForUser() {
|
|
45
|
+
const msg = this.cause instanceof Error ? this.cause.message : this.message;
|
|
46
|
+
return msg;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @deprecated getDuckConn is deprecated, use getDuckDb instead
|
|
52
|
+
*/
|
|
53
|
+
export const getDuckConn = getDuckDb;
|
|
54
|
+
|
|
55
|
+
export async function getDuckDb(): Promise<DuckDb> {
|
|
56
|
+
if (!globalThis.Worker) {
|
|
57
|
+
return Promise.reject('No Worker support');
|
|
58
|
+
}
|
|
59
|
+
if (duckConn) {
|
|
60
|
+
return duckConn;
|
|
61
|
+
} else if (initialize !== undefined) {
|
|
62
|
+
// The initialization has already been started, wait for it to finish
|
|
63
|
+
return initialize;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let resolve: (value: DuckDb) => void;
|
|
67
|
+
let reject: (reason?: unknown) => void;
|
|
68
|
+
initialize = new Promise((_resolve, _reject) => {
|
|
69
|
+
resolve = _resolve;
|
|
70
|
+
reject = _reject;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// TODO: Consider to load locally https://github.com/duckdb/duckdb-wasm/issues/1425#issuecomment-1742156605
|
|
75
|
+
const allBundles = duckdb.getJsDelivrBundles();
|
|
76
|
+
const bestBundle = await duckdb.selectBundle(allBundles);
|
|
77
|
+
if (!bestBundle.mainWorker) {
|
|
78
|
+
throw new Error('No best bundle found for DuckDB worker');
|
|
79
|
+
}
|
|
80
|
+
const workerUrl = URL.createObjectURL(
|
|
81
|
+
new Blob([`importScripts("${bestBundle.mainWorker}");`], {
|
|
82
|
+
type: 'text/javascript',
|
|
83
|
+
}),
|
|
84
|
+
);
|
|
85
|
+
// const worker = await duckdb.createWorker(bestBundle.mainWorker);
|
|
86
|
+
const worker = new window.Worker(workerUrl);
|
|
87
|
+
const logger = ENABLE_DUCK_LOGGING
|
|
88
|
+
? new duckdb.ConsoleLogger()
|
|
89
|
+
: SilentLogger;
|
|
90
|
+
const db = new (class extends duckdb.AsyncDuckDB {
|
|
91
|
+
onError(event: ErrorEvent) {
|
|
92
|
+
super.onError(event);
|
|
93
|
+
console.error('onError', event);
|
|
94
|
+
}
|
|
95
|
+
})(logger, worker);
|
|
96
|
+
await db.instantiate(bestBundle.mainModule, bestBundle.pthreadWorker);
|
|
97
|
+
URL.revokeObjectURL(workerUrl);
|
|
98
|
+
await db.open({
|
|
99
|
+
path: ':memory:',
|
|
100
|
+
query: {
|
|
101
|
+
// castBigIntToDouble: true
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const conn = await db.connect();
|
|
105
|
+
// Replace conn.query to include full query in the error message
|
|
106
|
+
const connQuery = conn.query;
|
|
107
|
+
conn.query = (async (q: string) => {
|
|
108
|
+
const stack = new Error().stack;
|
|
109
|
+
try {
|
|
110
|
+
return await connQuery.call(conn, q);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
throw new DuckQueryError(err, q, stack);
|
|
113
|
+
// throw new Error(
|
|
114
|
+
// `Query failed: ${err}\n\nFull query:\n\n${q}\n\nQuery call stack:\n\n${stack}\n\n`,
|
|
115
|
+
// );
|
|
116
|
+
}
|
|
117
|
+
}) as typeof conn.query;
|
|
118
|
+
await conn.query(`
|
|
119
|
+
SET max_expression_depth TO 100000;
|
|
120
|
+
SET memory_limit = '10GB';
|
|
121
|
+
`);
|
|
122
|
+
duckConn = {db, conn, worker};
|
|
123
|
+
resolve!(duckConn);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
reject!(err);
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return duckConn;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Cache the promise to avoid multiple initialization attempts
|
|
133
|
+
let duckPromise: Promise<DuckDb> | null = null;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @deprecated useDuckConn is deprecated, use useDuckDb instead
|
|
137
|
+
*/
|
|
138
|
+
export const useDuckConn = useDuckDb;
|
|
139
|
+
|
|
140
|
+
export function useDuckDb(): DuckDb {
|
|
141
|
+
if (!duckPromise) {
|
|
142
|
+
duckPromise = getDuckDb();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// If we don't have a connection yet, throw the promise
|
|
146
|
+
// This will trigger Suspense
|
|
147
|
+
if (!duckConn) {
|
|
148
|
+
throw duckPromise;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return duckConn;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const isNumericDuckType = (type: string) =>
|
|
155
|
+
type.indexOf('INT') >= 0 ||
|
|
156
|
+
type.indexOf('DECIMAL') >= 0 ||
|
|
157
|
+
type.indexOf('FLOAT') >= 0 ||
|
|
158
|
+
type.indexOf('REAL') >= 0 ||
|
|
159
|
+
type.indexOf('DOUBLE') >= 0;
|
|
160
|
+
|
|
161
|
+
export function getColValAsNumber(
|
|
162
|
+
res: arrow.Table,
|
|
163
|
+
column: string | number = 0,
|
|
164
|
+
index = 0,
|
|
165
|
+
): number {
|
|
166
|
+
const v = (
|
|
167
|
+
typeof column === 'number' ? res.getChildAt(column) : res.getChild(column)
|
|
168
|
+
)?.get(index);
|
|
169
|
+
if (v === undefined || v === null) {
|
|
170
|
+
return NaN;
|
|
171
|
+
}
|
|
172
|
+
// if it's an array (can be returned by duckdb as bigint)
|
|
173
|
+
return Number(v[0] ?? v);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const escapeVal = (val: unknown) => {
|
|
177
|
+
return `'${String(val).replace(/'/g, "''")}'`;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const escapeId = (id: string) => {
|
|
181
|
+
const str = String(id);
|
|
182
|
+
if (str.startsWith('"') && str.endsWith('"')) {
|
|
183
|
+
return str;
|
|
184
|
+
}
|
|
185
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export async function getDuckTables(schema = 'main'): Promise<string[]> {
|
|
189
|
+
const {conn} = await getDuckDb();
|
|
190
|
+
const tablesResults = await conn.query(
|
|
191
|
+
`SELECT * FROM information_schema.tables
|
|
192
|
+
WHERE table_schema = '${schema}'
|
|
193
|
+
ORDER BY table_name`,
|
|
194
|
+
);
|
|
195
|
+
const tableNames: string[] = [];
|
|
196
|
+
for (let i = 0; i < tablesResults.numRows; i++) {
|
|
197
|
+
tableNames.push(tablesResults.getChild('table_name')?.get(i));
|
|
198
|
+
}
|
|
199
|
+
return tableNames;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function getDuckTableSchema(
|
|
203
|
+
tableName: string,
|
|
204
|
+
schema = 'main',
|
|
205
|
+
): Promise<DataTable> {
|
|
206
|
+
const {conn} = await getDuckDb();
|
|
207
|
+
const describeResults = await conn.query(`DESCRIBE ${schema}.${tableName}`);
|
|
208
|
+
const columnNames = describeResults.getChild('column_name');
|
|
209
|
+
const columnTypes = describeResults.getChild('column_type');
|
|
210
|
+
const columns = [];
|
|
211
|
+
for (let di = 0; di < describeResults.numRows; di++) {
|
|
212
|
+
const columnName = columnNames?.get(di);
|
|
213
|
+
const columnType = columnTypes?.get(di);
|
|
214
|
+
columns.push({name: columnName, type: columnType});
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
tableName,
|
|
218
|
+
columns,
|
|
219
|
+
// Costly to get the row count for large tables
|
|
220
|
+
// rowCount: getColValAsNumber(
|
|
221
|
+
// await conn.query(`SELECT COUNT(*) FROM ${schema}.${tableName}`),
|
|
222
|
+
// ),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function getDuckTableSchemas(
|
|
227
|
+
schema = 'main',
|
|
228
|
+
): Promise<DataTable[]> {
|
|
229
|
+
const tableNames = await getDuckTables(schema);
|
|
230
|
+
const tablesInfo: DataTable[] = [];
|
|
231
|
+
for (const tableName of tableNames) {
|
|
232
|
+
tablesInfo.push(await getDuckTableSchema(tableName, schema));
|
|
233
|
+
}
|
|
234
|
+
return tablesInfo;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function checkTableExists(
|
|
238
|
+
tableName: string,
|
|
239
|
+
schema = 'main',
|
|
240
|
+
): Promise<boolean> {
|
|
241
|
+
const {conn} = await getDuckDb();
|
|
242
|
+
const res = await conn.query(
|
|
243
|
+
`SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${schema}' AND table_name = '${tableName}'`,
|
|
244
|
+
);
|
|
245
|
+
return getColValAsNumber(res) > 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function dropAllTables(schema?: string): Promise<void> {
|
|
249
|
+
try {
|
|
250
|
+
const {conn} = await getDuckDb();
|
|
251
|
+
if (schema && schema !== 'main') {
|
|
252
|
+
await conn.query(`DROP SCHEMA IF EXISTS ${schema} CASCADE`);
|
|
253
|
+
} else {
|
|
254
|
+
const res = await conn.query(
|
|
255
|
+
`SELECT table_name, table_schema, table_type FROM information_schema.tables${
|
|
256
|
+
schema ? ` WHERE table_schema = '${schema}'` : ''
|
|
257
|
+
}`,
|
|
258
|
+
);
|
|
259
|
+
const schemasCol = res.getChild('table_schema');
|
|
260
|
+
const tableNamesCol = res.getChild('table_name');
|
|
261
|
+
const tableTypesCol = res.getChild('table_type');
|
|
262
|
+
for (let i = 0; i < res.numRows; i++) {
|
|
263
|
+
try {
|
|
264
|
+
const schemaName = schemasCol?.get(i);
|
|
265
|
+
const tableName = tableNamesCol?.get(i);
|
|
266
|
+
const tableType = tableTypesCol?.get(i);
|
|
267
|
+
if (tableName) {
|
|
268
|
+
const query = `DROP ${
|
|
269
|
+
tableType === 'VIEW' ? 'VIEW' : 'TABLE'
|
|
270
|
+
} IF EXISTS ${schemaName}.${tableName}`;
|
|
271
|
+
await conn.query(query);
|
|
272
|
+
}
|
|
273
|
+
} catch (err) {
|
|
274
|
+
console.error(err);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (err) {
|
|
279
|
+
console.error(err);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function dropTable(tableName: string): Promise<void> {
|
|
284
|
+
const {conn} = await getDuckDb();
|
|
285
|
+
await conn.query(`DROP TABLE IF EXISTS ${tableName};`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export async function dropFile(fname: string): Promise<void> {
|
|
289
|
+
const {db} = await getDuckDb();
|
|
290
|
+
db.dropFile(fname);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function dropAllFiles(): Promise<void> {
|
|
294
|
+
const {db} = await getDuckDb();
|
|
295
|
+
db.dropFiles();
|
|
296
|
+
}
|