@leonardovida-md/drizzle-neo-duckdb 1.0.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.
@@ -0,0 +1,53 @@
1
+ import type { RowData } from './client.ts';
2
+ import type { DuckDBDatabase } from './driver.ts';
3
+ export interface IntrospectOptions {
4
+ schemas?: string[];
5
+ includeViews?: boolean;
6
+ useCustomTimeTypes?: boolean;
7
+ mapJsonAsDuckDbJson?: boolean;
8
+ importBasePath?: string;
9
+ }
10
+ interface DuckDbIndexRow extends RowData {
11
+ schema_name: string;
12
+ table_name: string;
13
+ index_name: string;
14
+ is_unique: boolean | null;
15
+ expressions: string | null;
16
+ }
17
+ export interface IntrospectedColumn {
18
+ name: string;
19
+ dataType: string;
20
+ columnDefault: string | null;
21
+ nullable: boolean;
22
+ characterLength: number | null;
23
+ numericPrecision: number | null;
24
+ numericScale: number | null;
25
+ }
26
+ export interface IntrospectedConstraint {
27
+ name: string;
28
+ type: string;
29
+ columns: string[];
30
+ referencedTable?: {
31
+ name: string;
32
+ schema: string;
33
+ columns: string[];
34
+ };
35
+ rawExpression?: string | null;
36
+ }
37
+ export interface IntrospectedTable {
38
+ schema: string;
39
+ name: string;
40
+ kind: 'table' | 'view';
41
+ columns: IntrospectedColumn[];
42
+ constraints: IntrospectedConstraint[];
43
+ indexes: DuckDbIndexRow[];
44
+ }
45
+ export interface IntrospectResult {
46
+ files: {
47
+ schemaTs: string;
48
+ metaJson: IntrospectedTable[];
49
+ relationsTs?: string;
50
+ };
51
+ }
52
+ export declare function introspect(db: DuckDBDatabase, opts?: IntrospectOptions): Promise<IntrospectResult>;
53
+ export {};
@@ -0,0 +1,4 @@
1
+ import type { MigrationConfig } from 'drizzle-orm/migrator';
2
+ import type { DuckDBDatabase } from './driver.ts';
3
+ export type DuckDbMigrationConfig = MigrationConfig | string;
4
+ export declare function migrate<TSchema extends Record<string, unknown>>(db: DuckDBDatabase<TSchema>, config: DuckDbMigrationConfig): Promise<void>;
@@ -0,0 +1,31 @@
1
+ import { PgSelectBuilder, type CreatePgSelectFromBuilderMode, type SelectedFields, type TableLikeHasEmptySelection } from 'drizzle-orm/pg-core/query-builders';
2
+ import { PgColumn, PgTable, type PgSession } from 'drizzle-orm/pg-core';
3
+ import { Subquery, ViewBaseConfig, type SQLWrapper } from 'drizzle-orm';
4
+ import { PgViewBase } from 'drizzle-orm/pg-core/view-base';
5
+ import type { GetSelectTableName, GetSelectTableSelection } from 'drizzle-orm/query-builders/select.types';
6
+ import { SQL, type ColumnsSelection } from 'drizzle-orm/sql/sql';
7
+ import type { DuckDBDialect } from './dialect.ts';
8
+ import { type DrizzleTypeError } from 'drizzle-orm/utils';
9
+ interface PgViewBaseInternal<TName extends string = string, TExisting extends boolean = boolean, TSelectedFields extends ColumnsSelection = ColumnsSelection> extends PgViewBase<TName, TExisting, TSelectedFields> {
10
+ [ViewBaseConfig]?: {
11
+ selectedFields: SelectedFields;
12
+ };
13
+ }
14
+ export declare class DuckDBSelectBuilder<TSelection extends SelectedFields | undefined, TBuilderMode extends 'db' | 'qb' = 'db'> extends PgSelectBuilder<TSelection, TBuilderMode> {
15
+ private _fields;
16
+ private _session;
17
+ private _dialect;
18
+ private _withList;
19
+ private _distinct;
20
+ constructor(config: {
21
+ fields: TSelection;
22
+ session: PgSession | undefined;
23
+ dialect: DuckDBDialect;
24
+ withList?: Subquery[];
25
+ distinct?: boolean | {
26
+ on: (PgColumn | SQLWrapper)[];
27
+ };
28
+ });
29
+ from<TFrom extends PgTable | Subquery | PgViewBaseInternal | SQL>(source: TableLikeHasEmptySelection<TFrom> extends true ? DrizzleTypeError<"Cannot reference a data-modifying statement subquery if it doesn't contain a `returning` clause"> : TFrom): CreatePgSelectFromBuilderMode<TBuilderMode, GetSelectTableName<TFrom>, TSelection extends undefined ? GetSelectTableSelection<TFrom> : TSelection, TSelection extends undefined ? 'single' : 'partial'>;
30
+ }
31
+ export {};
@@ -0,0 +1,62 @@
1
+ import { entityKind } from 'drizzle-orm/entity';
2
+ import type { Logger } from 'drizzle-orm/logger';
3
+ import { PgTransaction } from 'drizzle-orm/pg-core';
4
+ import type { SelectedFieldsOrdered } from 'drizzle-orm/pg-core/query-builders/select.types';
5
+ import type { PgTransactionConfig, PreparedQueryConfig, PgQueryResultHKT } from 'drizzle-orm/pg-core/session';
6
+ import { PgPreparedQuery, PgSession } from 'drizzle-orm/pg-core/session';
7
+ import type { RelationalSchemaConfig, TablesRelationalConfig } from 'drizzle-orm/relations';
8
+ import { type Query, SQL } from 'drizzle-orm/sql/sql';
9
+ import type { Assume } from 'drizzle-orm/utils';
10
+ import type { DuckDBDialect } from './dialect.ts';
11
+ import type { DuckDBClientLike, RowData } from './client.ts';
12
+ export type { DuckDBClientLike, RowData } from './client.ts';
13
+ export declare class DuckDBPreparedQuery<T extends PreparedQueryConfig> extends PgPreparedQuery<T> {
14
+ private client;
15
+ private dialect;
16
+ private queryString;
17
+ private params;
18
+ private logger;
19
+ private fields;
20
+ private _isResponseInArrayMode;
21
+ private customResultMapper;
22
+ private rewriteArrays;
23
+ private rejectStringArrayLiterals;
24
+ private warnOnStringArrayLiteral?;
25
+ static readonly [entityKind]: string;
26
+ constructor(client: DuckDBClientLike, dialect: DuckDBDialect, queryString: string, params: unknown[], logger: Logger, fields: SelectedFieldsOrdered | undefined, _isResponseInArrayMode: boolean, customResultMapper: ((rows: unknown[][]) => T['execute']) | undefined, rewriteArrays: boolean, rejectStringArrayLiterals: boolean, warnOnStringArrayLiteral?: ((sql: string) => void) | undefined);
27
+ execute(placeholderValues?: Record<string, unknown> | undefined): Promise<T['execute']>;
28
+ all(placeholderValues?: Record<string, unknown> | undefined): Promise<T['all']>;
29
+ isResponseInArrayMode(): boolean;
30
+ }
31
+ export interface DuckDBSessionOptions {
32
+ logger?: Logger;
33
+ rewriteArrays?: boolean;
34
+ rejectStringArrayLiterals?: boolean;
35
+ }
36
+ export declare class DuckDBSession<TFullSchema extends Record<string, unknown> = Record<string, never>, TSchema extends TablesRelationalConfig = Record<string, never>> extends PgSession<DuckDBQueryResultHKT, TFullSchema, TSchema> {
37
+ private client;
38
+ private schema;
39
+ private options;
40
+ static readonly [entityKind]: string;
41
+ protected dialect: DuckDBDialect;
42
+ private logger;
43
+ private rewriteArrays;
44
+ private rejectStringArrayLiterals;
45
+ private hasWarnedArrayLiteral;
46
+ constructor(client: DuckDBClientLike, dialect: DuckDBDialect, schema: RelationalSchemaConfig<TSchema> | undefined, options?: DuckDBSessionOptions);
47
+ prepareQuery<T extends PreparedQueryConfig = PreparedQueryConfig>(query: Query, fields: SelectedFieldsOrdered | undefined, name: string | undefined, isResponseInArrayMode: boolean, customResultMapper?: (rows: unknown[][]) => T['execute']): PgPreparedQuery<T>;
48
+ transaction<T>(transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>): Promise<T>;
49
+ private warnOnStringArrayLiteral;
50
+ }
51
+ export declare class DuckDBTransaction<TFullSchema extends Record<string, unknown>, TSchema extends TablesRelationalConfig> extends PgTransaction<DuckDBQueryResultHKT, TFullSchema, TSchema> {
52
+ static readonly [entityKind]: string;
53
+ rollback(): never;
54
+ getTransactionConfigSQL(config: PgTransactionConfig): SQL;
55
+ setTransaction(config: PgTransactionConfig): Promise<void>;
56
+ transaction<T>(transaction: (tx: DuckDBTransaction<TFullSchema, TSchema>) => Promise<T>): Promise<T>;
57
+ }
58
+ export type GenericRowData<T extends RowData = RowData> = T;
59
+ export type GenericTableData<T = RowData> = T[];
60
+ export interface DuckDBQueryResultHKT extends PgQueryResultHKT {
61
+ type: GenericTableData<Assume<this['row'], RowData>>;
62
+ }
@@ -0,0 +1,2 @@
1
+ export declare function adaptArrayOperators(query: string): string;
2
+ export declare function queryAdapter(query: string): string;
@@ -0,0 +1,2 @@
1
+ import { type AnyColumn, type SelectedFieldsOrdered } from 'drizzle-orm';
2
+ export declare function mapResultRow<TResult>(columns: SelectedFieldsOrdered<AnyColumn>, row: unknown[], joinsNotNullableMap: Record<string, boolean> | undefined): TResult;
@@ -0,0 +1,2 @@
1
+ import type { SelectedFields } from 'drizzle-orm/pg-core';
2
+ export declare function aliasFields(fields: SelectedFields, fullJoin?: boolean): SelectedFields;
@@ -0,0 +1,3 @@
1
+ export { aliasFields } from './sql/selection.ts';
2
+ export { adaptArrayOperators, queryAdapter } from './sql/query-rewriters.ts';
3
+ export { mapResultRow } from './sql/result-mapper.ts';
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@leonardovida-md/drizzle-neo-duckdb",
3
+ "module": "./dist/index.mjs",
4
+ "main": "./dist/index.mjs",
5
+ "types": "./dist/index.d.ts",
6
+ "version": "1.0.0",
7
+ "description": "A drizzle ORM client for use with DuckDB. Based on drizzle's Postgres client.",
8
+ "type": "module",
9
+ "scripts": {
10
+ "build": "bun build --target=node ./src/index.ts --outfile=./dist/index.mjs --packages=external && bun build --target=node ./src/bin/duckdb-introspect.ts --outfile=./dist/duckdb-introspect.mjs --packages=external && bun run build:declarations",
11
+ "build:declarations": "tsc --emitDeclarationOnly --project tsconfig.types.json",
12
+ "test": "vitest",
13
+ "t": "vitest --watch --ui"
14
+ },
15
+ "bin": {
16
+ "duckdb-introspect": "dist/duckdb-introspect.mjs"
17
+ },
18
+ "peerDependencies": {
19
+ "@duckdb/node-api": "1.4.2-r.1",
20
+ "drizzle-orm": "^0.40.0"
21
+ },
22
+ "peerDependenciesMeta": {
23
+ "@duckdb/node-api": {
24
+ "optional": true
25
+ }
26
+ },
27
+ "devDependencies": {
28
+ "@duckdb/node-api": "1.4.2-r.1",
29
+ "@types/bun": "^1.2.5",
30
+ "@types/uuid": "^10.0.0",
31
+ "@vitest/ui": "^1.6.0",
32
+ "drizzle-orm": "0.40.0",
33
+ "prettier": "^3.5.3",
34
+ "typescript": "^5.8.2",
35
+ "uuid": "^10.0.0",
36
+ "vitest": "^1.6.0"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/leonardovida-md/drizzle-neo-duckdb.git"
41
+ },
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/index.d.ts",
45
+ "import": "./dist/index.mjs"
46
+ },
47
+ "./package.json": "./package.json"
48
+ },
49
+ "sideEffects": false,
50
+ "publishConfig": {
51
+ "access": "public",
52
+ "registry": "https://registry.npmjs.org"
53
+ },
54
+ "engines": {
55
+ "node": ">=18.17"
56
+ },
57
+ "packageManager": "bun@1.2.2",
58
+ "keywords": [
59
+ "drizzle",
60
+ "duckdb"
61
+ ],
62
+ "author": "M L",
63
+ "license": "Apache-2.0",
64
+ "bugs": {
65
+ "url": "https://github.com/leonardovida-md/drizzle-neo-duckdb/issues"
66
+ },
67
+ "homepage": "https://github.com/leonardovida-md/drizzle-neo-duckdb#readme",
68
+ "files": [
69
+ "src/**/*.ts",
70
+ "dist/*.mjs",
71
+ "dist/**/*.d.ts"
72
+ ]
73
+ }
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env node
2
+ import { DuckDBInstance } from '@duckdb/node-api';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import process from 'node:process';
6
+ import { drizzle } from '../index.ts';
7
+ import { introspect } from '../introspect.ts';
8
+
9
+ interface CliOptions {
10
+ url?: string;
11
+ schemas?: string[];
12
+ outFile: string;
13
+ includeViews: boolean;
14
+ useCustomTimeTypes: boolean;
15
+ importBasePath?: string;
16
+ }
17
+
18
+ function parseArgs(argv: string[]): CliOptions {
19
+ const options: CliOptions = {
20
+ outFile: path.resolve(process.cwd(), 'drizzle/schema.ts'),
21
+ includeViews: false,
22
+ useCustomTimeTypes: true,
23
+ };
24
+
25
+ for (let i = 0; i < argv.length; i += 1) {
26
+ const arg = argv[i]!;
27
+ switch (arg) {
28
+ case '--url':
29
+ options.url = argv[++i];
30
+ break;
31
+ case '--schema':
32
+ case '--schemas':
33
+ options.schemas = argv[++i]?.split(',').map((s) => s.trim()).filter(Boolean);
34
+ break;
35
+ case '--out':
36
+ case '--outFile':
37
+ options.outFile = path.resolve(process.cwd(), argv[++i] ?? 'drizzle/schema.ts');
38
+ break;
39
+ case '--include-views':
40
+ case '--includeViews':
41
+ options.includeViews = true;
42
+ break;
43
+ case '--use-pg-time':
44
+ options.useCustomTimeTypes = false;
45
+ break;
46
+ case '--import-base':
47
+ options.importBasePath = argv[++i];
48
+ break;
49
+ case '--help':
50
+ case '-h':
51
+ printHelp();
52
+ process.exit(0);
53
+ default:
54
+ if (arg.startsWith('-')) {
55
+ console.warn(`Unknown option ${arg}`);
56
+ }
57
+ }
58
+ }
59
+
60
+ return options;
61
+ }
62
+
63
+ function printHelp(): void {
64
+ console.log(`duckdb-introspect
65
+
66
+ Usage:
67
+ bun x duckdb-introspect --url <duckdb path|md:> [--schema my_schema] [--out ./drizzle/schema.ts]
68
+
69
+ Options:
70
+ --url DuckDB database path (e.g. :memory:, ./local.duckdb, md:)
71
+ --schema Comma separated schema list (defaults to all non-system schemas)
72
+ --out Output file (default: ./drizzle/schema.ts)
73
+ --include-views Include views in the generated schema
74
+ --use-pg-time Use pg-core timestamp/date/time instead of DuckDB custom helpers
75
+ --import-base Override import path for duckdb helpers (default: package name)
76
+ `);
77
+ }
78
+
79
+ async function main() {
80
+ const options = parseArgs(process.argv.slice(2));
81
+ if (!options.url) {
82
+ printHelp();
83
+ throw new Error('Missing required --url');
84
+ }
85
+
86
+ const instanceOptions =
87
+ options.url.startsWith('md:') && process.env.MOTHERDUCK_TOKEN
88
+ ? { motherduck_token: process.env.MOTHERDUCK_TOKEN }
89
+ : undefined;
90
+
91
+ const instance = await DuckDBInstance.create(options.url, instanceOptions);
92
+ const connection = await instance.connect();
93
+ const db = drizzle(connection);
94
+
95
+ try {
96
+ const result = await introspect(db, {
97
+ schemas: options.schemas,
98
+ includeViews: options.includeViews,
99
+ useCustomTimeTypes: options.useCustomTimeTypes,
100
+ importBasePath: options.importBasePath,
101
+ });
102
+
103
+ await mkdir(path.dirname(options.outFile), { recursive: true });
104
+ await writeFile(options.outFile, result.files.schemaTs, 'utf8');
105
+
106
+ console.log(`Wrote schema to ${options.outFile}`);
107
+ } finally {
108
+ if ('closeSync' in connection && typeof connection.closeSync === 'function') {
109
+ connection.closeSync();
110
+ }
111
+ }
112
+ }
113
+
114
+ main().catch((err) => {
115
+ console.error(err instanceof Error ? err.message : err);
116
+ process.exit(1);
117
+ });
package/src/client.ts ADDED
@@ -0,0 +1,110 @@
1
+ import {
2
+ listValue,
3
+ type DuckDBConnection,
4
+ type DuckDBValue,
5
+ } from '@duckdb/node-api';
6
+
7
+ export type DuckDBClientLike = DuckDBConnection;
8
+ export type RowData = Record<string, unknown>;
9
+
10
+ export interface PrepareParamsOptions {
11
+ rejectStringArrayLiterals?: boolean;
12
+ warnOnStringArrayLiteral?: () => void;
13
+ }
14
+
15
+ function isPgArrayLiteral(value: string): boolean {
16
+ return value.startsWith('{') && value.endsWith('}');
17
+ }
18
+
19
+ function parsePgArrayLiteral(value: string): unknown {
20
+ const json = value.replace(/{/g, '[').replace(/}/g, ']');
21
+
22
+ try {
23
+ return JSON.parse(json);
24
+ } catch {
25
+ return value;
26
+ }
27
+ }
28
+
29
+ let warnedArrayLiteral = false;
30
+
31
+ export function prepareParams(
32
+ params: unknown[],
33
+ options: PrepareParamsOptions = {}
34
+ ): unknown[] {
35
+ return params.map((param) => {
36
+ if (typeof param === 'string' && isPgArrayLiteral(param)) {
37
+ if (options.rejectStringArrayLiterals) {
38
+ throw new Error(
39
+ 'Stringified array literals are not supported. Use duckDbList()/duckDbArray() or pass native arrays.'
40
+ );
41
+ }
42
+
43
+ if (!warnedArrayLiteral && options.warnOnStringArrayLiteral) {
44
+ warnedArrayLiteral = true;
45
+ options.warnOnStringArrayLiteral();
46
+ }
47
+ return parsePgArrayLiteral(param);
48
+ }
49
+ return param;
50
+ });
51
+ }
52
+
53
+ function toNodeApiValue(value: unknown): DuckDBValue {
54
+ if (Array.isArray(value)) {
55
+ return listValue(value.map((inner) => toNodeApiValue(inner)));
56
+ }
57
+ return value as DuckDBValue;
58
+ }
59
+
60
+ export async function closeClientConnection(
61
+ connection: DuckDBConnection
62
+ ): Promise<void> {
63
+ if ('close' in connection && typeof connection.close === 'function') {
64
+ await connection.close();
65
+ return;
66
+ }
67
+
68
+ if (
69
+ 'closeSync' in connection &&
70
+ typeof connection.closeSync === 'function'
71
+ ) {
72
+ connection.closeSync();
73
+ return;
74
+ }
75
+
76
+ if (
77
+ 'disconnectSync' in connection &&
78
+ typeof connection.disconnectSync === 'function'
79
+ ) {
80
+ connection.disconnectSync();
81
+ }
82
+ }
83
+
84
+ export async function executeOnClient(
85
+ client: DuckDBClientLike,
86
+ query: string,
87
+ params: unknown[]
88
+ ): Promise<RowData[]> {
89
+ const values =
90
+ params.length > 0
91
+ ? (params.map((param) => toNodeApiValue(param)) as DuckDBValue[])
92
+ : undefined;
93
+ const result = await client.run(query, values);
94
+ const rows = await result.getRowsJS();
95
+ const columns = result.columnNames();
96
+ const seen: Record<string, number> = {};
97
+ const uniqueColumns = columns.map((col) => {
98
+ const count = seen[col] ?? 0;
99
+ seen[col] = count + 1;
100
+ return count === 0 ? col : `${col}_${count}`;
101
+ });
102
+
103
+ return (rows ?? []).map((vals) => {
104
+ const obj: Record<string, unknown> = {};
105
+ uniqueColumns.forEach((col, idx) => {
106
+ obj[col] = vals[idx];
107
+ });
108
+ return obj;
109
+ }) as RowData[];
110
+ }