@flowblade/sqlduck 0.9.0 → 0.11.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/README.md +68 -1
- package/dist/index.cjs +356 -35
- package/dist/index.d.cts +153 -4
- package/dist/index.d.mts +153 -4
- package/dist/index.mjs +349 -36
- package/package.json +11 -7
package/README.md
CHANGED
|
@@ -4,6 +4,74 @@
|
|
|
4
4
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
|
+
### Create a database connection
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { DuckDBInstance } from '@duckdb/node-api';
|
|
11
|
+
DuckDBInstance.create(undefined, {
|
|
12
|
+
access_mode: 'READ_WRITE',
|
|
13
|
+
max_memory: '512M',
|
|
14
|
+
});
|
|
15
|
+
export const conn = await instance.connect();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### Append data to a database
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { SqlDuck, DuckDatabaseManager } from "@flowblade/sqlduck";
|
|
22
|
+
import * as z from "zod";
|
|
23
|
+
import { conn } from "./db.config.ts";
|
|
24
|
+
|
|
25
|
+
const dbManager = new DuckDatabaseManager(conn);
|
|
26
|
+
const database = await dbManager.attach({
|
|
27
|
+
type: ':memory:', // can be 'duckdb', ...
|
|
28
|
+
alias: 'mydb',
|
|
29
|
+
options: { COMPRESS: 'false' },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const sqlDuck = new SqlDuck({ conn });
|
|
33
|
+
|
|
34
|
+
// Define a zod schema, it will be used to create the table
|
|
35
|
+
const userSchema = z.object({
|
|
36
|
+
id: z.int32().min(1).meta({ primaryKey: true }),
|
|
37
|
+
name: z.string(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Example of a datasource (can be generator, async generator, async iterable)
|
|
41
|
+
async function* getUsers(): AsyncIterableIterator<
|
|
42
|
+
z.infer<typeof userSchema>
|
|
43
|
+
> {
|
|
44
|
+
// database or api call
|
|
45
|
+
yield { id: 1, name: 'John' };
|
|
46
|
+
yield { id: 2, name: 'Jane' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create a table from the schema and the datasource
|
|
50
|
+
const result = await sqlDuck.toTable({
|
|
51
|
+
table: new Table({ name: 'user', database: database.alias }),
|
|
52
|
+
schema: userSchema, // The schema to use to create the table
|
|
53
|
+
rowStream: getUsers(), // The async iterable that yields rows
|
|
54
|
+
// 👇Optional:
|
|
55
|
+
chunkSize: 2048, // Number of rows to append when using duckdb appender. Default is 2048
|
|
56
|
+
onDataAppended: ({ timeMs, totalRows, rowsPerSecond }) => {
|
|
57
|
+
console.log(
|
|
58
|
+
`Appended ${totalRows} in time ${timeMs}ms, est: ${rowsPerSecond} rows/s`
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
// Optional table creation options
|
|
62
|
+
createOptions: {
|
|
63
|
+
create: 'CREATE_OR_REPLACE',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
console.log(`Inserted ${result.totalRows} rows in ${result.timeMs}ms`);
|
|
68
|
+
console.log(`Table created with DDL: ${result.createTableDDL}`);
|
|
69
|
+
|
|
70
|
+
const reader = await conn.runAndReadAll('select * from mydb.user');
|
|
71
|
+
const rows = reader.getRowObjectsJS();
|
|
72
|
+
// [{id: 1, name: 'John'}, {id: 2, name: 'Jane'}]]
|
|
73
|
+
```
|
|
74
|
+
|
|
7
75
|
### Create a memory table
|
|
8
76
|
|
|
9
77
|
```typescript
|
|
@@ -35,7 +103,6 @@ const result = sqlDuck.toTable({
|
|
|
35
103
|
onDataAppended: ({ total }) => {
|
|
36
104
|
console.log(`Appended ${total} rows so far`);
|
|
37
105
|
},
|
|
38
|
-
onDataAppendedBatchSize: 4096, // Call onDataAppended every 4096 rows
|
|
39
106
|
// Optional table creation options
|
|
40
107
|
createOptions: {
|
|
41
108
|
create: "CREATE_OR_REPLACE",
|
package/dist/index.cjs
CHANGED
|
@@ -22,8 +22,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
22
22
|
}) : target, mod));
|
|
23
23
|
//#endregion
|
|
24
24
|
let _duckdb_node_api = require("@duckdb/node-api");
|
|
25
|
+
let _logtape_logtape = require("@logtape/logtape");
|
|
25
26
|
let zod = require("zod");
|
|
26
27
|
zod = __toESM(zod);
|
|
28
|
+
let _httpx_assert = require("@httpx/assert");
|
|
29
|
+
let _httpx_plain_object = require("@httpx/plain-object");
|
|
27
30
|
//#region src/helpers/duck-exec.ts
|
|
28
31
|
var DuckExec = class {
|
|
29
32
|
#conn;
|
|
@@ -141,6 +144,12 @@ const createOnDataAppendedCollector = () => {
|
|
|
141
144
|
};
|
|
142
145
|
};
|
|
143
146
|
//#endregion
|
|
147
|
+
//#region src/config/flowblade-logtape-sqlduck.config.ts
|
|
148
|
+
const flowbladeLogtapeSqlduckConfig = { categories: ["flowblade", "sqlduck"] };
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/logger/sqlduck-default-logtape-logger.ts
|
|
151
|
+
const sqlduckDefaultLogtapeLogger = (0, _logtape_logtape.getLogger)(flowbladeLogtapeSqlduckConfig.categories);
|
|
152
|
+
//#endregion
|
|
144
153
|
//#region src/table/get-duckdb-number-column-type.ts
|
|
145
154
|
const isFloatValue = (value) => {
|
|
146
155
|
if (!Number.isFinite(value)) return true;
|
|
@@ -174,6 +183,16 @@ const createOptions = {
|
|
|
174
183
|
CREATE_OR_REPLACE: "CREATE OR REPLACE TABLE",
|
|
175
184
|
IF_NOT_EXISTS: "CREATE TABLE IF NOT EXISTS"
|
|
176
185
|
};
|
|
186
|
+
const duckDbTypesMap = new Map([
|
|
187
|
+
["VARCHAR", _duckdb_node_api.VARCHAR],
|
|
188
|
+
["BIGINT", _duckdb_node_api.BIGINT],
|
|
189
|
+
["TIMESTAMP", _duckdb_node_api.TIMESTAMP],
|
|
190
|
+
["UUID", _duckdb_node_api.UUID],
|
|
191
|
+
["BOOLEAN", _duckdb_node_api.BOOLEAN],
|
|
192
|
+
["INTEGER", _duckdb_node_api.INTEGER],
|
|
193
|
+
["DOUBLE", _duckdb_node_api.DOUBLE],
|
|
194
|
+
["FLOAT", _duckdb_node_api.FLOAT]
|
|
195
|
+
]);
|
|
177
196
|
const getTableCreateFromZod = (params) => {
|
|
178
197
|
const { table, schema, options } = params;
|
|
179
198
|
const { create = "CREATE" } = options ?? {};
|
|
@@ -186,9 +205,10 @@ const getTableCreateFromZod = (params) => {
|
|
|
186
205
|
if (json.properties === void 0) throw new TypeError("Schema must have at least one property");
|
|
187
206
|
const columnTypesMap = /* @__PURE__ */ new Map();
|
|
188
207
|
for (const [columnName, def] of Object.entries(json.properties)) {
|
|
189
|
-
const { type, nullable, format, primaryKey, minimum, maximum } = def;
|
|
208
|
+
const { type, duckdbType, nullable, format, primaryKey, minimum, maximum } = def;
|
|
190
209
|
const c = { name: columnName };
|
|
191
|
-
|
|
210
|
+
if (duckdbType !== void 0 && duckDbTypesMap.has(duckdbType)) c.duckdbType = duckDbTypesMap.get(duckdbType);
|
|
211
|
+
else switch (type) {
|
|
192
212
|
case "string":
|
|
193
213
|
switch (format) {
|
|
194
214
|
case "date-time":
|
|
@@ -244,15 +264,18 @@ const getTableCreateFromZod = (params) => {
|
|
|
244
264
|
//#endregion
|
|
245
265
|
//#region src/table/create-table-from-zod.ts
|
|
246
266
|
const createTableFromZod = async (params) => {
|
|
247
|
-
const { conn, table, schema, options } = params;
|
|
267
|
+
const { conn, table, schema, options, logger = sqlduckDefaultLogtapeLogger } = params;
|
|
248
268
|
const { ddl, columnTypes } = getTableCreateFromZod({
|
|
249
269
|
table,
|
|
250
270
|
schema,
|
|
251
271
|
options
|
|
252
272
|
});
|
|
273
|
+
logger.debug(`Generate DDL for table '${table.getFullName()}'`, { ddl });
|
|
253
274
|
try {
|
|
254
275
|
await conn.run(ddl);
|
|
276
|
+
logger.info(`Table '${table.getFullName()}' successfully created`, { ddl });
|
|
255
277
|
} catch (e) {
|
|
278
|
+
logger.error(`Failed to create table '${table.getFullName()}': ${e.message}`, { ddl });
|
|
256
279
|
throw new Error(`Failed to create table '${table.getFullName()}': ${e.message}`, { cause: e });
|
|
257
280
|
}
|
|
258
281
|
return {
|
|
@@ -305,11 +328,11 @@ async function* rowsToColumnsChunks(params) {
|
|
|
305
328
|
//#endregion
|
|
306
329
|
//#region src/sql-duck.ts
|
|
307
330
|
var SqlDuck = class {
|
|
308
|
-
#
|
|
331
|
+
#conn;
|
|
309
332
|
#logger;
|
|
310
333
|
constructor(params) {
|
|
311
|
-
this.#
|
|
312
|
-
this.#logger = params.logger;
|
|
334
|
+
this.#conn = params.conn;
|
|
335
|
+
this.#logger = params.logger ?? sqlduckDefaultLogtapeLogger;
|
|
313
336
|
}
|
|
314
337
|
/**
|
|
315
338
|
* Create a table from a Zod schema and fill it with data from a row stream.
|
|
@@ -353,12 +376,12 @@ var SqlDuck = class {
|
|
|
353
376
|
if (!Number.isSafeInteger(chunkSize) || chunkSize < 1 || chunkSize > 2048) throw new Error("chunkSize must be a number between 1 and 2048");
|
|
354
377
|
const timeStart = Date.now();
|
|
355
378
|
const { columnTypes, ddl } = await createTableFromZod({
|
|
356
|
-
conn: this.#
|
|
379
|
+
conn: this.#conn,
|
|
357
380
|
schema,
|
|
358
381
|
table,
|
|
359
382
|
options: createOptions
|
|
360
383
|
});
|
|
361
|
-
const appender = await this.#
|
|
384
|
+
const appender = await this.#conn.createAppender(table.tableName, table.schemaName, table.databaseName);
|
|
362
385
|
const chunkTypes = Array.from(columnTypes.values());
|
|
363
386
|
let totalRows = 0;
|
|
364
387
|
const dataAppendedCollector = createOnDataAppendedCollector();
|
|
@@ -366,29 +389,74 @@ var SqlDuck = class {
|
|
|
366
389
|
rows: rowStream,
|
|
367
390
|
chunkSize
|
|
368
391
|
});
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
392
|
+
try {
|
|
393
|
+
for await (const dataChunk of columnStream) {
|
|
394
|
+
const chunk = _duckdb_node_api.DuckDBDataChunk.create(chunkTypes);
|
|
395
|
+
this.#logger.debug(`Inserting chunk of ${dataChunk.length} rows`, { table: table.getFullName() });
|
|
396
|
+
totalRows += dataChunk?.[0]?.length ?? 0;
|
|
397
|
+
chunk.setColumns(dataChunk);
|
|
398
|
+
appender.appendDataChunk(chunk);
|
|
399
|
+
appender.flushSync();
|
|
400
|
+
if (onDataAppended !== void 0) {
|
|
401
|
+
const payload = dataAppendedCollector(totalRows);
|
|
402
|
+
if (isOnDataAppendedAsyncCb(onDataAppended)) await onDataAppended(payload);
|
|
403
|
+
else onDataAppended(payload);
|
|
404
|
+
}
|
|
380
405
|
}
|
|
406
|
+
appender.closeSync();
|
|
407
|
+
const timeMs = Math.round(Date.now() - timeStart);
|
|
408
|
+
this.#logger.info(`Successfully appended ${totalRows} rows into '${table.getFullName()}' in ${timeMs}ms`, {
|
|
409
|
+
table: table.getFullName(),
|
|
410
|
+
timeMs,
|
|
411
|
+
totalRows
|
|
412
|
+
});
|
|
413
|
+
return {
|
|
414
|
+
timeMs,
|
|
415
|
+
totalRows,
|
|
416
|
+
createTableDDL: ddl
|
|
417
|
+
};
|
|
418
|
+
} catch (e) {
|
|
419
|
+
appender.closeSync();
|
|
420
|
+
const msg = `Failed to append data into table '${table.getFullName()}' - ${e?.message ?? ""}`;
|
|
421
|
+
this.#logger.error(msg, { table: table.getFullName() });
|
|
422
|
+
throw new Error(msg, { cause: e });
|
|
381
423
|
}
|
|
382
|
-
|
|
424
|
+
};
|
|
425
|
+
};
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/utils/zod-codecs.ts
|
|
428
|
+
const zodCodecs = {
|
|
429
|
+
dateToString: zod.codec(zod.date(), zod.iso.datetime(), {
|
|
430
|
+
decode: (date) => date.toISOString(),
|
|
431
|
+
encode: (isoString) => new Date(isoString)
|
|
432
|
+
}),
|
|
433
|
+
bigintToString: zod.codec(zod.bigint(), zod.string().meta({ format: "int64" }), {
|
|
434
|
+
decode: (bigint) => bigint.toString(),
|
|
435
|
+
encode: BigInt
|
|
436
|
+
})
|
|
437
|
+
};
|
|
438
|
+
//#endregion
|
|
439
|
+
//#region src/objects/database.ts
|
|
440
|
+
var Database = class {
|
|
441
|
+
#params;
|
|
442
|
+
get alias() {
|
|
443
|
+
return this.#params.alias;
|
|
444
|
+
}
|
|
445
|
+
constructor(params) {
|
|
446
|
+
this.#params = params;
|
|
447
|
+
}
|
|
448
|
+
toJson() {
|
|
383
449
|
return {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
createTableDDL: ddl
|
|
450
|
+
type: "database",
|
|
451
|
+
params: { alias: this.#params.alias }
|
|
387
452
|
};
|
|
388
|
-
}
|
|
453
|
+
}
|
|
454
|
+
[Symbol.toStringTag]() {
|
|
455
|
+
return this.alias;
|
|
456
|
+
}
|
|
389
457
|
};
|
|
390
458
|
//#endregion
|
|
391
|
-
//#region src/
|
|
459
|
+
//#region src/objects/table.ts
|
|
392
460
|
var Table = class Table {
|
|
393
461
|
#fqTable;
|
|
394
462
|
get tableName() {
|
|
@@ -430,20 +498,273 @@ var Table = class Table {
|
|
|
430
498
|
};
|
|
431
499
|
};
|
|
432
500
|
//#endregion
|
|
433
|
-
//#region src/
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
501
|
+
//#region src/validation/core/duckdb-reserved-keywords.ts
|
|
502
|
+
/**
|
|
503
|
+
* DuckDB reserved keywords that cannot be used as unquoted identifiers.
|
|
504
|
+
* @see https://duckdb.org/docs/sql/keywords-and-identifiers.html
|
|
505
|
+
*/
|
|
506
|
+
const duckdbReservedKeywords = [
|
|
507
|
+
"ALL",
|
|
508
|
+
"ANALYSE",
|
|
509
|
+
"ANALYZE",
|
|
510
|
+
"AND",
|
|
511
|
+
"ANY",
|
|
512
|
+
"ARRAY",
|
|
513
|
+
"AS",
|
|
514
|
+
"ASC",
|
|
515
|
+
"ASYMMETRIC",
|
|
516
|
+
"BOTH",
|
|
517
|
+
"CASE",
|
|
518
|
+
"CAST",
|
|
519
|
+
"CHECK",
|
|
520
|
+
"COLLATE",
|
|
521
|
+
"COLUMN",
|
|
522
|
+
"CONSTRAINT",
|
|
523
|
+
"CREATE",
|
|
524
|
+
"CROSS",
|
|
525
|
+
"CURRENT_CATALOG",
|
|
526
|
+
"CURRENT_DATE",
|
|
527
|
+
"CURRENT_ROLE",
|
|
528
|
+
"CURRENT_SCHEMA",
|
|
529
|
+
"CURRENT_TIME",
|
|
530
|
+
"CURRENT_TIMESTAMP",
|
|
531
|
+
"CURRENT_USER",
|
|
532
|
+
"DEFAULT",
|
|
533
|
+
"DEFERRABLE",
|
|
534
|
+
"DESC",
|
|
535
|
+
"DISTINCT",
|
|
536
|
+
"DO",
|
|
537
|
+
"ELSE",
|
|
538
|
+
"END",
|
|
539
|
+
"EXCEPT",
|
|
540
|
+
"EXISTS",
|
|
541
|
+
"EXTRACT",
|
|
542
|
+
"FALSE",
|
|
543
|
+
"FETCH",
|
|
544
|
+
"FOR",
|
|
545
|
+
"FOREIGN",
|
|
546
|
+
"FROM",
|
|
547
|
+
"GRANT",
|
|
548
|
+
"GROUP",
|
|
549
|
+
"HAVING",
|
|
550
|
+
"IF",
|
|
551
|
+
"ILIKE",
|
|
552
|
+
"IN",
|
|
553
|
+
"INITIALLY",
|
|
554
|
+
"INNER",
|
|
555
|
+
"INTERSECT",
|
|
556
|
+
"INTO",
|
|
557
|
+
"IS",
|
|
558
|
+
"ISNULL",
|
|
559
|
+
"JOIN",
|
|
560
|
+
"LATERAL",
|
|
561
|
+
"LEADING",
|
|
562
|
+
"LEFT",
|
|
563
|
+
"LIKE",
|
|
564
|
+
"LIMIT",
|
|
565
|
+
"LOCALTIME",
|
|
566
|
+
"LOCALTIMESTAMP",
|
|
567
|
+
"NATURAL",
|
|
568
|
+
"NOT",
|
|
569
|
+
"NOTNULL",
|
|
570
|
+
"NULL",
|
|
571
|
+
"OFFSET",
|
|
572
|
+
"ON",
|
|
573
|
+
"ONLY",
|
|
574
|
+
"OR",
|
|
575
|
+
"ORDER",
|
|
576
|
+
"OUTER",
|
|
577
|
+
"OVERLAPS",
|
|
578
|
+
"PLACING",
|
|
579
|
+
"PRIMARY",
|
|
580
|
+
"REFERENCES",
|
|
581
|
+
"RETURNING",
|
|
582
|
+
"RIGHT",
|
|
583
|
+
"ROW",
|
|
584
|
+
"SELECT",
|
|
585
|
+
"SESSION_USER",
|
|
586
|
+
"SIMILAR",
|
|
587
|
+
"SOME",
|
|
588
|
+
"SYMMETRIC",
|
|
589
|
+
"TABLE",
|
|
590
|
+
"THEN",
|
|
591
|
+
"TO",
|
|
592
|
+
"TRAILING",
|
|
593
|
+
"TRUE",
|
|
594
|
+
"UNION",
|
|
595
|
+
"UNIQUE",
|
|
596
|
+
"USING",
|
|
597
|
+
"VARIADIC",
|
|
598
|
+
"VERBOSE",
|
|
599
|
+
"WHEN",
|
|
600
|
+
"WHERE",
|
|
601
|
+
"WINDOW",
|
|
602
|
+
"WITH"
|
|
603
|
+
];
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/validation/zod/duckdb-valid-names.schemas.ts
|
|
606
|
+
const duckdbMaximumObjectNameLength = 120;
|
|
607
|
+
const duckDbObjectNameRegex = /^[a-z_]\w*$/i;
|
|
608
|
+
const duckdbReservedKeywordsSet = new Set(duckdbReservedKeywords.map((k) => k.toUpperCase()));
|
|
609
|
+
const duckTableNameSchema = zod.string().min(1).max(duckdbMaximumObjectNameLength).regex(duckDbObjectNameRegex, "Table name must start with a letter or underscore, and contain only letters, numbers and underscores").refine((value) => !duckdbReservedKeywordsSet.has(value.toUpperCase()), { error: `Value is a DuckDB reserved keyword and cannot be used as a table name` });
|
|
610
|
+
const duckTableAliasSchema = duckTableNameSchema;
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/manager/database/duck-database-manager.schemas.ts
|
|
613
|
+
const duckdbAttachOptionsSchema = zod.strictObject({
|
|
614
|
+
ACCESS_MODE: zod.optional(zod.enum([
|
|
615
|
+
"READ_ONLY",
|
|
616
|
+
"READ_WRITE",
|
|
617
|
+
"AUTOMATIC"
|
|
618
|
+
])),
|
|
619
|
+
COMPRESS: zod.optional(zod.enum(["true", "false"])),
|
|
620
|
+
TYPE: zod.optional(zod.enum(["DUCKDB", "SQLITE"])),
|
|
621
|
+
BLOCK_SIZE: zod.optional(zod.int32().min(16384).max(262144)),
|
|
622
|
+
ROW_GROUP_SIZE: zod.optional(zod.int32().positive()),
|
|
623
|
+
STORAGE_VERSION: zod.optional(zod.string().startsWith("v").regex(/^v?\d{1,4}\.\d{1,4}\.\d{1,4}$/)),
|
|
624
|
+
ENCRYPTION_KEY: zod.optional(zod.string().min(8)),
|
|
625
|
+
ENCRYPTION_CIPHER: zod.optional(zod.enum([
|
|
626
|
+
"CBC",
|
|
627
|
+
"CTR",
|
|
628
|
+
"GCM"
|
|
629
|
+
]))
|
|
630
|
+
});
|
|
631
|
+
const duckDatabaseManagerDbParamsSchema = zod.discriminatedUnion("type", [zod.strictObject({
|
|
632
|
+
type: zod.literal(":memory:"),
|
|
633
|
+
alias: duckTableAliasSchema,
|
|
634
|
+
options: zod.optional(duckdbAttachOptionsSchema)
|
|
635
|
+
}), zod.strictObject({
|
|
636
|
+
type: zod.literal("duckdb"),
|
|
637
|
+
path: zod.string().min(4).endsWith(".db"),
|
|
638
|
+
alias: duckTableAliasSchema,
|
|
639
|
+
options: zod.optional(duckdbAttachOptionsSchema)
|
|
640
|
+
})]);
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/manager/database/commands/duck-database-attach-command.ts
|
|
643
|
+
var DuckDatabaseAttachCommand = class {
|
|
644
|
+
options;
|
|
645
|
+
dbParams;
|
|
646
|
+
constructor(dbParams, options) {
|
|
647
|
+
this.dbParams = dbParams;
|
|
648
|
+
this.options = options ?? {};
|
|
649
|
+
}
|
|
650
|
+
getRawSql = () => {
|
|
651
|
+
const dbParams = this.dbParams;
|
|
652
|
+
const parts = ["ATTACH", this.options.behaviour].filter(Boolean);
|
|
653
|
+
const { type, alias } = dbParams;
|
|
654
|
+
switch (type) {
|
|
655
|
+
case ":memory:":
|
|
656
|
+
parts.push("':memory:'");
|
|
657
|
+
break;
|
|
658
|
+
case "duckdb":
|
|
659
|
+
parts.push(`'${dbParams.path}'`);
|
|
660
|
+
break;
|
|
661
|
+
default: (0, _httpx_assert.assertNever)(type);
|
|
662
|
+
}
|
|
663
|
+
if (alias !== null) parts.push("AS", `${alias}`);
|
|
664
|
+
const options = (0, _httpx_plain_object.isPlainObject)(dbParams.options) ? Object.entries(dbParams.options).map(([key, value]) => {
|
|
665
|
+
return key === "ACCESS_MODE" ? value : `${key} '${value}'`;
|
|
666
|
+
}) : [];
|
|
667
|
+
if (options.length > 0) parts.push(`(${options.join(", ")})`);
|
|
668
|
+
return parts.filter(Boolean).join(" ");
|
|
669
|
+
};
|
|
670
|
+
};
|
|
671
|
+
//#endregion
|
|
672
|
+
//#region src/manager/database/duck-database-manager.ts
|
|
673
|
+
var DuckDatabaseManager = class {
|
|
674
|
+
#conn;
|
|
675
|
+
#logger;
|
|
676
|
+
constructor(conn, params) {
|
|
677
|
+
this.#conn = conn;
|
|
678
|
+
this.#logger = params?.logger ?? sqlduckDefaultLogtapeLogger.with({ source: "DuckDatabaseManager" });
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Attach a database to the current connection
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* ```typescript
|
|
685
|
+
* const dbManager = new DuckDatabaseManager(conn);
|
|
686
|
+
* const database = dbManager.attach({
|
|
687
|
+
* type: ':memory:', // can be 'duckdb', 's3'...
|
|
688
|
+
* alias: 'mydb',
|
|
689
|
+
* options: { COMPRESS: 'true' }
|
|
690
|
+
* });
|
|
691
|
+
*
|
|
692
|
+
* console.log(database.alias); // 'mydb'
|
|
693
|
+
* ```
|
|
694
|
+
*/
|
|
695
|
+
attach = async (dbParams, options) => {
|
|
696
|
+
const params = zod.parse(duckDatabaseManagerDbParamsSchema, dbParams);
|
|
697
|
+
const rawSql = new DuckDatabaseAttachCommand(params, options).getRawSql();
|
|
698
|
+
await this.#executeRawSqlCommand(`attach(${params.alias})`, rawSql);
|
|
699
|
+
return new Database({ alias: params.alias });
|
|
700
|
+
};
|
|
701
|
+
attachOrReplace = async (dbParams) => {
|
|
702
|
+
return this.attach(dbParams, { behaviour: "OR REPLACE" });
|
|
703
|
+
};
|
|
704
|
+
attachIfNotExists = async (dbParams) => {
|
|
705
|
+
return this.attach(dbParams, { behaviour: "IF NOT EXISTS" });
|
|
706
|
+
};
|
|
707
|
+
showDatabases = async () => {
|
|
708
|
+
return await this.#executeRawSqlCommand("showDatabases()", `SHOW DATABASES`);
|
|
709
|
+
};
|
|
710
|
+
detach = async (dbAlias) => {
|
|
711
|
+
const safeAlias = zod.parse(duckTableAliasSchema, dbAlias);
|
|
712
|
+
await this.#executeRawSqlCommand(`detach(${safeAlias})`, `DETACH ${safeAlias}`);
|
|
713
|
+
return true;
|
|
714
|
+
};
|
|
715
|
+
detachIfExists = async (dbAlias) => {
|
|
716
|
+
const safeAlias = zod.parse(duckTableAliasSchema, dbAlias);
|
|
717
|
+
await this.#executeRawSqlCommand(`detachIfExists(${safeAlias})`, `DETACH IF EXISTS ${safeAlias}`);
|
|
718
|
+
return true;
|
|
719
|
+
};
|
|
720
|
+
/**
|
|
721
|
+
* The statistics recomputed by the ANALYZE statement are only used for join order optimization.
|
|
722
|
+
*
|
|
723
|
+
* It is therefore recommended to recompute these statistics for improved join orders,
|
|
724
|
+
* especially after performing large updates (inserts and/or deletes).
|
|
725
|
+
*
|
|
726
|
+
* @link https://duckdb.org/docs/stable/sql/statements/analyze
|
|
727
|
+
*/
|
|
728
|
+
analyze = async () => {
|
|
729
|
+
await this.#executeRawSqlCommand("analyze()", "ANALYZE");
|
|
730
|
+
return true;
|
|
731
|
+
};
|
|
732
|
+
checkpoint = async (dbAlias) => {
|
|
733
|
+
const safeAlias = zod.parse(duckTableAliasSchema, dbAlias);
|
|
734
|
+
await this.#executeRawSqlCommand(`checkpoint(${safeAlias})`, `CHECKPOINT ${safeAlias}`);
|
|
735
|
+
return true;
|
|
736
|
+
};
|
|
737
|
+
#executeRawSqlCommand = async (name, rawSql) => {
|
|
738
|
+
const startTime = Date.now();
|
|
739
|
+
try {
|
|
740
|
+
const result = await this.#conn.runAndReadAll(rawSql);
|
|
741
|
+
const timeMs = Math.round(Date.now() - startTime);
|
|
742
|
+
const data = result.getRowObjectsJS();
|
|
743
|
+
this.#logger.info(`DuckDatabaseManager.${name} in ${timeMs}ms`, { timeMs });
|
|
744
|
+
return data;
|
|
745
|
+
} catch (e) {
|
|
746
|
+
const msg = `DuckDatabaseManager: failed to run "${name}" - ${e?.message ?? ""}`;
|
|
747
|
+
const timeMs = Math.round(Date.now() - startTime);
|
|
748
|
+
this.#logger.error(msg, {
|
|
749
|
+
name,
|
|
750
|
+
sql: rawSql,
|
|
751
|
+
timeMs
|
|
752
|
+
});
|
|
753
|
+
throw new Error(msg, { cause: e });
|
|
754
|
+
}
|
|
755
|
+
};
|
|
443
756
|
};
|
|
444
757
|
//#endregion
|
|
758
|
+
exports.Database = Database;
|
|
759
|
+
exports.DuckDatabaseManager = DuckDatabaseManager;
|
|
445
760
|
exports.DuckMemory = DuckMemory;
|
|
446
761
|
exports.SqlDuck = SqlDuck;
|
|
447
762
|
exports.Table = Table;
|
|
763
|
+
exports.duckDatabaseManagerDbParamsSchema = duckDatabaseManagerDbParamsSchema;
|
|
764
|
+
exports.duckTableAliasSchema = duckTableAliasSchema;
|
|
765
|
+
exports.duckTableNameSchema = duckTableNameSchema;
|
|
766
|
+
exports.duckdbReservedKeywords = duckdbReservedKeywords;
|
|
767
|
+
exports.flowbladeLogtapeSqlduckConfig = flowbladeLogtapeSqlduckConfig;
|
|
448
768
|
exports.getTableCreateFromZod = getTableCreateFromZod;
|
|
769
|
+
exports.sqlduckDefaultLogtapeLogger = sqlduckDefaultLogtapeLogger;
|
|
449
770
|
exports.zodCodecs = zodCodecs;
|