@lightdash/warehouses 0.3452.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
|
@@ -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('
|
|
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, {
|
|
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('
|
|
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.
|
|
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.
|
|
33
|
+
"@lightdash/common": "0.3454.0"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/big.js": "6.2.2",
|