@lightdash/warehouses 0.3453.0 → 0.3454.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/dist/.tsbuildinfo +1 -1
- package/dist/warehouseClients/DuckdbWarehouseClient.d.ts +11 -1
- package/dist/warehouseClients/DuckdbWarehouseClient.d.ts.map +1 -1
- package/dist/warehouseClients/DuckdbWarehouseClient.embedded.test.d.ts +2 -0
- package/dist/warehouseClients/DuckdbWarehouseClient.embedded.test.d.ts.map +1 -0
- package/dist/warehouseClients/DuckdbWarehouseClient.embedded.test.js +92 -0
- package/dist/warehouseClients/DuckdbWarehouseClient.js +289 -54
- package/dist/warehouseClients/DuckdbWarehouseClient.test.js +115 -4
- package/package.json +2 -2
|
@@ -7,6 +7,7 @@ exports.DuckdbWarehouseClient = exports.DuckdbSqlBuilder = exports.getDuckdbTime
|
|
|
7
7
|
const node_api_1 = require("@duckdb/node-api");
|
|
8
8
|
const common_1 = require("@lightdash/common");
|
|
9
9
|
const crypto_1 = require("crypto");
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
11
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
11
12
|
const os_1 = __importDefault(require("os"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
@@ -137,6 +138,28 @@ class AsyncSemaphore {
|
|
|
137
138
|
}
|
|
138
139
|
}
|
|
139
140
|
}
|
|
141
|
+
class ConcurrencyBudget {
|
|
142
|
+
constructor(limit) {
|
|
143
|
+
this.limit = limit;
|
|
144
|
+
this.active = 0;
|
|
145
|
+
}
|
|
146
|
+
tryAcquire() {
|
|
147
|
+
if (this.active >= this.limit) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
this.active += 1;
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
release() {
|
|
154
|
+
this.active -= 1;
|
|
155
|
+
}
|
|
156
|
+
isIdle() {
|
|
157
|
+
return this.active === 0;
|
|
158
|
+
}
|
|
159
|
+
reset() {
|
|
160
|
+
this.active = 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
140
163
|
// DuckDB StatementType values — see duckdb/common/enums/statement_type.hpp
|
|
141
164
|
const ALLOWED_STATEMENT_TYPES_USER_SQL = new Set([1 /* SELECT */]);
|
|
142
165
|
const BLOCKED_STATEMENT_TYPES_INTERNAL_SQL = new Set([
|
|
@@ -148,7 +171,7 @@ const BLOCKED_STATEMENT_TYPES_INTERNAL_SQL = new Set([
|
|
|
148
171
|
26, // DETACH
|
|
149
172
|
]);
|
|
150
173
|
const BLOCKED_FUNCTION_PATTERN = /\b(current_setting|duckdb_settings|duckdb_secrets|query|query_table)\s*\(/i;
|
|
151
|
-
const BLOCKED_USER_SQL_FILE_FUNCTION_PATTERN = /\b(
|
|
174
|
+
const BLOCKED_USER_SQL_FILE_FUNCTION_PATTERN = /\b(read\s*_\s*(?:blob|csv(?:\s*_\s*auto)?|json(?:\s*_\s*(?:auto|objects(?:\s*_\s*auto)?))?|ndjson(?:\s*_\s*(?:auto|objects(?:\s*_\s*auto)?))?|parquet|text|xlsx)|parquet\s*_\s*scan|glob|sqlite\s*_\s*scan|postgres\s*_\s*scan|mysql\s*_\s*scan)\s*\(/i;
|
|
152
175
|
const BLOCKED_USER_SQL_FILE_TABLE_PATTERN = /\b(?:from|join)\s+'[^']*'/i;
|
|
153
176
|
const buildMotherduckConnectionString = ({ database, token, }) => {
|
|
154
177
|
const params = new URLSearchParams({
|
|
@@ -157,6 +180,43 @@ const buildMotherduckConnectionString = ({ database, token, }) => {
|
|
|
157
180
|
});
|
|
158
181
|
return `md:${encodeURIComponent(database)}?${params.toString()}`;
|
|
159
182
|
};
|
|
183
|
+
const EMBEDDED_DATASET_PATTERN = /^[a-z0-9_-]+$/;
|
|
184
|
+
const DEFAULT_EMBEDDED_DATA_DIRECTORY = path_1.default.resolve(__dirname, '../../../backend/assets/playground');
|
|
185
|
+
const EMBEDDED_RESOURCE_LIMITS = {
|
|
186
|
+
memoryLimit: '256MB',
|
|
187
|
+
threads: 1,
|
|
188
|
+
};
|
|
189
|
+
const EMBEDDED_QUERY_TIMEOUT_MS = 10000;
|
|
190
|
+
const EMBEDDED_MAX_CONCURRENT_QUERIES = 2;
|
|
191
|
+
const EMBEDDED_MAX_CONCURRENT_QUERIES_PER_ORGANIZATION = 1;
|
|
192
|
+
const resolveEmbeddedDatabasePath = (dataset) => {
|
|
193
|
+
if (!EMBEDDED_DATASET_PATTERN.test(dataset)) {
|
|
194
|
+
throw new common_1.ParameterError('Embedded DuckDB dataset must contain only lowercase letters, numbers, hyphens, and underscores');
|
|
195
|
+
}
|
|
196
|
+
const baseDirectory = process.env.PLAYGROUND_DATA_DIR ?? DEFAULT_EMBEDDED_DATA_DIRECTORY;
|
|
197
|
+
let realBaseDirectory;
|
|
198
|
+
try {
|
|
199
|
+
realBaseDirectory = fs_1.default.realpathSync(baseDirectory);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
throw new common_1.ParameterError(`Embedded DuckDB dataset "${dataset}" was not found in ${baseDirectory}`);
|
|
203
|
+
}
|
|
204
|
+
const databasePath = path_1.default.join(baseDirectory, `${dataset}.duckdb`);
|
|
205
|
+
let realDatabasePath;
|
|
206
|
+
try {
|
|
207
|
+
realDatabasePath = fs_1.default.realpathSync(databasePath);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
throw new common_1.ParameterError(`Embedded DuckDB dataset "${dataset}" was not found in ${baseDirectory}`);
|
|
211
|
+
}
|
|
212
|
+
const relativePath = path_1.default.relative(realBaseDirectory, realDatabasePath);
|
|
213
|
+
if (relativePath === '' ||
|
|
214
|
+
relativePath.startsWith(`..${path_1.default.sep}`) ||
|
|
215
|
+
path_1.default.isAbsolute(relativePath)) {
|
|
216
|
+
throw new common_1.ParameterError(`Embedded DuckDB dataset "${dataset}" resolves outside ${baseDirectory}`);
|
|
217
|
+
}
|
|
218
|
+
return realDatabasePath;
|
|
219
|
+
};
|
|
160
220
|
class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
161
221
|
constructor(credentials, options) {
|
|
162
222
|
const isS3Only = credentials &&
|
|
@@ -167,6 +227,11 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
167
227
|
'type' in credentials &&
|
|
168
228
|
credentials.type === common_1.WarehouseTypes.DUCKDB &&
|
|
169
229
|
credentials.connectionType === common_1.DuckdbConnectionType.DUCKLAKE;
|
|
230
|
+
const isEmbedded = !isS3Only &&
|
|
231
|
+
credentials &&
|
|
232
|
+
'type' in credentials &&
|
|
233
|
+
credentials.type === common_1.WarehouseTypes.DUCKDB &&
|
|
234
|
+
credentials.connectionType === common_1.DuckdbConnectionType.EMBEDDED;
|
|
170
235
|
let effectiveCredentials;
|
|
171
236
|
if (isS3Only) {
|
|
172
237
|
effectiveCredentials = DUCKDB_INTERNAL_CREDENTIALS;
|
|
@@ -188,6 +253,9 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
188
253
|
dataTimezone: ducklake.dataTimezone,
|
|
189
254
|
};
|
|
190
255
|
}
|
|
256
|
+
else if (isEmbedded) {
|
|
257
|
+
effectiveCredentials = DUCKDB_INTERNAL_CREDENTIALS;
|
|
258
|
+
}
|
|
191
259
|
else {
|
|
192
260
|
effectiveCredentials =
|
|
193
261
|
credentials ??
|
|
@@ -201,9 +269,16 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
201
269
|
this.ducklakeConfig =
|
|
202
270
|
credentials;
|
|
203
271
|
}
|
|
272
|
+
if (isEmbedded) {
|
|
273
|
+
this.embeddedConfig =
|
|
274
|
+
credentials;
|
|
275
|
+
}
|
|
204
276
|
// Project DuckDB credentials map to MotherDuck only. The in-memory
|
|
205
277
|
// internal credentials remain available for pre-aggregate helper flows.
|
|
206
|
-
if (this.
|
|
278
|
+
if (this.embeddedConfig) {
|
|
279
|
+
this.databasePath = resolveEmbeddedDatabasePath(this.embeddedConfig.dataset);
|
|
280
|
+
}
|
|
281
|
+
else if (this.ducklakeConfig ||
|
|
207
282
|
effectiveCredentials.database === ':memory:') {
|
|
208
283
|
this.databasePath = ':memory:';
|
|
209
284
|
}
|
|
@@ -217,7 +292,12 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
217
292
|
token,
|
|
218
293
|
});
|
|
219
294
|
}
|
|
220
|
-
this.resourceLimits =
|
|
295
|
+
this.resourceLimits = this.embeddedConfig
|
|
296
|
+
? {
|
|
297
|
+
...EMBEDDED_RESOURCE_LIMITS,
|
|
298
|
+
...options?.resourceLimits,
|
|
299
|
+
}
|
|
300
|
+
: options?.resourceLimits;
|
|
221
301
|
this.sharedResourceLimits = options?.sharedResourceLimits;
|
|
222
302
|
// DuckLake attaches a postgres catalog secret on every fresh DuckDB
|
|
223
303
|
// instance, and the postgres extension only pools 8 connections per
|
|
@@ -231,6 +311,8 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
231
311
|
options?.instanceCacheKey ?? ducklakeAutoCacheKey;
|
|
232
312
|
this.logger = options?.logger;
|
|
233
313
|
this.onQueryProfile = options?.onQueryProfile;
|
|
314
|
+
this.embeddedQueryTimeoutMs =
|
|
315
|
+
options?.embeddedQueryTimeoutMs ?? EMBEDDED_QUERY_TIMEOUT_MS;
|
|
234
316
|
}
|
|
235
317
|
static hashDucklakeConfig(ducklake) {
|
|
236
318
|
return (0, crypto_1.createHash)('sha256')
|
|
@@ -256,19 +338,48 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
256
338
|
}
|
|
257
339
|
return this.instanceCacheKey;
|
|
258
340
|
}
|
|
341
|
+
static tryAcquireEmbeddedConcurrency(organizationUuid) {
|
|
342
|
+
if (!DuckdbWarehouseClient.embeddedConcurrencyBudget.tryAcquire()) {
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
if (!organizationUuid) {
|
|
346
|
+
return () => DuckdbWarehouseClient.embeddedConcurrencyBudget.release();
|
|
347
|
+
}
|
|
348
|
+
const organizationBudget = DuckdbWarehouseClient.embeddedOrganizationConcurrencyBudgets.get(organizationUuid) ??
|
|
349
|
+
new ConcurrencyBudget(EMBEDDED_MAX_CONCURRENT_QUERIES_PER_ORGANIZATION);
|
|
350
|
+
DuckdbWarehouseClient.embeddedOrganizationConcurrencyBudgets.set(organizationUuid, organizationBudget);
|
|
351
|
+
if (!organizationBudget.tryAcquire()) {
|
|
352
|
+
DuckdbWarehouseClient.embeddedConcurrencyBudget.release();
|
|
353
|
+
return undefined;
|
|
354
|
+
}
|
|
355
|
+
return () => {
|
|
356
|
+
organizationBudget.release();
|
|
357
|
+
DuckdbWarehouseClient.embeddedConcurrencyBudget.release();
|
|
358
|
+
if (organizationBudget.isIdle()) {
|
|
359
|
+
DuckdbWarehouseClient.embeddedOrganizationConcurrencyBudgets.delete(organizationUuid);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
259
363
|
getSQLWithMetadata(sql, tags) {
|
|
260
364
|
if (!tags) {
|
|
261
365
|
return sql;
|
|
262
366
|
}
|
|
263
367
|
return `${sql}\n-- ${JSON.stringify(tags)}`;
|
|
264
368
|
}
|
|
265
|
-
static async hardenInstance(db
|
|
369
|
+
static async hardenInstance(db) {
|
|
266
370
|
await db.run('SET allow_community_extensions = false;');
|
|
267
|
-
|
|
268
|
-
await db.run(
|
|
269
|
-
await db.run(`SET autoload_known_extensions = ${autoload};`);
|
|
371
|
+
await db.run('SET autoinstall_known_extensions = false;');
|
|
372
|
+
await db.run('SET autoload_known_extensions = false;');
|
|
270
373
|
await db.run('SET allow_unredacted_secrets = false;');
|
|
271
374
|
}
|
|
375
|
+
static async applyResourceLimits(db, resourceLimits) {
|
|
376
|
+
if (resourceLimits?.memoryLimit) {
|
|
377
|
+
await db.run(`SET memory_limit = '${resourceLimits.memoryLimit}';`);
|
|
378
|
+
}
|
|
379
|
+
if (resourceLimits?.threads) {
|
|
380
|
+
await db.run(`SET threads = ${resourceLimits.threads};`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
272
383
|
static usesS3CredentialChain(s3Config) {
|
|
273
384
|
return !(s3Config.accessKey && s3Config.secretKey);
|
|
274
385
|
}
|
|
@@ -299,15 +410,11 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
299
410
|
// exhausts on projects with >8 models.
|
|
300
411
|
await db.run('SET pg_connection_limit = 64;');
|
|
301
412
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if (client.sharedResourceLimits?.memoryLimit) {
|
|
306
|
-
await db.run(`SET memory_limit = '${client.sharedResourceLimits.memoryLimit}';`);
|
|
307
|
-
}
|
|
308
|
-
if (client.sharedResourceLimits?.threads) {
|
|
309
|
-
await db.run(`SET threads = ${client.sharedResourceLimits.threads};`);
|
|
413
|
+
if (client.ducklakeConfig) {
|
|
414
|
+
await db.run('SET autoinstall_known_extensions = true;');
|
|
415
|
+
await db.run('SET autoload_known_extensions = true;');
|
|
310
416
|
}
|
|
417
|
+
await DuckdbWarehouseClient.applyResourceLimits(db, client.sharedResourceLimits);
|
|
311
418
|
if (client.s3Config) {
|
|
312
419
|
await db.run(DuckdbWarehouseClient.buildS3SecretSql(client.s3Config));
|
|
313
420
|
}
|
|
@@ -319,6 +426,7 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
319
426
|
await db.run(stmt);
|
|
320
427
|
}
|
|
321
428
|
}
|
|
429
|
+
await DuckdbWarehouseClient.hardenInstance(db);
|
|
322
430
|
const bootstrapMs = performance.now() - bootstrapStart;
|
|
323
431
|
client.logger?.info(`DuckDB query bootstrap complete: cacheKey=${client.instanceCacheKey ?? 'none'} bootstrap=${(0, common_1.formatMilliseconds)(bootstrapMs)}ms httpfs=${(0, common_1.formatMilliseconds)(httpfsMs)}ms memory_limit=${client.sharedResourceLimits?.memoryLimit ?? 'default'} threads=${client.sharedResourceLimits?.threads ?? 'default'} s3=${client.s3Config ? 'configured' : 'none'} ducklake=${client.ducklakeConfig ? 'configured' : 'none'} shared=${client.instanceCacheKey ? 'true' : 'false'}`, {
|
|
324
432
|
shared: !!client.instanceCacheKey,
|
|
@@ -385,6 +493,7 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
385
493
|
throw error;
|
|
386
494
|
}
|
|
387
495
|
DuckdbWarehouseClient.sharedInstances.set(instanceCacheKey, instance);
|
|
496
|
+
DuckdbWarehouseClient.sharedInstanceResourceLimits.set(instanceCacheKey, client.sharedResourceLimits);
|
|
388
497
|
client.logger?.info(`DuckDB shared instance initialized: cacheKey=${instanceCacheKey} semaphore_wait=${(0, common_1.formatMilliseconds)(semaphoreWaitMs)}ms instance_create=${(0, common_1.formatMilliseconds)(instanceCreateMs)}ms bootstrap=${(0, common_1.formatMilliseconds)(bootstrapMs)}ms`, {
|
|
389
498
|
instanceCacheKey,
|
|
390
499
|
semaphoreWaitMs,
|
|
@@ -403,28 +512,38 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
403
512
|
semaphore.release();
|
|
404
513
|
}
|
|
405
514
|
}
|
|
406
|
-
static clearSharedInstance(instanceCacheKey, logger) {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
515
|
+
static async clearSharedInstance(instanceCacheKey, logger) {
|
|
516
|
+
const semaphore = DuckdbWarehouseClient.getSharedInstanceSemaphore(instanceCacheKey);
|
|
517
|
+
await semaphore.acquire();
|
|
518
|
+
try {
|
|
519
|
+
const sharedInstance = DuckdbWarehouseClient.sharedInstances.get(instanceCacheKey);
|
|
520
|
+
if (sharedInstance) {
|
|
521
|
+
try {
|
|
522
|
+
sharedInstance.closeSync?.();
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// best-effort cleanup
|
|
526
|
+
}
|
|
527
|
+
DuckdbWarehouseClient.sharedInstances.delete(instanceCacheKey);
|
|
528
|
+
DuckdbWarehouseClient.sharedInstanceResourceLimits.delete(instanceCacheKey);
|
|
529
|
+
logger?.info(`DuckDB shared instance cleared: ${instanceCacheKey}`);
|
|
414
530
|
}
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
531
|
+
}
|
|
532
|
+
finally {
|
|
533
|
+
semaphore.release();
|
|
418
534
|
}
|
|
419
535
|
}
|
|
420
536
|
/** Reset shared state without closing — for use in tests with mocked instances. */
|
|
421
537
|
static resetSharedDuckdbStateForTesting() {
|
|
422
538
|
DuckdbWarehouseClient.sharedInstances.clear();
|
|
539
|
+
DuckdbWarehouseClient.sharedInstanceResourceLimits.clear();
|
|
423
540
|
DuckdbWarehouseClient.sharedInstanceSemaphores.clear();
|
|
541
|
+
DuckdbWarehouseClient.embeddedConcurrencyBudget.reset();
|
|
542
|
+
DuckdbWarehouseClient.embeddedOrganizationConcurrencyBudgets.clear();
|
|
424
543
|
}
|
|
425
544
|
async close() {
|
|
426
545
|
if (this.instanceCacheKey) {
|
|
427
|
-
DuckdbWarehouseClient.clearSharedInstance(this.instanceCacheKey, this.logger);
|
|
546
|
+
await DuckdbWarehouseClient.clearSharedInstance(this.instanceCacheKey, this.logger);
|
|
428
547
|
}
|
|
429
548
|
}
|
|
430
549
|
static escapeDuckdbString(v) {
|
|
@@ -659,7 +778,7 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
659
778
|
this.logger?.info('DuckDB connect retries exhausted, recreating shared instance', {
|
|
660
779
|
instanceCacheKey,
|
|
661
780
|
});
|
|
662
|
-
DuckdbWarehouseClient.clearSharedInstance(instanceCacheKey, this.logger);
|
|
781
|
+
await DuckdbWarehouseClient.clearSharedInstance(instanceCacheKey, this.logger);
|
|
663
782
|
sharedInstanceAcquisition =
|
|
664
783
|
await DuckdbWarehouseClient.getOrCreateSharedInstance(this);
|
|
665
784
|
semaphoreWaitMs += sharedInstanceAcquisition.semaphoreWaitMs;
|
|
@@ -685,19 +804,15 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
685
804
|
await db.run('LOAD httpfs;');
|
|
686
805
|
await DuckdbWarehouseClient.loadAwsExtensionForCredentialChain(db, this.s3Config);
|
|
687
806
|
}
|
|
688
|
-
await DuckdbWarehouseClient.hardenInstance(db, {
|
|
689
|
-
allowKnownExtensionAutoload: !!this.ducklakeConfig,
|
|
690
|
-
});
|
|
691
807
|
await db.run(`SET temp_directory = '${tempDir}';`);
|
|
692
808
|
if (this.ducklakeConfig) {
|
|
693
809
|
await db.run('SET pg_connection_limit = 64;');
|
|
694
810
|
}
|
|
695
|
-
if (this.
|
|
696
|
-
await db.run(
|
|
697
|
-
|
|
698
|
-
if (this.resourceLimits?.threads) {
|
|
699
|
-
await db.run(`SET threads = ${this.resourceLimits.threads};`);
|
|
811
|
+
if (this.ducklakeConfig) {
|
|
812
|
+
await db.run('SET autoinstall_known_extensions = true;');
|
|
813
|
+
await db.run('SET autoload_known_extensions = true;');
|
|
700
814
|
}
|
|
815
|
+
await DuckdbWarehouseClient.applyResourceLimits(db, this.resourceLimits);
|
|
701
816
|
if (this.s3Config) {
|
|
702
817
|
await db.run(DuckdbWarehouseClient.buildS3SecretSql(this.s3Config));
|
|
703
818
|
}
|
|
@@ -709,6 +824,7 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
709
824
|
await db.run(stmt);
|
|
710
825
|
}
|
|
711
826
|
}
|
|
827
|
+
await DuckdbWarehouseClient.hardenInstance(db);
|
|
712
828
|
this.logger?.info(`DuckDB isolated bootstrap: memory_limit=${this.resourceLimits?.memoryLimit ?? 'default'} threads=${this.resourceLimits?.threads ?? 'default'} s3=${this.s3Config ? 'configured' : 'none'} ducklake=${this.ducklakeConfig ? 'configured' : 'none'}`);
|
|
713
829
|
}
|
|
714
830
|
/** Ephemeral DuckDB instance with resource limits (e.g. parquet conversion). */
|
|
@@ -779,6 +895,8 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
779
895
|
const sessionStart = performance.now();
|
|
780
896
|
const sharedConnection = await this.connectWithRetry();
|
|
781
897
|
try {
|
|
898
|
+
await DuckdbWarehouseClient.applyResourceLimits(sharedConnection.connection, DuckdbWarehouseClient.sharedInstanceResourceLimits.get(this.getRequiredInstanceCacheKey()));
|
|
899
|
+
await DuckdbWarehouseClient.hardenInstance(sharedConnection.connection);
|
|
782
900
|
const queryStart = performance.now();
|
|
783
901
|
const result = await callback(sharedConnection.connection);
|
|
784
902
|
const queryMs = performance.now() - queryStart;
|
|
@@ -810,9 +928,9 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
810
928
|
* - Shared instance (has instanceCacheKey): warm cached instance for queries
|
|
811
929
|
* - Default: ephemeral query session
|
|
812
930
|
*/
|
|
813
|
-
async withSession(callback) {
|
|
931
|
+
async withSession(callback, organizationUuid) {
|
|
814
932
|
if (this.databasePath !== ':memory:') {
|
|
815
|
-
return this.withDirectSession(callback);
|
|
933
|
+
return this.withDirectSession(callback, organizationUuid);
|
|
816
934
|
}
|
|
817
935
|
if (this.hasResourceLimits()) {
|
|
818
936
|
return this.withIsolatedSession(callback);
|
|
@@ -823,17 +941,38 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
823
941
|
return this.withEphemeralQuerySession(callback);
|
|
824
942
|
}
|
|
825
943
|
/** Direct connection to the configured MotherDuck database. */
|
|
826
|
-
async withDirectSession(callback) {
|
|
944
|
+
async withDirectSession(callback, organizationUuid) {
|
|
827
945
|
const sessionStart = performance.now();
|
|
946
|
+
const releaseEmbeddedConcurrency = this.embeddedConfig
|
|
947
|
+
? DuckdbWarehouseClient.tryAcquireEmbeddedConcurrency(organizationUuid)
|
|
948
|
+
: undefined;
|
|
949
|
+
if (this.embeddedConfig && !releaseEmbeddedConcurrency) {
|
|
950
|
+
throw new common_1.WarehouseQueryError('Playground query capacity is full. Try again shortly.');
|
|
951
|
+
}
|
|
828
952
|
const instanceCreateStart = performance.now();
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
const connectStart = performance.now();
|
|
832
|
-
const connection = await instance.connect();
|
|
833
|
-
const connectMs = performance.now() - connectStart;
|
|
953
|
+
let instance;
|
|
954
|
+
let connection;
|
|
834
955
|
try {
|
|
956
|
+
instance = this.embeddedConfig
|
|
957
|
+
? await node_api_1.DuckDBInstance.create(this.databasePath, {
|
|
958
|
+
access_mode: 'READ_ONLY',
|
|
959
|
+
memory_limit: this.resourceLimits?.memoryLimit ??
|
|
960
|
+
EMBEDDED_RESOURCE_LIMITS.memoryLimit,
|
|
961
|
+
threads: String(this.resourceLimits?.threads ??
|
|
962
|
+
EMBEDDED_RESOURCE_LIMITS.threads),
|
|
963
|
+
})
|
|
964
|
+
: await node_api_1.DuckDBInstance.create(this.databasePath);
|
|
965
|
+
const instanceCreateMs = performance.now() - instanceCreateStart;
|
|
966
|
+
const connectStart = performance.now();
|
|
967
|
+
connection = await instance.connect();
|
|
968
|
+
const connectMs = performance.now() - connectStart;
|
|
969
|
+
if (this.embeddedConfig) {
|
|
970
|
+
await DuckdbWarehouseClient.hardenInstance(connection);
|
|
971
|
+
}
|
|
835
972
|
const queryStart = performance.now();
|
|
836
|
-
const result =
|
|
973
|
+
const result = this.embeddedConfig
|
|
974
|
+
? await this.withEmbeddedQueryDeadline(connection, callback)
|
|
975
|
+
: await callback(connection);
|
|
837
976
|
const queryMs = performance.now() - queryStart;
|
|
838
977
|
const totalMs = performance.now() - sessionStart;
|
|
839
978
|
const sanitizedPath = this.databasePath.replace(/motherduck_token=[^&]*/, 'motherduck_token=***');
|
|
@@ -846,10 +985,38 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
846
985
|
return result;
|
|
847
986
|
}
|
|
848
987
|
finally {
|
|
849
|
-
connection
|
|
850
|
-
connection
|
|
851
|
-
instance
|
|
988
|
+
connection?.closeSync?.();
|
|
989
|
+
connection?.disconnectSync?.();
|
|
990
|
+
instance?.closeSync?.();
|
|
991
|
+
releaseEmbeddedConcurrency?.();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
async withEmbeddedQueryDeadline(connection, callback) {
|
|
995
|
+
let deadlineExceeded = false;
|
|
996
|
+
const timeout = setTimeout(() => {
|
|
997
|
+
deadlineExceeded = true;
|
|
998
|
+
connection.interrupt();
|
|
999
|
+
}, this.embeddedQueryTimeoutMs);
|
|
1000
|
+
timeout.unref();
|
|
1001
|
+
try {
|
|
1002
|
+
const result = await callback(connection);
|
|
1003
|
+
if (deadlineExceeded) {
|
|
1004
|
+
throw this.getEmbeddedQueryTimeoutError();
|
|
1005
|
+
}
|
|
1006
|
+
return result;
|
|
1007
|
+
}
|
|
1008
|
+
catch (error) {
|
|
1009
|
+
if (deadlineExceeded) {
|
|
1010
|
+
throw this.getEmbeddedQueryTimeoutError();
|
|
1011
|
+
}
|
|
1012
|
+
throw error;
|
|
852
1013
|
}
|
|
1014
|
+
finally {
|
|
1015
|
+
clearTimeout(timeout);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
getEmbeddedQueryTimeoutError() {
|
|
1019
|
+
return new common_1.WarehouseQueryError(`Playground query exceeded the ${this.embeddedQueryTimeoutMs / 1000} second execution limit and was stopped. Try a simpler query.`, { timeoutMs: this.embeddedQueryTimeoutMs });
|
|
853
1020
|
}
|
|
854
1021
|
getBindValues(options) {
|
|
855
1022
|
const hasValues = !!options?.values && options.values.length > 0;
|
|
@@ -947,9 +1114,70 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
947
1114
|
return fields;
|
|
948
1115
|
}
|
|
949
1116
|
static stripSqlComments(sql) {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1117
|
+
let result = '';
|
|
1118
|
+
let index = 0;
|
|
1119
|
+
let quote = null;
|
|
1120
|
+
while (index < sql.length) {
|
|
1121
|
+
const current = sql[index];
|
|
1122
|
+
const next = sql[index + 1];
|
|
1123
|
+
if (quote) {
|
|
1124
|
+
result += current;
|
|
1125
|
+
if (quote === '$$') {
|
|
1126
|
+
if (current === '$' && next === '$') {
|
|
1127
|
+
result += next;
|
|
1128
|
+
index += 2;
|
|
1129
|
+
quote = null;
|
|
1130
|
+
}
|
|
1131
|
+
else {
|
|
1132
|
+
index += 1;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
else if (current === quote) {
|
|
1136
|
+
if (next === quote) {
|
|
1137
|
+
result += next;
|
|
1138
|
+
index += 2;
|
|
1139
|
+
}
|
|
1140
|
+
else {
|
|
1141
|
+
quote = null;
|
|
1142
|
+
index += 1;
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
index += 1;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
else if (current === "'" || current === '"') {
|
|
1150
|
+
quote = current;
|
|
1151
|
+
result += current;
|
|
1152
|
+
index += 1;
|
|
1153
|
+
}
|
|
1154
|
+
else if (current === '$' && next === '$') {
|
|
1155
|
+
quote = '$$';
|
|
1156
|
+
result += '$$';
|
|
1157
|
+
index += 2;
|
|
1158
|
+
}
|
|
1159
|
+
else if (current === '-' && next === '-') {
|
|
1160
|
+
result += ' ';
|
|
1161
|
+
index += 2;
|
|
1162
|
+
while (index < sql.length && sql[index] !== '\n') {
|
|
1163
|
+
index += 1;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
else if (current === '/' && next === '*') {
|
|
1167
|
+
result += ' ';
|
|
1168
|
+
index += 2;
|
|
1169
|
+
while (index < sql.length &&
|
|
1170
|
+
!(sql[index] === '*' && sql[index + 1] === '/')) {
|
|
1171
|
+
index += 1;
|
|
1172
|
+
}
|
|
1173
|
+
index = Math.min(index + 2, sql.length);
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
result += current;
|
|
1177
|
+
index += 1;
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
return result;
|
|
953
1181
|
}
|
|
954
1182
|
static validateSqlFunctions(sql) {
|
|
955
1183
|
const stripped = DuckdbWarehouseClient.stripSqlComments(sql);
|
|
@@ -962,7 +1190,8 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
962
1190
|
const stripped = DuckdbWarehouseClient.stripSqlComments(sql);
|
|
963
1191
|
const functionMatch = stripped.match(BLOCKED_USER_SQL_FILE_FUNCTION_PATTERN);
|
|
964
1192
|
if (functionMatch) {
|
|
965
|
-
|
|
1193
|
+
const functionName = functionMatch[1].replace(/\s+/g, '');
|
|
1194
|
+
throw new Error(`SQL validation error: function '${functionName}' is not allowed`);
|
|
966
1195
|
}
|
|
967
1196
|
if (BLOCKED_USER_SQL_FILE_TABLE_PATTERN.test(stripped)) {
|
|
968
1197
|
throw new Error('SQL validation error: file table paths are not allowed');
|
|
@@ -1014,13 +1243,16 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
1014
1243
|
if (options?.timezone) {
|
|
1015
1244
|
await db.run(`SET TimeZone = '${this.escapeString(options.timezone)}';`);
|
|
1016
1245
|
}
|
|
1017
|
-
const profilePath = this.logger
|
|
1246
|
+
const profilePath = this.logger && !this.embeddedConfig
|
|
1018
1247
|
? path_1.default.join(os_1.default.tmpdir(), `duckdb-profile-${Date.now()}-${Math.random().toString(36).slice(2)}.json`)
|
|
1019
1248
|
: undefined;
|
|
1020
1249
|
if (profilePath) {
|
|
1021
1250
|
await db.run("PRAGMA enable_profiling='json';");
|
|
1022
1251
|
await db.run(`PRAGMA profiling_output='${profilePath}';`);
|
|
1023
1252
|
}
|
|
1253
|
+
if (this.embeddedConfig) {
|
|
1254
|
+
await db.run("SET disabled_filesystems = 'LocalFileSystem';");
|
|
1255
|
+
}
|
|
1024
1256
|
await this.validateUserSql(db, sql);
|
|
1025
1257
|
reportPhase?.('session', performance.now() - sessionStart);
|
|
1026
1258
|
const queryStart = performance.now();
|
|
@@ -1045,7 +1277,7 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
1045
1277
|
if (profilePath) {
|
|
1046
1278
|
await this.logQueryProfile(profilePath, this.logger, options?.tags);
|
|
1047
1279
|
}
|
|
1048
|
-
});
|
|
1280
|
+
}, options?.tags?.organization_uuid);
|
|
1049
1281
|
}
|
|
1050
1282
|
async executeAsyncQuery(...args) {
|
|
1051
1283
|
const [queryArgs, resultsStreamCallback] = args;
|
|
@@ -1160,7 +1392,10 @@ class DuckdbWarehouseClient extends WarehouseBaseClient_1.default {
|
|
|
1160
1392
|
}
|
|
1161
1393
|
exports.DuckdbWarehouseClient = DuckdbWarehouseClient;
|
|
1162
1394
|
DuckdbWarehouseClient.sharedInstances = new Map();
|
|
1395
|
+
DuckdbWarehouseClient.sharedInstanceResourceLimits = new Map();
|
|
1163
1396
|
DuckdbWarehouseClient.sharedInstanceSemaphores = new Map();
|
|
1397
|
+
DuckdbWarehouseClient.embeddedConcurrencyBudget = new ConcurrencyBudget(EMBEDDED_MAX_CONCURRENT_QUERIES);
|
|
1398
|
+
DuckdbWarehouseClient.embeddedOrganizationConcurrencyBudgets = new Map();
|
|
1164
1399
|
DuckdbWarehouseClient.sqlBuilder = new DuckdbSqlBuilder();
|
|
1165
1400
|
DuckdbWarehouseClient.DUCKLAKE_CATALOG_SECRET = '__lightdash_ducklake_catalog';
|
|
1166
1401
|
DuckdbWarehouseClient.DUCKLAKE_DATA_SECRET = '__lightdash_ducklake_data';
|