@flowblade/sqlduck 0.10.0 → 0.12.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 +105 -30
- package/dist/index.d.mts +90 -12
- package/dist/index.mjs +204 -29
- package/dist/types-DCqYqEsa.d.mts +80 -0
- package/dist/validation/zod/index.d.mts +37 -0
- package/dist/validation/zod/index.mjs +2 -0
- package/dist/zod-CwR_oehs.mjs +207 -0
- package/package.json +26 -26
- package/dist/index.cjs +0 -486
- package/dist/index.d.cts +0 -211
package/README.md
CHANGED
|
@@ -2,8 +2,81 @@
|
|
|
2
2
|
|
|
3
3
|
> Currently experimental
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
- 🛡️ DuckDB table creation from Zod schemas.
|
|
7
|
+
- 🧩 Easily ingest data from generators or async iterables.
|
|
8
|
+
|
|
9
|
+
|
|
5
10
|
## Quick start
|
|
6
11
|
|
|
12
|
+
### Create a database connection
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
import { DuckDBInstance } from '@duckdb/node-api';
|
|
16
|
+
DuckDBInstance.create(undefined, {
|
|
17
|
+
access_mode: 'READ_WRITE',
|
|
18
|
+
max_memory: '512M',
|
|
19
|
+
});
|
|
20
|
+
export const conn = await instance.connect();
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Append data to a database
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { SqlDuck, DuckDatabaseManager } from "@flowblade/sqlduck";
|
|
27
|
+
import * as z from "zod";
|
|
28
|
+
import { conn } from "./db.config.ts";
|
|
29
|
+
|
|
30
|
+
const dbManager = new DuckDatabaseManager(conn);
|
|
31
|
+
const database = await dbManager.attach({
|
|
32
|
+
type: 'memory', // can be 'duckdb', ...
|
|
33
|
+
alias: 'mydb',
|
|
34
|
+
options: { COMPRESS: 'false' },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const sqlDuck = new SqlDuck({ conn });
|
|
38
|
+
|
|
39
|
+
// Define a zod schema, it will be used to create the table
|
|
40
|
+
const userSchema = z.object({
|
|
41
|
+
id: z.int32().min(1).meta({ primaryKey: true }),
|
|
42
|
+
name: z.string(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Example of a datasource (can be generator, async generator, async iterable)
|
|
46
|
+
async function* getUsers(): AsyncIterableIterator<
|
|
47
|
+
z.infer<typeof userSchema>
|
|
48
|
+
> {
|
|
49
|
+
// database or api call
|
|
50
|
+
yield { id: 1, name: 'John' };
|
|
51
|
+
yield { id: 2, name: 'Jane' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create a table from the schema and the datasource
|
|
55
|
+
const result = await sqlDuck.toTable({
|
|
56
|
+
table: new Table({ name: 'user', database: database.alias }),
|
|
57
|
+
schema: userSchema, // The schema to use to create the table
|
|
58
|
+
rowStream: getUsers(), // The async iterable that yields rows
|
|
59
|
+
// 👇Optional:
|
|
60
|
+
chunkSize: 2048, // Number of rows to append when using duckdb appender. Default is 2048
|
|
61
|
+
onDataAppended: ({ timeMs, totalRows, rowsPerSecond }) => {
|
|
62
|
+
console.log(
|
|
63
|
+
`Appended ${totalRows} in time ${timeMs}ms, est: ${rowsPerSecond} rows/s`
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
// Optional table creation options
|
|
67
|
+
createOptions: {
|
|
68
|
+
create: 'CREATE_OR_REPLACE',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
console.log(`Inserted ${result.totalRows} rows in ${result.timeMs}ms`);
|
|
73
|
+
console.log(`Table created with DDL: ${result.createTableDDL}`);
|
|
74
|
+
|
|
75
|
+
const reader = await conn.runAndReadAll('select * from mydb.user');
|
|
76
|
+
const rows = reader.getRowObjectsJS();
|
|
77
|
+
// [{id: 1, name: 'John'}, {id: 2, name: 'Jane'}]]
|
|
78
|
+
```
|
|
79
|
+
|
|
7
80
|
### Create a memory table
|
|
8
81
|
|
|
9
82
|
```typescript
|
|
@@ -35,7 +108,6 @@ const result = sqlDuck.toTable({
|
|
|
35
108
|
onDataAppended: ({ total }) => {
|
|
36
109
|
console.log(`Appended ${total} rows so far`);
|
|
37
110
|
},
|
|
38
|
-
onDataAppendedBatchSize: 4096, // Call onDataAppended every 4096 rows
|
|
39
111
|
// Optional table creation options
|
|
40
112
|
createOptions: {
|
|
41
113
|
create: "CREATE_OR_REPLACE",
|
|
@@ -62,30 +134,31 @@ const queryResult = await dbDuckDbMemoryConn.query<{
|
|
|
62
134
|
RUN v4.1.1 /home/sebastien/github/flowblade/packages/sqlduck
|
|
63
135
|
|
|
64
136
|
|
|
65
|
-
✓ bench/appender.bench.ts > appender benches
|
|
66
|
-
name
|
|
67
|
-
· duckdb appender, count:
|
|
68
|
-
· duckdb appender, count:
|
|
137
|
+
✓ bench/appender.bench.ts > appender benches 4157ms
|
|
138
|
+
name hz min max mean p75 p99 p995 p999 rme samples
|
|
139
|
+
· duckdb appender memory, count: 100000, chunk size 2048 2.6950 357.08 385.05 371.06 385.05 385.05 385.05 385.05 ±47.90% 2
|
|
140
|
+
· duckdb appender file, count: 100000, chunk size 2048 1.4218 703.35 703.35 703.35 703.35 703.35 703.35 703.35 ±0.00% 1
|
|
141
|
+
· duckdb appender, count: 100000, chunk size 1024 2.5157 391.12 403.89 397.50 403.89 403.89 403.89 403.89 ±20.41% 2
|
|
69
142
|
|
|
70
|
-
✓ bench/stream.bench.ts > Bench stream
|
|
71
|
-
name
|
|
72
|
-
· rowToColumnsChunk with chunkSize 2048 (count:
|
|
73
|
-
· mapFakeRowStream with chunkSize 2048 (count:
|
|
143
|
+
✓ bench/stream.bench.ts > Bench stream 2809ms
|
|
144
|
+
name hz min max mean p75 p99 p995 p999 rme samples
|
|
145
|
+
· rowToColumnsChunk with chunkSize 2048 (count: 100000) 9.2627 87.7271 151.56 107.96 116.92 151.56 151.56 151.56 ±15.98% 10
|
|
146
|
+
· mapFakeRowStream with chunkSize 2048 (count: 100000) 7.1479 125.04 168.13 139.90 152.97 168.13 168.13 168.13 ±7.78% 10
|
|
74
147
|
|
|
75
|
-
✓ bench/table-create.bench.ts > Bench getTableCreateFromZod
|
|
148
|
+
✓ bench/table-create.bench.ts > Bench getTableCreateFromZod 614ms
|
|
76
149
|
name hz min max mean p75 p99 p995 p999 rme samples
|
|
77
|
-
· getTableCreateFromZod
|
|
150
|
+
· getTableCreateFromZod 18,899.24 0.0334 5.3351 0.0529 0.0546 0.1943 0.3087 0.7214 ±2.72% 9450
|
|
78
151
|
|
|
79
152
|
BENCH Summary
|
|
80
153
|
|
|
81
|
-
duckdb appender, count:
|
|
82
|
-
1.
|
|
154
|
+
duckdb appender memory, count: 100000, chunk size 2048 - bench/appender.bench.ts > appender benches
|
|
155
|
+
1.07x faster than duckdb appender, count: 100000, chunk size 1024
|
|
156
|
+
1.90x faster than duckdb appender file, count: 100000, chunk size 2048
|
|
83
157
|
|
|
84
|
-
rowToColumnsChunk with chunkSize 2048 (count:
|
|
85
|
-
1.
|
|
158
|
+
rowToColumnsChunk with chunkSize 2048 (count: 100000) - bench/stream.bench.ts > Bench stream
|
|
159
|
+
1.30x faster than mapFakeRowStream with chunkSize 2048 (count: 100000)
|
|
86
160
|
|
|
87
161
|
getTableCreateFromZod - bench/table-create.bench.ts > Bench getTableCreateFromZod
|
|
88
|
-
|
|
89
162
|
```
|
|
90
163
|
|
|
91
164
|
### Bun 1.3.11
|
|
@@ -94,29 +167,31 @@ const queryResult = await dbDuckDbMemoryConn.query<{
|
|
|
94
167
|
RUN v4.1.1 /home/sebastien/github/flowblade/packages/sqlduck
|
|
95
168
|
|
|
96
169
|
|
|
97
|
-
✓ bench/appender.bench.ts > appender benches
|
|
98
|
-
name
|
|
99
|
-
· duckdb appender, count:
|
|
100
|
-
· duckdb appender, count:
|
|
170
|
+
✓ bench/appender.bench.ts > appender benches 4159ms
|
|
171
|
+
name hz min max mean p75 p99 p995 p999 rme samples
|
|
172
|
+
· duckdb appender memory, count: 100000, chunk size 2048 2.6465 375.34 380.38 377.86 380.38 380.38 380.38 380.38 ±8.48% 2
|
|
173
|
+
· duckdb appender file, count: 100000, chunk size 2048 1.5016 665.98 665.98 665.98 665.98 665.98 665.98 665.98 ±0.00% 1
|
|
174
|
+
· duckdb appender, count: 100000, chunk size 1024 2.2828 413.11 463.01 438.06 463.01 463.01 463.01 463.01 ±72.39% 2
|
|
101
175
|
|
|
102
|
-
✓ bench/stream.bench.ts > Bench stream
|
|
103
|
-
name
|
|
104
|
-
· rowToColumnsChunk with chunkSize 2048 (count:
|
|
105
|
-
· mapFakeRowStream with chunkSize 2048 (count:
|
|
176
|
+
✓ bench/stream.bench.ts > Bench stream 2690ms
|
|
177
|
+
name hz min max mean p75 p99 p995 p999 rme samples
|
|
178
|
+
· rowToColumnsChunk with chunkSize 2048 (count: 100000) 9.5675 95.6610 114.11 104.52 107.75 114.11 114.11 114.11 ±3.46% 10
|
|
179
|
+
· mapFakeRowStream with chunkSize 2048 (count: 100000) 7.6895 117.83 138.26 130.05 137.51 138.26 138.26 138.26 ±4.05% 10
|
|
106
180
|
|
|
107
|
-
✓ bench/table-create.bench.ts > Bench getTableCreateFromZod
|
|
181
|
+
✓ bench/table-create.bench.ts > Bench getTableCreateFromZod 629ms
|
|
108
182
|
name hz min max mean p75 p99 p995 p999 rme samples
|
|
109
|
-
· getTableCreateFromZod
|
|
183
|
+
· getTableCreateFromZod 18,892.06 0.0281 7.5844 0.0529 0.0516 0.1893 0.2477 3.2823 ±6.26% 9447
|
|
110
184
|
|
|
111
185
|
BENCH Summary
|
|
112
186
|
|
|
113
|
-
rowToColumnsChunk with chunkSize 2048 (count:
|
|
114
|
-
1.
|
|
187
|
+
rowToColumnsChunk with chunkSize 2048 (count: 100000) - bench/stream.bench.ts > Bench stream
|
|
188
|
+
1.24x faster than mapFakeRowStream with chunkSize 2048 (count: 100000)
|
|
115
189
|
|
|
116
190
|
getTableCreateFromZod - bench/table-create.bench.ts > Bench getTableCreateFromZod
|
|
117
191
|
|
|
118
|
-
duckdb appender, count:
|
|
119
|
-
1.
|
|
192
|
+
duckdb appender memory, count: 100000, chunk size 2048 - bench/appender.bench.ts > appender benches
|
|
193
|
+
1.16x faster than duckdb appender, count: 100000, chunk size 1024
|
|
194
|
+
1.76x faster than duckdb appender file, count: 100000, chunk size 2048
|
|
120
195
|
|
|
121
196
|
```
|
|
122
197
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { n as DuckConnectionParams } from "./types-DCqYqEsa.mjs";
|
|
2
|
+
import * as _$_duckdb_node_api0 from "@duckdb/node-api";
|
|
3
3
|
import { DuckDBConnection, DuckDBType } from "@duckdb/node-api";
|
|
4
|
+
import * as _$_logtape_logtape0 from "@logtape/logtape";
|
|
5
|
+
import { Logger } from "@logtape/logtape";
|
|
4
6
|
import * as z from "zod";
|
|
5
7
|
import { ZodObject } from "zod";
|
|
6
8
|
|
|
@@ -23,11 +25,6 @@ type OnDataAppendedSyncCb = (stats: OnDataAppendedStats) => void;
|
|
|
23
25
|
type OnDataAppendedAsyncCb = (stats: OnDataAppendedStats) => Promise<void>;
|
|
24
26
|
type OnDataAppendedCb = OnDataAppendedSyncCb | OnDataAppendedAsyncCb;
|
|
25
27
|
//#endregion
|
|
26
|
-
//#region src/config/flowblade-logtape-sqlduck.config.d.ts
|
|
27
|
-
declare const flowbladeLogtapeSqlduckConfig: {
|
|
28
|
-
categories: string[];
|
|
29
|
-
};
|
|
30
|
-
//#endregion
|
|
31
28
|
//#region src/helpers/duck-memory.d.ts
|
|
32
29
|
declare const duckMemoryTags: readonly ["BASE_TABLE", "HASH_TABLE", "PARQUET_READER", "CSV_READER", "ORDER_BY", "ART_INDEX", "COLUMN_DATA", "METADATA", "OVERFLOW_STRINGS", "IN_MEMORY_TABLE", "ALLOCATOR", "EXTENSION", "TRANSACTION", "EXTERNAL_FILE_CACHE", "WINDOW", "OBJECT_CACHE"];
|
|
33
30
|
type DuckMemoryTag = (typeof duckMemoryTags)[number];
|
|
@@ -56,10 +53,7 @@ declare class DuckMemory {
|
|
|
56
53
|
getSummary: () => Promise<DuckMemorySummary>;
|
|
57
54
|
}
|
|
58
55
|
//#endregion
|
|
59
|
-
//#region src/
|
|
60
|
-
declare const sqlduckDefaultLogtapeLogger: _logtape_logtape0.Logger;
|
|
61
|
-
//#endregion
|
|
62
|
-
//#region src/table/table.d.ts
|
|
56
|
+
//#region src/objects/table.d.ts
|
|
63
57
|
/**
|
|
64
58
|
* Fully qualified table information
|
|
65
59
|
*/
|
|
@@ -144,6 +138,11 @@ type ToTableParams<TSchema extends TableSchemaZod> = {
|
|
|
144
138
|
* Callback called each time a datachunk is appended to the table
|
|
145
139
|
*/
|
|
146
140
|
onDataAppended?: OnDataAppendedCb;
|
|
141
|
+
/**
|
|
142
|
+
* Automatically checkpoint the table after all chunks have been appended.
|
|
143
|
+
* @default true
|
|
144
|
+
*/
|
|
145
|
+
autoCheckpoint?: boolean;
|
|
147
146
|
};
|
|
148
147
|
type ToTableResult = {
|
|
149
148
|
/**
|
|
@@ -208,4 +207,83 @@ declare const zodCodecs: {
|
|
|
208
207
|
readonly bigintToString: z.ZodCodec<z.ZodBigInt, z.ZodString>;
|
|
209
208
|
};
|
|
210
209
|
//#endregion
|
|
211
|
-
|
|
210
|
+
//#region src/objects/database.d.ts
|
|
211
|
+
type DatabaseProperties = {
|
|
212
|
+
alias: string;
|
|
213
|
+
};
|
|
214
|
+
declare class Database {
|
|
215
|
+
#private;
|
|
216
|
+
get alias(): string;
|
|
217
|
+
constructor(params: DatabaseProperties);
|
|
218
|
+
toJson(): {
|
|
219
|
+
type: string;
|
|
220
|
+
params: {
|
|
221
|
+
alias: string;
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
[Symbol.toStringTag](): string;
|
|
225
|
+
}
|
|
226
|
+
//#endregion
|
|
227
|
+
//#region src/validation/core/duck-reserved-keywords.d.ts
|
|
228
|
+
/**
|
|
229
|
+
* DuckDB reserved keywords that cannot be used as unquoted identifiers.
|
|
230
|
+
* @see https://duckdb.org/docs/sql/keywords-and-identifiers.html
|
|
231
|
+
*/
|
|
232
|
+
declare const duckReservedKeywords: readonly ["ALL", "ANALYSE", "ANALYZE", "AND", "ANY", "ARRAY", "AS", "ASC", "ASYMMETRIC", "BOTH", "CASE", "CAST", "CHECK", "COLLATE", "COLUMN", "CONSTRAINT", "CREATE", "CROSS", "CURRENT_CATALOG", "CURRENT_DATE", "CURRENT_ROLE", "CURRENT_SCHEMA", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_USER", "DEFAULT", "DEFERRABLE", "DESC", "DISTINCT", "DO", "ELSE", "END", "EXCEPT", "EXISTS", "EXTRACT", "FALSE", "FETCH", "FOR", "FOREIGN", "FROM", "GRANT", "GROUP", "HAVING", "IF", "ILIKE", "IN", "INITIALLY", "INNER", "INTERSECT", "INTO", "IS", "ISNULL", "JOIN", "LATERAL", "LEADING", "LEFT", "LIKE", "LIMIT", "LOCALTIME", "LOCALTIMESTAMP", "NATURAL", "NOT", "NOTNULL", "NULL", "OFFSET", "ON", "ONLY", "OR", "ORDER", "OUTER", "OVERLAPS", "PLACING", "PRIMARY", "REFERENCES", "RETURNING", "RIGHT", "ROW", "SELECT", "SESSION_USER", "SIMILAR", "SOME", "SYMMETRIC", "TABLE", "THEN", "TO", "TRAILING", "TRUE", "UNION", "UNIQUE", "USING", "VARIADIC", "VERBOSE", "WHEN", "WHERE", "WINDOW", "WITH"];
|
|
233
|
+
type DuckdbReservedKeywords = (typeof duckReservedKeywords)[number];
|
|
234
|
+
//#endregion
|
|
235
|
+
//#region src/manager/database/commands/duck-database-attach-command.d.ts
|
|
236
|
+
type Behaviour = 'OR REPLACE' | 'IF NOT EXISTS';
|
|
237
|
+
type DuckDatabaseAttachCommandOptions = {
|
|
238
|
+
behaviour?: Behaviour;
|
|
239
|
+
};
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/manager/database/duck-database-manager.d.ts
|
|
242
|
+
declare class DuckDatabaseManager {
|
|
243
|
+
#private;
|
|
244
|
+
constructor(conn: DuckDBConnection, params?: {
|
|
245
|
+
logger?: Logger;
|
|
246
|
+
});
|
|
247
|
+
/**
|
|
248
|
+
* Attach a database to the current connection
|
|
249
|
+
*
|
|
250
|
+
* @example
|
|
251
|
+
* ```typescript
|
|
252
|
+
* const dbManager = new DuckDatabaseManager(conn);
|
|
253
|
+
* const database = dbManager.attach({
|
|
254
|
+
* type: 'memory', // can be 'duckdb', 's3'...
|
|
255
|
+
* alias: 'mydb',
|
|
256
|
+
* options: { COMPRESS: 'true' }
|
|
257
|
+
* });
|
|
258
|
+
*
|
|
259
|
+
* console.log(database.alias); // 'mydb'
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
attach: (dbParams: DuckConnectionParams, options?: DuckDatabaseAttachCommandOptions) => Promise<Database>;
|
|
263
|
+
attachOrReplace: (dbParams: DuckConnectionParams) => Promise<Database>;
|
|
264
|
+
attachIfNotExists: (dbParams: DuckConnectionParams) => Promise<Database>;
|
|
265
|
+
showDatabases: () => Promise<Record<string, _$_duckdb_node_api0.JS>[]>;
|
|
266
|
+
detach: (dbAlias: string) => Promise<boolean>;
|
|
267
|
+
detachIfExists: (dbAlias: string) => Promise<boolean>;
|
|
268
|
+
/**
|
|
269
|
+
* The statistics recomputed by the ANALYZE statement are only used for join order optimization.
|
|
270
|
+
*
|
|
271
|
+
* It is therefore recommended to recompute these statistics for improved join orders,
|
|
272
|
+
* especially after performing large updates (inserts and/or deletes).
|
|
273
|
+
*
|
|
274
|
+
* @link https://duckdb.org/docs/stable/sql/statements/analyze
|
|
275
|
+
*/
|
|
276
|
+
analyze: () => Promise<boolean>;
|
|
277
|
+
checkpoint: (dbAlias: string) => Promise<boolean>;
|
|
278
|
+
vacuum: () => Promise<boolean>;
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/config/flowblade-logtape-sqlduck.config.d.ts
|
|
282
|
+
declare const flowbladeLogtapeSqlduckConfig: {
|
|
283
|
+
categories: string[];
|
|
284
|
+
};
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/logger/sqlduck-default-logtape-logger.d.ts
|
|
287
|
+
declare const sqlduckDefaultLogtapeLogger: _$_logtape_logtape0.Logger;
|
|
288
|
+
//#endregion
|
|
289
|
+
export { Database, type DuckConnectionParams, DuckDatabaseManager, DuckMemory, type DuckMemoryTag, type DuckdbReservedKeywords, type OnDataAppendedCb, type OnDataAppendedStats, SqlDuck, type SqlDuckParams, Table, type ToTableParams, duckReservedKeywords, flowbladeLogtapeSqlduckConfig, getTableCreateFromZod, sqlduckDefaultLogtapeLogger, zodCodecs };
|
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as assertValidAliasName, c as duckValidatorsZod, i as duckConnectionParamsZodSchema, l as duckReservedKeywords } from "./zod-CwR_oehs.mjs";
|
|
2
2
|
import { BIGINT, BOOLEAN, DOUBLE, DuckDBDataChunk, DuckDBTimestampValue, FLOAT, HUGEINT, INTEGER, SMALLINT, TIMESTAMP, TINYINT, UBIGINT, UHUGEINT, UINTEGER, USMALLINT, UTINYINT, UUID, VARCHAR } from "@duckdb/node-api";
|
|
3
|
+
import { getLogger } from "@logtape/logtape";
|
|
3
4
|
import * as z from "zod";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
//#endregion
|
|
5
|
+
import { assertNever } from "@httpx/assert";
|
|
6
|
+
import { isPlainObject } from "@httpx/plain-object";
|
|
7
7
|
//#region src/helpers/duck-exec.ts
|
|
8
8
|
var DuckExec = class {
|
|
9
9
|
#conn;
|
|
@@ -100,9 +100,6 @@ var DuckMemory = class {
|
|
|
100
100
|
};
|
|
101
101
|
};
|
|
102
102
|
//#endregion
|
|
103
|
-
//#region src/logger/sqlduck-default-logtape-logger.ts
|
|
104
|
-
const sqlduckDefaultLogtapeLogger = getLogger(flowbladeLogtapeSqlduckConfig.categories);
|
|
105
|
-
//#endregion
|
|
106
103
|
//#region src/appender/data-appender-callback.ts
|
|
107
104
|
const isOnDataAppendedAsyncCb = (v) => {
|
|
108
105
|
return v.constructor.name === "AsyncFunction";
|
|
@@ -124,6 +121,177 @@ const createOnDataAppendedCollector = () => {
|
|
|
124
121
|
};
|
|
125
122
|
};
|
|
126
123
|
//#endregion
|
|
124
|
+
//#region src/config/flowblade-logtape-sqlduck.config.ts
|
|
125
|
+
const flowbladeLogtapeSqlduckConfig = { categories: ["flowblade", "sqlduck"] };
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/logger/sqlduck-default-logtape-logger.ts
|
|
128
|
+
const sqlduckDefaultLogtapeLogger = getLogger(flowbladeLogtapeSqlduckConfig.categories);
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region src/objects/database.ts
|
|
131
|
+
var Database = class {
|
|
132
|
+
#params;
|
|
133
|
+
get alias() {
|
|
134
|
+
return this.#params.alias;
|
|
135
|
+
}
|
|
136
|
+
constructor(params) {
|
|
137
|
+
this.#params = params;
|
|
138
|
+
}
|
|
139
|
+
toJson() {
|
|
140
|
+
return {
|
|
141
|
+
type: "database",
|
|
142
|
+
params: { alias: this.#params.alias }
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
[Symbol.toStringTag]() {
|
|
146
|
+
return this.alias;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/manager/database/commands/duck-database-attach-command.ts
|
|
151
|
+
var DuckDatabaseAttachCommand = class {
|
|
152
|
+
options;
|
|
153
|
+
dbParams;
|
|
154
|
+
constructor(dbParams, options) {
|
|
155
|
+
this.dbParams = dbParams;
|
|
156
|
+
this.options = options ?? {};
|
|
157
|
+
}
|
|
158
|
+
getRawSql = () => {
|
|
159
|
+
const dbParams = this.dbParams;
|
|
160
|
+
const parts = ["ATTACH", this.options.behaviour].filter(Boolean);
|
|
161
|
+
const { type, alias } = dbParams;
|
|
162
|
+
switch (type) {
|
|
163
|
+
case "memory":
|
|
164
|
+
parts.push("':memory:'");
|
|
165
|
+
break;
|
|
166
|
+
case "duckdb":
|
|
167
|
+
parts.push(`'${dbParams.path}'`);
|
|
168
|
+
break;
|
|
169
|
+
default: assertNever(type);
|
|
170
|
+
}
|
|
171
|
+
if (alias !== null) parts.push("AS", `${alias}`);
|
|
172
|
+
const options = [];
|
|
173
|
+
if (isPlainObject(dbParams.options)) for (const [key, value] of Object.entries(dbParams.options)) switch (key) {
|
|
174
|
+
case "accessMode":
|
|
175
|
+
options.push(`${value}`);
|
|
176
|
+
break;
|
|
177
|
+
case "compress":
|
|
178
|
+
if (value === true) options.push("COMPRESS");
|
|
179
|
+
break;
|
|
180
|
+
case "blockSize":
|
|
181
|
+
options.push(`BLOCK_SIZE ${value}`);
|
|
182
|
+
break;
|
|
183
|
+
case "rowGroupSize":
|
|
184
|
+
options.push(`ROW_GROUP_SIZE ${value}`);
|
|
185
|
+
break;
|
|
186
|
+
case "type":
|
|
187
|
+
options.push(`TYPE ${value}`);
|
|
188
|
+
break;
|
|
189
|
+
case "storageVersion":
|
|
190
|
+
options.push(`STORAGE_VERSION '${value}'`);
|
|
191
|
+
break;
|
|
192
|
+
case "encryptionCipher":
|
|
193
|
+
options.push(`ENCRYPTION_CIPHER '${value}'`);
|
|
194
|
+
break;
|
|
195
|
+
case "encryptionKey":
|
|
196
|
+
options.push(`ENCRYPTION_KEY '${value}'`);
|
|
197
|
+
break;
|
|
198
|
+
default:
|
|
199
|
+
}
|
|
200
|
+
if (options.length > 0) parts.push(`(${options.join(", ")})`);
|
|
201
|
+
return parts.filter(Boolean).join(" ");
|
|
202
|
+
};
|
|
203
|
+
};
|
|
204
|
+
//#endregion
|
|
205
|
+
//#region src/manager/database/duck-database-manager.ts
|
|
206
|
+
var DuckDatabaseManager = class {
|
|
207
|
+
#conn;
|
|
208
|
+
#logger;
|
|
209
|
+
constructor(conn, params) {
|
|
210
|
+
this.#conn = conn;
|
|
211
|
+
this.#logger = params?.logger ?? sqlduckDefaultLogtapeLogger.with({ source: "DuckDatabaseManager" });
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Attach a database to the current connection
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```typescript
|
|
218
|
+
* const dbManager = new DuckDatabaseManager(conn);
|
|
219
|
+
* const database = dbManager.attach({
|
|
220
|
+
* type: 'memory', // can be 'duckdb', 's3'...
|
|
221
|
+
* alias: 'mydb',
|
|
222
|
+
* options: { COMPRESS: 'true' }
|
|
223
|
+
* });
|
|
224
|
+
*
|
|
225
|
+
* console.log(database.alias); // 'mydb'
|
|
226
|
+
* ```
|
|
227
|
+
*/
|
|
228
|
+
attach = async (dbParams, options) => {
|
|
229
|
+
const params = duckConnectionParamsZodSchema.parse(dbParams);
|
|
230
|
+
const rawSql = new DuckDatabaseAttachCommand(params, options).getRawSql();
|
|
231
|
+
await this.#executeRawSqlCommand(`attach(${params.alias})`, rawSql);
|
|
232
|
+
return new Database({ alias: params.alias });
|
|
233
|
+
};
|
|
234
|
+
attachOrReplace = async (dbParams) => {
|
|
235
|
+
return this.attach(dbParams, { behaviour: "OR REPLACE" });
|
|
236
|
+
};
|
|
237
|
+
attachIfNotExists = async (dbParams) => {
|
|
238
|
+
return this.attach(dbParams, { behaviour: "IF NOT EXISTS" });
|
|
239
|
+
};
|
|
240
|
+
showDatabases = async () => {
|
|
241
|
+
return await this.#executeRawSqlCommand("showDatabases()", `SHOW DATABASES`);
|
|
242
|
+
};
|
|
243
|
+
detach = async (dbAlias) => {
|
|
244
|
+
assertValidAliasName(dbAlias);
|
|
245
|
+
await this.#executeRawSqlCommand(`detach(${dbAlias})`, `DETACH ${dbAlias}`);
|
|
246
|
+
return true;
|
|
247
|
+
};
|
|
248
|
+
detachIfExists = async (dbAlias) => {
|
|
249
|
+
assertValidAliasName(dbAlias);
|
|
250
|
+
await this.#executeRawSqlCommand(`detachIfExists(${dbAlias})`, `DETACH IF EXISTS ${dbAlias}`);
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* The statistics recomputed by the ANALYZE statement are only used for join order optimization.
|
|
255
|
+
*
|
|
256
|
+
* It is therefore recommended to recompute these statistics for improved join orders,
|
|
257
|
+
* especially after performing large updates (inserts and/or deletes).
|
|
258
|
+
*
|
|
259
|
+
* @link https://duckdb.org/docs/stable/sql/statements/analyze
|
|
260
|
+
*/
|
|
261
|
+
analyze = async () => {
|
|
262
|
+
await this.#executeRawSqlCommand("analyze()", "ANALYZE");
|
|
263
|
+
return true;
|
|
264
|
+
};
|
|
265
|
+
checkpoint = async (dbAlias) => {
|
|
266
|
+
const safeAlias = duckValidatorsZod.aliasName.parse(dbAlias);
|
|
267
|
+
await this.#executeRawSqlCommand(`checkpoint(${safeAlias})`, `CHECKPOINT ${safeAlias}`);
|
|
268
|
+
return true;
|
|
269
|
+
};
|
|
270
|
+
vacuum = async () => {
|
|
271
|
+
await this.#executeRawSqlCommand("vacuum()", "VACUUM");
|
|
272
|
+
return true;
|
|
273
|
+
};
|
|
274
|
+
#executeRawSqlCommand = async (name, rawSql) => {
|
|
275
|
+
const startTime = Date.now();
|
|
276
|
+
try {
|
|
277
|
+
const result = await this.#conn.runAndReadAll(rawSql);
|
|
278
|
+
const timeMs = Math.round(Date.now() - startTime);
|
|
279
|
+
const data = result.getRowObjectsJS();
|
|
280
|
+
this.#logger.info(`DuckDatabaseManager.${name} in ${timeMs}ms`, { timeMs });
|
|
281
|
+
return data;
|
|
282
|
+
} catch (e) {
|
|
283
|
+
const msg = `DuckDatabaseManager: failed to run "${name}" - ${e?.message ?? ""}`;
|
|
284
|
+
const timeMs = Math.round(Date.now() - startTime);
|
|
285
|
+
this.#logger.error(msg, {
|
|
286
|
+
name,
|
|
287
|
+
sql: rawSql,
|
|
288
|
+
timeMs
|
|
289
|
+
});
|
|
290
|
+
throw new Error(msg, { cause: e });
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
//#endregion
|
|
127
295
|
//#region src/table/get-duckdb-number-column-type.ts
|
|
128
296
|
const isFloatValue = (value) => {
|
|
129
297
|
if (!Number.isFinite(value)) return true;
|
|
@@ -157,7 +325,7 @@ const createOptions = {
|
|
|
157
325
|
CREATE_OR_REPLACE: "CREATE OR REPLACE TABLE",
|
|
158
326
|
IF_NOT_EXISTS: "CREATE TABLE IF NOT EXISTS"
|
|
159
327
|
};
|
|
160
|
-
const
|
|
328
|
+
const duckDbTypesMap = new Map([
|
|
161
329
|
["VARCHAR", VARCHAR],
|
|
162
330
|
["BIGINT", BIGINT],
|
|
163
331
|
["TIMESTAMP", TIMESTAMP],
|
|
@@ -166,8 +334,7 @@ const duckDbTypes = [
|
|
|
166
334
|
["INTEGER", INTEGER],
|
|
167
335
|
["DOUBLE", DOUBLE],
|
|
168
336
|
["FLOAT", FLOAT]
|
|
169
|
-
];
|
|
170
|
-
const duckDbTypesMap = new Map(duckDbTypes);
|
|
337
|
+
]);
|
|
171
338
|
const getTableCreateFromZod = (params) => {
|
|
172
339
|
const { table, schema, options } = params;
|
|
173
340
|
const { create = "CREATE" } = options ?? {};
|
|
@@ -303,10 +470,10 @@ async function* rowsToColumnsChunks(params) {
|
|
|
303
470
|
//#endregion
|
|
304
471
|
//#region src/sql-duck.ts
|
|
305
472
|
var SqlDuck = class {
|
|
306
|
-
#
|
|
473
|
+
#conn;
|
|
307
474
|
#logger;
|
|
308
475
|
constructor(params) {
|
|
309
|
-
this.#
|
|
476
|
+
this.#conn = params.conn;
|
|
310
477
|
this.#logger = params.logger ?? sqlduckDefaultLogtapeLogger;
|
|
311
478
|
}
|
|
312
479
|
/**
|
|
@@ -347,16 +514,16 @@ var SqlDuck = class {
|
|
|
347
514
|
* ```
|
|
348
515
|
*/
|
|
349
516
|
toTable = async (params) => {
|
|
350
|
-
const { table, schema, chunkSize = 2048, rowStream, createOptions, onDataAppended } = params;
|
|
517
|
+
const { table, schema, chunkSize = 2048, rowStream, createOptions, onDataAppended, autoCheckpoint = true } = params;
|
|
351
518
|
if (!Number.isSafeInteger(chunkSize) || chunkSize < 1 || chunkSize > 2048) throw new Error("chunkSize must be a number between 1 and 2048");
|
|
352
519
|
const timeStart = Date.now();
|
|
353
520
|
const { columnTypes, ddl } = await createTableFromZod({
|
|
354
|
-
conn: this.#
|
|
521
|
+
conn: this.#conn,
|
|
355
522
|
schema,
|
|
356
523
|
table,
|
|
357
524
|
options: createOptions
|
|
358
525
|
});
|
|
359
|
-
const appender = await this.#
|
|
526
|
+
const appender = await this.#conn.createAppender(table.tableName, table.schemaName, table.databaseName);
|
|
360
527
|
const chunkTypes = Array.from(columnTypes.values());
|
|
361
528
|
let totalRows = 0;
|
|
362
529
|
const dataAppendedCollector = createOnDataAppendedCollector();
|
|
@@ -379,6 +546,14 @@ var SqlDuck = class {
|
|
|
379
546
|
}
|
|
380
547
|
}
|
|
381
548
|
appender.closeSync();
|
|
549
|
+
if (autoCheckpoint && typeof table.databaseName === "string") {
|
|
550
|
+
const dbManager = new DuckDatabaseManager(this.#conn);
|
|
551
|
+
try {
|
|
552
|
+
await dbManager.checkpoint(table.databaseName);
|
|
553
|
+
} catch (e) {
|
|
554
|
+
this.#logger.warning(`Failed to checkpoint database '${table.databaseName}' after appending data into table '${table.getFullName()}' - ${e?.message ?? ""}`, { table: table.getFullName() });
|
|
555
|
+
}
|
|
556
|
+
}
|
|
382
557
|
const timeMs = Math.round(Date.now() - timeStart);
|
|
383
558
|
this.#logger.info(`Successfully appended ${totalRows} rows into '${table.getFullName()}' in ${timeMs}ms`, {
|
|
384
559
|
table: table.getFullName(),
|
|
@@ -399,7 +574,19 @@ var SqlDuck = class {
|
|
|
399
574
|
};
|
|
400
575
|
};
|
|
401
576
|
//#endregion
|
|
402
|
-
//#region src/
|
|
577
|
+
//#region src/utils/zod-codecs.ts
|
|
578
|
+
const zodCodecs = {
|
|
579
|
+
dateToString: z.codec(z.date(), z.iso.datetime(), {
|
|
580
|
+
decode: (date) => date.toISOString(),
|
|
581
|
+
encode: (isoString) => new Date(isoString)
|
|
582
|
+
}),
|
|
583
|
+
bigintToString: z.codec(z.bigint(), z.string().meta({ format: "int64" }), {
|
|
584
|
+
decode: (bigint) => bigint.toString(),
|
|
585
|
+
encode: BigInt
|
|
586
|
+
})
|
|
587
|
+
};
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/objects/table.ts
|
|
403
590
|
var Table = class Table {
|
|
404
591
|
#fqTable;
|
|
405
592
|
get tableName() {
|
|
@@ -441,16 +628,4 @@ var Table = class Table {
|
|
|
441
628
|
};
|
|
442
629
|
};
|
|
443
630
|
//#endregion
|
|
444
|
-
|
|
445
|
-
const zodCodecs = {
|
|
446
|
-
dateToString: z.codec(z.date(), z.iso.datetime(), {
|
|
447
|
-
decode: (date) => date.toISOString(),
|
|
448
|
-
encode: (isoString) => new Date(isoString)
|
|
449
|
-
}),
|
|
450
|
-
bigintToString: z.codec(z.bigint(), z.string().meta({ format: "int64" }), {
|
|
451
|
-
decode: (bigint) => bigint.toString(),
|
|
452
|
-
encode: BigInt
|
|
453
|
-
})
|
|
454
|
-
};
|
|
455
|
-
//#endregion
|
|
456
|
-
export { DuckMemory, SqlDuck, Table, flowbladeLogtapeSqlduckConfig, getTableCreateFromZod, sqlduckDefaultLogtapeLogger, zodCodecs };
|
|
631
|
+
export { Database, DuckDatabaseManager, DuckMemory, SqlDuck, Table, duckReservedKeywords, flowbladeLogtapeSqlduckConfig, getTableCreateFromZod, sqlduckDefaultLogtapeLogger, zodCodecs };
|