@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.
@@ -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(read_(?:blob|csv(?:_auto)?|json(?:_auto|_objects(?:_auto)?)?|ndjson(?:_auto|_objects(?:_auto)?)?|parquet|text|xlsx))\s*\(/i;
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.ducklakeConfig ||
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 = options?.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, options) {
369
+ static async hardenInstance(db) {
266
370
  await db.run('SET allow_community_extensions = false;');
267
- const autoload = options?.allowKnownExtensionAutoload ?? false;
268
- await db.run(`SET autoinstall_known_extensions = ${autoload};`);
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
- await DuckdbWarehouseClient.hardenInstance(db, {
303
- allowKnownExtensionAutoload: !!client.ducklakeConfig,
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 sharedInstance = DuckdbWarehouseClient.sharedInstances.get(instanceCacheKey);
408
- if (sharedInstance) {
409
- try {
410
- sharedInstance.closeSync?.();
411
- }
412
- catch {
413
- // best-effort cleanup
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
- DuckdbWarehouseClient.sharedInstances.delete(instanceCacheKey);
416
- DuckdbWarehouseClient.sharedInstanceSemaphores.delete(instanceCacheKey);
417
- logger?.info(`DuckDB shared instance cleared: ${instanceCacheKey}`);
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.resourceLimits?.memoryLimit) {
696
- await db.run(`SET memory_limit = '${this.resourceLimits.memoryLimit}';`);
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
- const instance = await node_api_1.DuckDBInstance.create(this.databasePath);
830
- const instanceCreateMs = performance.now() - instanceCreateStart;
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 = await callback(connection);
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.closeSync?.();
850
- connection.disconnectSync?.();
851
- instance.closeSync?.();
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
- return sql
951
- .replace(/--[^\n]*/g, '') // line comments
952
- .replace(/\/\*[\s\S]*?\*\//g, ''); // block comments
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
- throw new Error(`SQL validation error: function '${functionMatch[1]}' is not allowed`);
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';