@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.
@@ -91,6 +91,7 @@ const createMockConnection = (streamMock, runMock = vi.fn(), opts) => ({
91
91
  run: runMock,
92
92
  stream: streamMock,
93
93
  extractStatements: opts?.extractStatements ?? createMockExtractStatements(),
94
+ interrupt: opts?.interrupt ?? vi.fn(),
94
95
  closeSync: vi.fn(),
95
96
  disconnectSync: vi.fn(),
96
97
  }),
@@ -340,7 +341,99 @@ describe('DuckdbWarehouseClient', () => {
340
341
  expect(runMock).not.toHaveBeenCalledWith("SET memory_limit = '6GB';");
341
342
  expect(runMock).not.toHaveBeenCalledWith('SET threads = 8;');
342
343
  });
343
- it('should log structured DuckDB profile metrics with query tags', async () => {
344
+ it('hardens each query connection on a shared instance', async () => {
345
+ const bootstrapRunMock = vi.fn();
346
+ const queryRunMock = vi.fn();
347
+ const streamMock = vi.fn(async () => getMockStreamResult([[{ val: 1 }]], [DUCKDB_TYPE_IDS.INTEGER]));
348
+ const connectMock = vi
349
+ .fn()
350
+ .mockResolvedValueOnce({
351
+ run: bootstrapRunMock,
352
+ closeSync: vi.fn(),
353
+ disconnectSync: vi.fn(),
354
+ })
355
+ .mockResolvedValueOnce({
356
+ run: queryRunMock,
357
+ stream: streamMock,
358
+ extractStatements: createMockExtractStatements(),
359
+ closeSync: vi.fn(),
360
+ disconnectSync: vi.fn(),
361
+ });
362
+ createInstanceMock.mockResolvedValue({
363
+ connect: connectMock,
364
+ closeSync: vi.fn(),
365
+ });
366
+ const client = new DuckdbWarehouseClient_1.DuckdbWarehouseClient(undefined, {
367
+ instanceCacheKey: 'shared-query-hardening',
368
+ sharedResourceLimits: { memoryLimit: '256MB', threads: 1 },
369
+ });
370
+ await client.runQuery('SELECT 1 AS val');
371
+ expect(queryRunMock.mock.calls.map(([sql]) => sql)).toEqual([
372
+ "SET memory_limit = '256MB';",
373
+ 'SET threads = 1;',
374
+ 'SET allow_community_extensions = false;',
375
+ 'SET autoinstall_known_extensions = false;',
376
+ 'SET autoload_known_extensions = false;',
377
+ 'SET allow_unredacted_secrets = false;',
378
+ ]);
379
+ });
380
+ it('confines embedded user queries without writing profiles to local files', async () => {
381
+ const runMock = vi.fn();
382
+ const streamMock = vi.fn(async () => getMockStreamResult([[{ val: 1 }]], [DUCKDB_TYPE_IDS.INTEGER]));
383
+ const logger = { info: vi.fn() };
384
+ createInstanceMock.mockResolvedValue(createMockConnection(streamMock, runMock));
385
+ const client = new DuckdbWarehouseClient_1.DuckdbWarehouseClient({
386
+ type: common_1.WarehouseTypes.DUCKDB,
387
+ connectionType: common_1.DuckdbConnectionType.EMBEDDED,
388
+ dataset: 'jaffle_shop',
389
+ }, { logger });
390
+ await client.runQuery('SELECT 1 AS val');
391
+ expect(createInstanceMock).toHaveBeenCalledWith(expect.stringContaining('jaffle_shop.duckdb'), {
392
+ access_mode: 'READ_ONLY',
393
+ memory_limit: '256MB',
394
+ threads: '1',
395
+ });
396
+ expect(runMock).toHaveBeenCalledWith("SET disabled_filesystems = 'LocalFileSystem';");
397
+ expect(runMock).not.toHaveBeenCalledWith("PRAGMA enable_profiling='json';");
398
+ });
399
+ it('rejects embedded queries beyond the process concurrency budget', async () => {
400
+ const pendingStreams = [];
401
+ const streamMock = vi.fn(() => new Promise((resolve) => {
402
+ pendingStreams.push(resolve);
403
+ }));
404
+ createInstanceMock.mockImplementation(async () => createMockConnection(streamMock));
405
+ const createClient = () => new DuckdbWarehouseClient_1.DuckdbWarehouseClient({
406
+ type: common_1.WarehouseTypes.DUCKDB,
407
+ connectionType: common_1.DuckdbConnectionType.EMBEDDED,
408
+ dataset: 'jaffle_shop',
409
+ });
410
+ const firstQuery = createClient().runQuery('SELECT 1 AS val');
411
+ const secondQuery = createClient().runQuery('SELECT 2 AS val');
412
+ await vi.waitFor(() => expect(streamMock).toHaveBeenCalledTimes(2));
413
+ await expect(createClient().runQuery('SELECT 3 AS val')).rejects.toThrow('Playground query capacity is full. Try again shortly.');
414
+ expect(createInstanceMock).toHaveBeenCalledTimes(2);
415
+ pendingStreams.forEach((resolve) => resolve(getMockStreamResult([[{ val: 1 }]], [DUCKDB_TYPE_IDS.INTEGER])));
416
+ await Promise.all([firstQuery, secondQuery]);
417
+ });
418
+ it('rejects concurrent embedded queries for the same organization', async () => {
419
+ let resolveStream = () => { };
420
+ const streamMock = vi.fn(() => new Promise((resolve) => {
421
+ resolveStream = resolve;
422
+ }));
423
+ createInstanceMock.mockImplementation(async () => createMockConnection(streamMock));
424
+ const createClient = () => new DuckdbWarehouseClient_1.DuckdbWarehouseClient({
425
+ type: common_1.WarehouseTypes.DUCKDB,
426
+ connectionType: common_1.DuckdbConnectionType.EMBEDDED,
427
+ dataset: 'jaffle_shop',
428
+ });
429
+ const tags = { organization_uuid: 'organization-1' };
430
+ const firstQuery = createClient().runQuery('SELECT 1 AS val', tags);
431
+ await vi.waitFor(() => expect(streamMock).toHaveBeenCalledOnce());
432
+ await expect(createClient().runQuery('SELECT 2 AS val', tags)).rejects.toThrow('Playground query capacity is full. Try again shortly.');
433
+ resolveStream(getMockStreamResult([[{ val: 1 }]], [DUCKDB_TYPE_IDS.INTEGER]));
434
+ await firstQuery;
435
+ });
436
+ it('logs structured profiles for non-embedded queries and reports metrics', async () => {
344
437
  const runMock = vi.fn(async (sql) => {
345
438
  const match = sql.match(/^PRAGMA profiling_output='(.+)';$/);
346
439
  if (match) {
@@ -362,8 +455,12 @@ describe('DuckdbWarehouseClient', () => {
362
455
  });
363
456
  const streamMock = vi.fn(async () => getMockStreamResult([[{ val: 1 }]], [DUCKDB_TYPE_IDS.INTEGER]));
364
457
  const logger = { info: vi.fn() };
458
+ const onQueryProfile = vi.fn();
365
459
  createInstanceMock.mockResolvedValue(createMockConnection(streamMock, runMock));
366
- const client = new DuckdbWarehouseClient_1.DuckdbWarehouseClient(undefined, { logger });
460
+ const client = new DuckdbWarehouseClient_1.DuckdbWarehouseClient(undefined, {
461
+ logger,
462
+ onQueryProfile,
463
+ });
367
464
  await client.runQuery('SELECT 1 AS val', {
368
465
  query_uuid: 'query-123',
369
466
  chart_uuid: 'chart-123',
@@ -384,8 +481,14 @@ describe('DuckdbWarehouseClient', () => {
384
481
  bytesRead: 20225287,
385
482
  scanAmplification: 9905024 / 68,
386
483
  }));
484
+ expect(onQueryProfile).toHaveBeenCalledWith(expect.objectContaining({
485
+ latencyMs: 4747,
486
+ readParquetMs: 4632,
487
+ bytesRead: 20225287,
488
+ }));
489
+ expect(runMock).not.toHaveBeenCalledWith("SET disabled_filesystems = 'LocalFileSystem';");
387
490
  });
388
- it('should log raw profile timings when DuckDB reports cpu above latency', async () => {
491
+ it('logs raw profile timings when DuckDB reports cpu above latency', async () => {
389
492
  const runMock = vi.fn(async (sql) => {
390
493
  const match = sql.match(/^PRAGMA profiling_output='(.+)';$/);
391
494
  if (match) {
@@ -479,6 +582,11 @@ describe('DuckdbWarehouseClient', () => {
479
582
  'read_text',
480
583
  'read_blob',
481
584
  'read_xlsx',
585
+ 'parquet_scan',
586
+ 'glob',
587
+ 'sqlite_scan',
588
+ 'postgres_scan',
589
+ 'mysql_scan',
482
590
  ])('should reject user queries with %s()', async (blockedFunction) => {
483
591
  const streamMock = vi.fn(async () => getMockStreamResult([[{ val: 1 }]], [DUCKDB_TYPE_IDS.INTEGER]));
484
592
  const extractStatementsMock = createMockExtractStatements();
@@ -706,10 +814,11 @@ describe('DuckdbWarehouseClient', () => {
706
814
  // No explicit INSTALL/LOAD in DuckLake mode — rely on autoload.
707
815
  expect(joined).not.toMatch(/INSTALL httpfs/);
708
816
  expect(joined).not.toMatch(/LOAD httpfs/);
709
- // Hardening flips autoload to TRUE for DuckLake.
710
817
  expect(stmts).toEqual(expect.arrayContaining([
711
818
  'SET autoinstall_known_extensions = true;',
712
819
  'SET autoload_known_extensions = true;',
820
+ 'SET autoinstall_known_extensions = false;',
821
+ 'SET autoload_known_extensions = false;',
713
822
  'SET allow_community_extensions = false;',
714
823
  'SET allow_unredacted_secrets = false;',
715
824
  ]));
@@ -721,6 +830,8 @@ describe('DuckdbWarehouseClient', () => {
721
830
  expect(dataIdx).toBeGreaterThan(catalogIdx);
722
831
  expect(duckLakeSecretIdx).toBeGreaterThan(dataIdx);
723
832
  expect(attachIdx).toBeGreaterThan(duckLakeSecretIdx);
833
+ expect(stmts.lastIndexOf('SET autoinstall_known_extensions = false;')).toBeGreaterThan(attachIdx);
834
+ expect(stmts.lastIndexOf('SET autoload_known_extensions = false;')).toBeGreaterThan(attachIdx);
724
835
  expect(stmts[catalogIdx]).toMatch(/TYPE postgres/);
725
836
  expect(stmts[catalogIdx]).toMatch(/HOST 'pg.example.com'/);
726
837
  expect(stmts[catalogIdx]).toMatch(/PASSWORD 'p@ss'/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/warehouses",
3
- "version": "0.3453.0",
3
+ "version": "0.3454.0",
4
4
  "description": "Warehouse connectors for Lightdash",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "snowflake-sdk": "2.3.4",
31
31
  "ssh2": "1.14.0",
32
32
  "trino-client": "0.2.9",
33
- "@lightdash/common": "0.3453.0"
33
+ "@lightdash/common": "0.3454.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/big.js": "6.2.2",