@leonardovida-md/drizzle-neo-duckdb 1.0.2 → 1.0.3

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,324 @@
1
+ import {
2
+ listValue,
3
+ arrayValue,
4
+ structValue,
5
+ mapValue,
6
+ blobValue,
7
+ timestampValue,
8
+ timestampTZValue,
9
+ type DuckDBValue,
10
+ type DuckDBMapEntry,
11
+ } from '@duckdb/node-api';
12
+
13
+ /**
14
+ * Symbol used to identify wrapped DuckDB values for native binding.
15
+ * Uses Symbol.for() to ensure cross-module compatibility.
16
+ */
17
+ export const DUCKDB_VALUE_MARKER = Symbol.for('drizzle-duckdb:value');
18
+
19
+ /**
20
+ * Type identifier for each wrapper kind.
21
+ */
22
+ export type DuckDBValueKind =
23
+ | 'list'
24
+ | 'array'
25
+ | 'struct'
26
+ | 'map'
27
+ | 'timestamp'
28
+ | 'blob'
29
+ | 'json';
30
+
31
+ /**
32
+ * Base interface for all tagged DuckDB value wrappers.
33
+ */
34
+ export interface DuckDBValueWrapper<
35
+ TKind extends DuckDBValueKind = DuckDBValueKind,
36
+ TData = unknown,
37
+ > {
38
+ readonly [DUCKDB_VALUE_MARKER]: true;
39
+ readonly kind: TKind;
40
+ readonly data: TData;
41
+ }
42
+
43
+ /**
44
+ * List wrapper - maps to DuckDBListValue
45
+ */
46
+ export interface ListValueWrapper
47
+ extends DuckDBValueWrapper<'list', unknown[]> {
48
+ readonly elementType?: string;
49
+ }
50
+
51
+ /**
52
+ * Array wrapper (fixed size) - maps to DuckDBArrayValue
53
+ */
54
+ export interface ArrayValueWrapper
55
+ extends DuckDBValueWrapper<'array', unknown[]> {
56
+ readonly elementType?: string;
57
+ readonly fixedLength?: number;
58
+ }
59
+
60
+ /**
61
+ * Struct wrapper - maps to DuckDBStructValue
62
+ */
63
+ export interface StructValueWrapper
64
+ extends DuckDBValueWrapper<'struct', Record<string, unknown>> {
65
+ readonly schema?: Record<string, string>;
66
+ }
67
+
68
+ /**
69
+ * Map wrapper - maps to DuckDBMapValue
70
+ */
71
+ export interface MapValueWrapper
72
+ extends DuckDBValueWrapper<'map', Record<string, unknown>> {
73
+ readonly valueType?: string;
74
+ }
75
+
76
+ /**
77
+ * Timestamp wrapper - maps to DuckDBTimestampValue or DuckDBTimestampTZValue
78
+ */
79
+ export interface TimestampValueWrapper
80
+ extends DuckDBValueWrapper<'timestamp', Date | string> {
81
+ readonly withTimezone: boolean;
82
+ readonly precision?: number;
83
+ }
84
+
85
+ /**
86
+ * Blob wrapper - maps to DuckDBBlobValue
87
+ */
88
+ export interface BlobValueWrapper
89
+ extends DuckDBValueWrapper<'blob', Buffer | Uint8Array> {}
90
+
91
+ /**
92
+ * JSON wrapper - delays JSON.stringify() to binding time.
93
+ * DuckDB stores JSON as VARCHAR internally.
94
+ */
95
+ export interface JsonValueWrapper extends DuckDBValueWrapper<'json', unknown> {}
96
+
97
+ /**
98
+ * Union of all wrapper types for exhaustive type checking.
99
+ */
100
+ export type AnyDuckDBValueWrapper =
101
+ | ListValueWrapper
102
+ | ArrayValueWrapper
103
+ | StructValueWrapper
104
+ | MapValueWrapper
105
+ | TimestampValueWrapper
106
+ | BlobValueWrapper
107
+ | JsonValueWrapper;
108
+
109
+ /**
110
+ * Type guard to check if a value is a tagged DuckDB wrapper.
111
+ * Optimized for fast detection in the hot path.
112
+ */
113
+ export function isDuckDBWrapper(
114
+ value: unknown
115
+ ): value is AnyDuckDBValueWrapper {
116
+ return (
117
+ value !== null &&
118
+ typeof value === 'object' &&
119
+ DUCKDB_VALUE_MARKER in value &&
120
+ (value as DuckDBValueWrapper)[DUCKDB_VALUE_MARKER] === true
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Create a list wrapper for variable-length lists.
126
+ */
127
+ export function wrapList(
128
+ data: unknown[],
129
+ elementType?: string
130
+ ): ListValueWrapper {
131
+ return {
132
+ [DUCKDB_VALUE_MARKER]: true,
133
+ kind: 'list',
134
+ data,
135
+ elementType,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Create an array wrapper for fixed-length arrays.
141
+ */
142
+ export function wrapArray(
143
+ data: unknown[],
144
+ elementType?: string,
145
+ fixedLength?: number
146
+ ): ArrayValueWrapper {
147
+ return {
148
+ [DUCKDB_VALUE_MARKER]: true,
149
+ kind: 'array',
150
+ data,
151
+ elementType,
152
+ fixedLength,
153
+ };
154
+ }
155
+
156
+ /**
157
+ * Create a struct wrapper for named field structures.
158
+ */
159
+ export function wrapStruct(
160
+ data: Record<string, unknown>,
161
+ schema?: Record<string, string>
162
+ ): StructValueWrapper {
163
+ return {
164
+ [DUCKDB_VALUE_MARKER]: true,
165
+ kind: 'struct',
166
+ data,
167
+ schema,
168
+ };
169
+ }
170
+
171
+ /**
172
+ * Create a map wrapper for key-value maps.
173
+ */
174
+ export function wrapMap(
175
+ data: Record<string, unknown>,
176
+ valueType?: string
177
+ ): MapValueWrapper {
178
+ return {
179
+ [DUCKDB_VALUE_MARKER]: true,
180
+ kind: 'map',
181
+ data,
182
+ valueType,
183
+ };
184
+ }
185
+
186
+ /**
187
+ * Create a timestamp wrapper.
188
+ */
189
+ export function wrapTimestamp(
190
+ data: Date | string,
191
+ withTimezone: boolean,
192
+ precision?: number
193
+ ): TimestampValueWrapper {
194
+ return {
195
+ [DUCKDB_VALUE_MARKER]: true,
196
+ kind: 'timestamp',
197
+ data,
198
+ withTimezone,
199
+ precision,
200
+ };
201
+ }
202
+
203
+ /**
204
+ * Create a blob wrapper for binary data.
205
+ */
206
+ export function wrapBlob(data: Buffer | Uint8Array): BlobValueWrapper {
207
+ return {
208
+ [DUCKDB_VALUE_MARKER]: true,
209
+ kind: 'blob',
210
+ data,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Create a JSON wrapper that delays JSON.stringify() to binding time.
216
+ * This ensures consistent handling with other wrapped types.
217
+ */
218
+ export function wrapJson(data: unknown): JsonValueWrapper {
219
+ return {
220
+ [DUCKDB_VALUE_MARKER]: true,
221
+ kind: 'json',
222
+ data,
223
+ };
224
+ }
225
+
226
+ /**
227
+ * Convert a Date or string to microseconds since Unix epoch.
228
+ * Handles both Date objects and ISO-like timestamp strings.
229
+ */
230
+ function dateToMicros(value: Date | string): bigint {
231
+ if (value instanceof Date) {
232
+ return BigInt(value.getTime()) * 1000n;
233
+ }
234
+
235
+ // For strings, normalize the format for reliable parsing
236
+ // Handle both 'YYYY-MM-DD HH:MM:SS' and 'YYYY-MM-DDTHH:MM:SS' formats
237
+ let normalized = value;
238
+ if (!value.includes('T') && value.includes(' ')) {
239
+ // Convert 'YYYY-MM-DD HH:MM:SS' to ISO format
240
+ normalized = value.replace(' ', 'T');
241
+ }
242
+ // Add 'Z' suffix if no timezone offset to treat as UTC
243
+ if (!normalized.endsWith('Z') && !/[+-]\d{2}:?\d{2}$/.test(normalized)) {
244
+ normalized += 'Z';
245
+ }
246
+
247
+ const date = new Date(normalized);
248
+ if (isNaN(date.getTime())) {
249
+ throw new Error(`Invalid timestamp string: ${value}`);
250
+ }
251
+ return BigInt(date.getTime()) * 1000n;
252
+ }
253
+
254
+ /**
255
+ * Convert Buffer or Uint8Array to Uint8Array.
256
+ */
257
+ function toUint8Array(data: Buffer | Uint8Array): Uint8Array {
258
+ return data instanceof Uint8Array && !(data instanceof Buffer)
259
+ ? data
260
+ : new Uint8Array(data);
261
+ }
262
+
263
+ /**
264
+ * Convert struct entries to DuckDB struct value entries.
265
+ */
266
+ function convertStructEntries(
267
+ data: Record<string, unknown>,
268
+ toValue: (v: unknown) => DuckDBValue
269
+ ): Record<string, DuckDBValue> {
270
+ const entries: Record<string, DuckDBValue> = {};
271
+ for (const [key, val] of Object.entries(data)) {
272
+ entries[key] = toValue(val);
273
+ }
274
+ return entries;
275
+ }
276
+
277
+ /**
278
+ * Convert map entries to DuckDB map entry format.
279
+ */
280
+ function convertMapEntries(
281
+ data: Record<string, unknown>,
282
+ toValue: (v: unknown) => DuckDBValue
283
+ ): DuckDBMapEntry[] {
284
+ return Object.entries(data).map(([key, val]) => ({
285
+ key: key as DuckDBValue,
286
+ value: toValue(val),
287
+ }));
288
+ }
289
+
290
+ /**
291
+ * Convert a wrapper to a DuckDB Node API value.
292
+ * Uses exhaustive switch for compile-time safety.
293
+ */
294
+ export function wrapperToNodeApiValue(
295
+ wrapper: AnyDuckDBValueWrapper,
296
+ toValue: (v: unknown) => DuckDBValue
297
+ ): DuckDBValue {
298
+ switch (wrapper.kind) {
299
+ case 'list':
300
+ return listValue(wrapper.data.map(toValue));
301
+ case 'array':
302
+ return arrayValue(wrapper.data.map(toValue));
303
+ case 'struct':
304
+ return structValue(convertStructEntries(wrapper.data, toValue));
305
+ case 'map':
306
+ return mapValue(convertMapEntries(wrapper.data, toValue));
307
+ case 'timestamp':
308
+ return wrapper.withTimezone
309
+ ? timestampTZValue(dateToMicros(wrapper.data))
310
+ : timestampValue(dateToMicros(wrapper.data));
311
+ case 'blob':
312
+ return blobValue(toUint8Array(wrapper.data));
313
+ case 'json':
314
+ // JSON is stored as VARCHAR in DuckDB - stringify at binding time
315
+ return JSON.stringify(wrapper.data);
316
+ default: {
317
+ // Exhaustive check - TypeScript will error if a case is missing
318
+ const _exhaustive: never = wrapper;
319
+ throw new Error(
320
+ `Unknown wrapper kind: ${(_exhaustive as AnyDuckDBValueWrapper).kind}`
321
+ );
322
+ }
323
+ }
324
+ }