diff --git a/packages/context/src/connections/connection-type.ts b/packages/context/src/connections/connection-type.ts index 6cd48042..d122e950 100644 --- a/packages/context/src/connections/connection-type.ts +++ b/packages/context/src/connections/connection-type.ts @@ -3,6 +3,7 @@ import { z } from 'zod'; export const connectionTypeSchema = z.enum([ 'POSTGRESQL', 'SQLITE', + 'DUCKDB', 'SQLSERVER', 'BIGQUERY', 'SNOWFLAKE', diff --git a/packages/context/src/connections/dialects.test.ts b/packages/context/src/connections/dialects.test.ts index 6c9b6c41..c2bc2d7b 100644 --- a/packages/context/src/connections/dialects.test.ts +++ b/packages/context/src/connections/dialects.test.ts @@ -8,6 +8,7 @@ describe('getDialectForDriver', () => { ['mysql', '`public`.`orders`'], ['clickhouse', '`public`.`orders`'], ['sqlite', '"orders"'], + ['duckdb', '"public"."orders"'], ['snowflake', '"analytics"."public"."orders"'], ['bigquery', '`analytics`.`public`.`orders`'], ['sqlserver', '[analytics].[public].[orders]'], @@ -24,7 +25,7 @@ describe('getDialectForDriver', () => { it('throws with a supported-driver list for unknown drivers', () => { expect(() => getDialectForDriver('oracle')).toThrow( - 'Unsupported warehouse driver "oracle". Supported drivers: bigquery, clickhouse, mysql, postgres, postgresql, sqlite, sqlite3, snowflake, sqlserver', + 'Unsupported warehouse driver "oracle". Supported drivers: bigquery, clickhouse, duckdb, mysql, postgres, postgresql, sqlite, sqlite3, snowflake, sqlserver', ); }); }); diff --git a/packages/context/src/connections/dialects.ts b/packages/context/src/connections/dialects.ts index afac4bd2..e3a5d2d6 100644 --- a/packages/context/src/connections/dialects.ts +++ b/packages/context/src/connections/dialects.ts @@ -8,6 +8,7 @@ export type SupportedDriver = | 'snowflake' | 'bigquery' | 'clickhouse' + | 'duckdb' | 'sqlite' | 'sqlite3'; @@ -21,6 +22,7 @@ export interface KtxDialect { const supportedDrivers: SupportedDriver[] = [ 'bigquery', 'clickhouse', + 'duckdb', 'mysql', 'postgres', 'postgresql', @@ -86,6 +88,7 @@ const dialects: Record = { postgresql: createDialect('postgresql', doubleQuoted), mysql: createDialect('mysql', backtickQuoted), clickhouse: createDialect('clickhouse', backtickQuoted), + duckdb: createDialect('duckdb', doubleQuoted), sqlite: createDialect('sqlite', doubleQuoted, true), sqlite3: createDialect('sqlite3', doubleQuoted, true), snowflake: createDialect('snowflake', doubleQuoted), diff --git a/packages/context/src/connections/local-query-executor.test.ts b/packages/context/src/connections/local-query-executor.test.ts index d2f77975..4aa7b9a6 100644 --- a/packages/context/src/connections/local-query-executor.test.ts +++ b/packages/context/src/connections/local-query-executor.test.ts @@ -56,4 +56,42 @@ describe('createDefaultLocalQueryExecutor', () => { }), ).rejects.toThrow('No local query executor is configured for driver "snowflake".'); }); + + it('dispatches duckdb only when a duckdb executor slot is supplied', async () => { + const duckdb = { + execute: vi.fn(async () => ({ + headers: ['duckdb'], + rows: [[3]], + totalRows: 1, + command: 'SELECT', + rowCount: 1, + })), + }; + const executor = createDefaultLocalQueryExecutor({ + postgres: { execute: vi.fn() }, + sqlite: { execute: vi.fn() }, + duckdb, + }); + + await expect( + executor.execute({ + connectionId: 'warehouse', + connection: { driver: 'duckdb' }, + sql: 'select 1', + }), + ).resolves.toMatchObject({ headers: ['duckdb'] }); + expect(duckdb.execute).toHaveBeenCalledTimes(1); + + const missingSlot = createDefaultLocalQueryExecutor({ + postgres: { execute: vi.fn() }, + sqlite: { execute: vi.fn() }, + }); + await expect( + missingSlot.execute({ + connectionId: 'warehouse', + connection: { driver: 'duckdb' }, + sql: 'select 1', + }), + ).rejects.toThrow('No local query executor is configured for driver "duckdb".'); + }); }); diff --git a/packages/context/src/connections/local-query-executor.ts b/packages/context/src/connections/local-query-executor.ts index 9b5f2032..e4532047 100644 --- a/packages/context/src/connections/local-query-executor.ts +++ b/packages/context/src/connections/local-query-executor.ts @@ -9,6 +9,7 @@ import { createSqliteQueryExecutor } from './sqlite-query-executor.js'; export interface DefaultLocalQueryExecutorOptions { postgres?: KtxSqlQueryExecutorPort; sqlite?: KtxSqlQueryExecutorPort; + duckdb?: KtxSqlQueryExecutorPort; } function driverFor(input: KtxSqlQueryExecutionInput): string { @@ -28,6 +29,12 @@ export function createDefaultLocalQueryExecutor(options: DefaultLocalQueryExecut if (driver === 'sqlite' || driver === 'sqlite3') { return sqlite.execute(input); } + if (driver === 'duckdb') { + if (!options.duckdb) { + throw new Error(`No local query executor is configured for driver "${input.connection?.driver ?? 'unknown'}".`); + } + return options.duckdb.execute(input); + } throw new Error(`No local query executor is configured for driver "${input.connection?.driver ?? 'unknown'}".`); }, }; diff --git a/packages/context/src/connections/local-warehouse-descriptor.test.ts b/packages/context/src/connections/local-warehouse-descriptor.test.ts index 0eee9f34..d82603e3 100644 --- a/packages/context/src/connections/local-warehouse-descriptor.test.ts +++ b/packages/context/src/connections/local-warehouse-descriptor.test.ts @@ -35,6 +35,15 @@ describe('localConnectionToWarehouseDescriptor', () => { }); }); + it('maps DuckDB configs to DUCKDB warehouse descriptors', () => { + expect(localConnectionToWarehouseDescriptor('warehouse', { driver: 'duckdb', path: 'data/warehouse.duckdb' })).toMatchObject({ + id: 'warehouse', + connection_type: 'DUCKDB', + connection_params: { driver: 'duckdb', path: 'data/warehouse.duckdb' }, + }); + expect(localConnectionTypeForConfig('warehouse', { driver: 'duckdb', path: 'data/warehouse.duckdb' })).toBe('DUCKDB'); + }); + it('returns null for non-warehouse adapters', () => { expect( localConnectionToWarehouseDescriptor('looker', { diff --git a/packages/context/src/connections/local-warehouse-descriptor.ts b/packages/context/src/connections/local-warehouse-descriptor.ts index c2cc6516..02efa193 100644 --- a/packages/context/src/connections/local-warehouse-descriptor.ts +++ b/packages/context/src/connections/local-warehouse-descriptor.ts @@ -22,6 +22,7 @@ const DRIVER_TO_CONNECTION_TYPE: Record = { postgres: 'POSTGRESQL', postgresql: 'POSTGRESQL', sqlite: 'SQLITE', + duckdb: 'DUCKDB', sqlserver: 'SQLSERVER', mssql: 'SQLSERVER', mysql: 'MYSQL', diff --git a/packages/context/src/project/driver-schemas.test.ts b/packages/context/src/project/driver-schemas.test.ts index 89862546..e84a5b3d 100644 --- a/packages/context/src/project/driver-schemas.test.ts +++ b/packages/context/src/project/driver-schemas.test.ts @@ -29,6 +29,13 @@ describe('connectionConfigSchema (driver discriminated union)', () => { }); }); + it('accepts duckdb local file config', () => { + expect(connectionConfigSchema.parse({ driver: 'duckdb', path: 'data/warehouse.duckdb' })).toMatchObject({ + driver: 'duckdb', + path: 'data/warehouse.duckdb', + }); + }); + it('rejects an unknown driver', () => { expect(() => connectionConfigSchema.parse({ driver: 'nope', url: 'x' })).toThrow(); }); diff --git a/packages/context/src/project/driver-schemas.ts b/packages/context/src/project/driver-schemas.ts index 1815975d..f4921afc 100644 --- a/packages/context/src/project/driver-schemas.ts +++ b/packages/context/src/project/driver-schemas.ts @@ -12,6 +12,7 @@ const warehouseDrivers = [ 'snowflake', 'bigquery', 'sqlite', + 'duckdb', 'clickhouse', 'sqlserver', ] as const; @@ -27,6 +28,11 @@ function warehouseConnectionSchema(driver: .min(1) .optional() .describe('Warehouse connection URL or DSN; may contain environment-variable references like env:DATABASE_URL.'), + path: z + .string() + .min(1) + .optional() + .describe('Local database file path for file-backed warehouse drivers such as SQLite and DuckDB.'), }) .describe( `${driver} warehouse connection. Additional driver-tunable fields (e.g. historicSql, context.queryHistory) are accepted and passed through.`, @@ -40,6 +46,7 @@ const warehouseConnectionSchemas = [ warehouseConnectionSchema('snowflake'), warehouseConnectionSchema('bigquery'), warehouseConnectionSchema('sqlite'), + warehouseConnectionSchema('duckdb'), warehouseConnectionSchema('clickhouse'), warehouseConnectionSchema('sqlserver'), ] as const; diff --git a/packages/context/src/scan/local-scan.test.ts b/packages/context/src/scan/local-scan.test.ts index 16fab098..4efdd39b 100644 --- a/packages/context/src/scan/local-scan.test.ts +++ b/packages/context/src/scan/local-scan.test.ts @@ -1403,6 +1403,37 @@ describe('local scan', () => { ); }); + it('accepts duckdb as a native standalone scan driver when the host supplies a live-database adapter', async () => { + await writeFile( + join(project.projectDir, 'ktx.yaml'), + [ + 'connections:', + ' warehouse:', + ' driver: duckdb', + ' path: warehouse.duckdb', + 'ingest:', + ' adapters:', + ' - live-database', + '', + ].join('\n'), + 'utf-8', + ); + project = await loadKtxProject({ projectDir: project.projectDir }); + + const result = await runLocalScan({ + project, + adapters: [fetchOnlyAdapter()], + connectionId: 'warehouse', + jobId: 'scan-run-duckdb', + now: () => new Date('2026-04-29T12:00:00.000Z'), + }); + + expect(result.report.driver).toBe('duckdb'); + expect(result.report.artifactPaths.reportPath).toBe( + 'raw-sources/warehouse/live-database/2026-04-29-120000-scan-run-duckdb/scan-report.json', + ); + }); + it('accepts mysql as a native standalone scan driver when the host supplies a live-database adapter', async () => { await writeFile( join(project.projectDir, 'ktx.yaml'), diff --git a/packages/context/src/scan/local-scan.ts b/packages/context/src/scan/local-scan.ts index f9ac77d3..8379fa10 100644 --- a/packages/context/src/scan/local-scan.ts +++ b/packages/context/src/scan/local-scan.ts @@ -102,6 +102,7 @@ function normalizeDriver(driver: string | undefined): KtxConnectionDriver { normalized === 'postgresql' || normalized === 'sqlite' || normalized === 'sqlite3' || + normalized === 'duckdb' || normalized === 'mysql' || normalized === 'clickhouse' || normalized === 'sqlserver' || @@ -111,7 +112,7 @@ function normalizeDriver(driver: string | undefined): KtxConnectionDriver { return normalized === 'sqlite3' ? 'sqlite' : normalized; } throw new Error( - `Standalone ktx scan supports postgres/postgresql/sqlite/mysql/clickhouse/sqlserver/bigquery/snowflake in this phase, received "${driver ?? 'unknown'}"`, + `Standalone ktx scan supports postgres/postgresql/sqlite/duckdb/mysql/clickhouse/sqlserver/bigquery/snowflake in this phase, received "${driver ?? 'unknown'}"`, ); } diff --git a/packages/context/src/scan/types.ts b/packages/context/src/scan/types.ts index bc8959f5..094a05f1 100644 --- a/packages/context/src/scan/types.ts +++ b/packages/context/src/scan/types.ts @@ -2,6 +2,7 @@ export type KtxConnectionDriver = | 'sqlite' | 'postgres' | 'postgresql' + | 'duckdb' | 'sqlserver' | 'bigquery' | 'snowflake'