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

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.
@@ -1,144 +1,205 @@
1
- const selectionRegex = /select\s+(.+)\s+from/i;
2
- const tableIdPropSelectionRegex = new RegExp(
3
- [
4
- `("(.+)"\\."(.+)")`, // table identifier + property
5
- `(\\s+as\\s+'?(.+?)'?\\.'?(.+?)'?)?`, // optional AS clause
6
- ].join(''),
7
- 'i'
8
- );
9
- const noTableIdPropSelectionRegex = /"(.+)"(\s+as\s+'?\1'?)?/i;
1
+ type ArrayOperator = {
2
+ token: '@>' | '<@' | '&&';
3
+ fn: 'array_has_all' | 'array_has_any';
4
+ swap?: boolean;
5
+ };
6
+
7
+ const OPERATORS: ArrayOperator[] = [
8
+ { token: '@>', fn: 'array_has_all' },
9
+ { token: '<@', fn: 'array_has_all', swap: true },
10
+ { token: '&&', fn: 'array_has_any' },
11
+ ];
12
+
13
+ const isWhitespace = (char: string | undefined) =>
14
+ char !== undefined && /\s/.test(char);
15
+
16
+ export function scrubForRewrite(query: string): string {
17
+ let scrubbed = '';
18
+ type State = 'code' | 'single' | 'double' | 'lineComment' | 'blockComment';
19
+ let state: State = 'code';
20
+
21
+ for (let i = 0; i < query.length; i += 1) {
22
+ const char = query[i]!;
23
+ const next = query[i + 1];
24
+
25
+ if (state === 'code') {
26
+ if (char === "'") {
27
+ scrubbed += "'";
28
+ state = 'single';
29
+ continue;
30
+ }
31
+ if (char === '"') {
32
+ scrubbed += '"';
33
+ state = 'double';
34
+ continue;
35
+ }
36
+ if (char === '-' && next === '-') {
37
+ scrubbed += ' ';
38
+ i += 1;
39
+ state = 'lineComment';
40
+ continue;
41
+ }
42
+ if (char === '/' && next === '*') {
43
+ scrubbed += ' ';
44
+ i += 1;
45
+ state = 'blockComment';
46
+ continue;
47
+ }
10
48
 
11
- export function adaptArrayOperators(query: string): string {
12
- type ArrayOperator = {
13
- token: '@>' | '<@' | '&&';
14
- fn: 'array_has_all' | 'array_has_any';
15
- swap?: boolean;
16
- };
17
-
18
- const operators: ArrayOperator[] = [
19
- { token: '@>', fn: 'array_has_all' },
20
- { token: '<@', fn: 'array_has_all', swap: true },
21
- { token: '&&', fn: 'array_has_any' },
22
- ];
23
-
24
- const isWhitespace = (char: string | undefined) =>
25
- char !== undefined && /\s/.test(char);
26
-
27
- const walkLeft = (source: string, start: number): [number, string] => {
28
- let idx = start;
29
- while (idx >= 0 && isWhitespace(source[idx])) {
30
- idx--;
49
+ scrubbed += char;
50
+ continue;
31
51
  }
32
52
 
33
- let depth = 0;
34
- let inString = false;
35
- for (; idx >= 0; idx--) {
36
- const ch = source[idx];
37
- if (ch === "'" && source[idx - 1] !== '\\') {
38
- inString = !inString;
53
+ if (state === 'single') {
54
+ if (char === "'" && next === "'") {
55
+ scrubbed += "''";
56
+ i += 1;
57
+ continue;
39
58
  }
40
- if (inString) continue;
41
- if (ch === ')' || ch === ']') {
42
- depth++;
43
- } else if (ch === '(' || ch === '[') {
44
- depth--;
45
- if (depth < 0) {
46
- return [idx + 1, source.slice(idx + 1, start + 1)];
47
- }
48
- } else if (depth === 0 && isWhitespace(ch)) {
49
- return [idx + 1, source.slice(idx + 1, start + 1)];
59
+ // Preserve quote for boundary detection but mask inner chars with a
60
+ // non-whitespace placeholder to avoid false positives on operators.
61
+ scrubbed += char === "'" ? "'" : '.';
62
+ if (char === "'") {
63
+ state = 'code';
50
64
  }
65
+ continue;
51
66
  }
52
- return [0, source.slice(0, start + 1)];
53
- };
54
67
 
55
- const walkRight = (source: string, start: number): [number, string] => {
56
- let idx = start;
57
- while (idx < source.length && isWhitespace(source[idx])) {
58
- idx++;
68
+ if (state === 'double') {
69
+ if (char === '"' && next === '"') {
70
+ scrubbed += '""';
71
+ i += 1;
72
+ continue;
73
+ }
74
+ scrubbed += char === '"' ? '"' : '.';
75
+ if (char === '"') {
76
+ state = 'code';
77
+ }
78
+ continue;
59
79
  }
60
80
 
61
- let depth = 0;
62
- let inString = false;
63
- for (; idx < source.length; idx++) {
64
- const ch = source[idx];
65
- if (ch === "'" && source[idx - 1] !== '\\') {
66
- inString = !inString;
67
- }
68
- if (inString) continue;
69
- if (ch === '(' || ch === '[') {
70
- depth++;
71
- } else if (ch === ')' || ch === ']') {
72
- depth--;
73
- if (depth < 0) {
74
- return [idx, source.slice(start, idx)];
75
- }
76
- } else if (depth === 0 && isWhitespace(ch)) {
77
- return [idx, source.slice(start, idx)];
81
+ if (state === 'lineComment') {
82
+ scrubbed += char === '\n' ? '\n' : ' ';
83
+ if (char === '\n') {
84
+ state = 'code';
78
85
  }
86
+ continue;
79
87
  }
80
- return [source.length, source.slice(start)];
81
- };
82
88
 
83
- let rewritten = query;
84
- for (const { token, fn, swap } of operators) {
85
- let idx = rewritten.indexOf(token);
86
- while (idx !== -1) {
87
- const [leftStart, leftExpr] = walkLeft(rewritten, idx - 1);
88
- const [rightEnd, rightExpr] = walkRight(rewritten, idx + token.length);
89
+ if (state === 'blockComment') {
90
+ if (char === '*' && next === '/') {
91
+ scrubbed += ' ';
92
+ i += 1;
93
+ state = 'code';
94
+ } else {
95
+ scrubbed += ' ';
96
+ }
97
+ }
98
+ }
89
99
 
90
- const left = leftExpr.trim();
91
- const right = rightExpr.trim();
100
+ return scrubbed;
101
+ }
92
102
 
93
- const replacement = `${fn}(${swap ? right : left}, ${
94
- swap ? left : right
95
- })`;
103
+ function findNextOperator(
104
+ scrubbed: string,
105
+ start: number
106
+ ): { index: number; operator: ArrayOperator } | null {
107
+ for (let idx = start; idx < scrubbed.length; idx += 1) {
108
+ for (const operator of OPERATORS) {
109
+ if (scrubbed.startsWith(operator.token, idx)) {
110
+ return { index: idx, operator };
111
+ }
112
+ }
113
+ }
114
+ return null;
115
+ }
96
116
 
97
- rewritten =
98
- rewritten.slice(0, leftStart) + replacement + rewritten.slice(rightEnd);
117
+ function walkLeft(
118
+ source: string,
119
+ scrubbed: string,
120
+ start: number
121
+ ): [number, string] {
122
+ let idx = start;
123
+ while (idx >= 0 && isWhitespace(scrubbed[idx])) {
124
+ idx -= 1;
125
+ }
99
126
 
100
- idx = rewritten.indexOf(token, leftStart + replacement.length);
127
+ let depth = 0;
128
+ for (; idx >= 0; idx -= 1) {
129
+ const ch = scrubbed[idx];
130
+ if (ch === ')' || ch === ']') {
131
+ depth += 1;
132
+ } else if (ch === '(' || ch === '[') {
133
+ if (depth === 0) {
134
+ return [idx + 1, source.slice(idx + 1, start + 1)];
135
+ }
136
+ depth = Math.max(0, depth - 1);
137
+ } else if (depth === 0 && isWhitespace(ch)) {
138
+ return [idx + 1, source.slice(idx + 1, start + 1)];
101
139
  }
102
140
  }
103
141
 
104
- return rewritten;
142
+ return [0, source.slice(0, start + 1)];
105
143
  }
106
144
 
107
- export function queryAdapter(query: string): string {
108
- const selection = selectionRegex.exec(query);
109
-
110
- if (selection?.length !== 2) {
111
- return query;
145
+ function walkRight(
146
+ source: string,
147
+ scrubbed: string,
148
+ start: number
149
+ ): [number, string] {
150
+ let idx = start;
151
+ while (idx < scrubbed.length && isWhitespace(scrubbed[idx])) {
152
+ idx += 1;
112
153
  }
113
154
 
114
- const fields = selection[1]
115
- .split(',')
116
- .map((field) => {
117
- const trimmedField = field.trim();
118
- const tableProp = tableIdPropSelectionRegex.exec(trimmedField);
119
- if (tableProp) {
120
- const [, identifier, table, column, , aliasTable, aliasColumn] =
121
- tableProp;
122
-
123
- const asAlias = `'${aliasTable ?? table}.${aliasColumn ?? column}'`;
124
- if (tableProp[4]) {
125
- return trimmedField.replace(tableProp[4], ` as ${asAlias}`);
126
- }
127
- return `${identifier} as ${asAlias}`;
155
+ let depth = 0;
156
+ for (; idx < scrubbed.length; idx += 1) {
157
+ const ch = scrubbed[idx];
158
+ if (ch === '(' || ch === '[') {
159
+ depth += 1;
160
+ } else if (ch === ')' || ch === ']') {
161
+ if (depth === 0) {
162
+ return [idx, source.slice(start, idx)];
128
163
  }
164
+ depth = Math.max(0, depth - 1);
165
+ } else if (depth === 0 && isWhitespace(ch)) {
166
+ return [idx, source.slice(start, idx)];
167
+ }
168
+ }
129
169
 
130
- const noTableProp = noTableIdPropSelectionRegex.exec(trimmedField);
131
- if (noTableProp) {
132
- const [, column, alias] = noTableProp;
133
- const asAlias = ` as '${column}'`;
134
- return alias
135
- ? trimmedField.replace(alias, asAlias)
136
- : `${trimmedField}${asAlias}`;
137
- }
170
+ return [scrubbed.length, source.slice(start)];
171
+ }
138
172
 
139
- return trimmedField;
140
- })
141
- .filter(Boolean) as string[];
173
+ export function adaptArrayOperators(query: string): string {
174
+ let rewritten = query;
175
+ let scrubbed = scrubForRewrite(query);
176
+ let searchStart = 0;
177
+
178
+ // Re-run after each replacement to keep indexes aligned with the current string
179
+ while (true) {
180
+ const next = findNextOperator(scrubbed, searchStart);
181
+ if (!next) break;
182
+
183
+ const { index, operator } = next;
184
+ const [leftStart, leftExpr] = walkLeft(rewritten, scrubbed, index - 1);
185
+ const [rightEnd, rightExpr] = walkRight(
186
+ rewritten,
187
+ scrubbed,
188
+ index + operator.token.length
189
+ );
190
+
191
+ const left = leftExpr.trim();
192
+ const right = rightExpr.trim();
193
+
194
+ const replacement = `${operator.fn}(${operator.swap ? right : left}, ${
195
+ operator.swap ? left : right
196
+ })`;
197
+
198
+ rewritten =
199
+ rewritten.slice(0, leftStart) + replacement + rewritten.slice(rightEnd);
200
+ scrubbed = scrubForRewrite(rewritten);
201
+ searchStart = leftStart + replacement.length;
202
+ }
142
203
 
143
- return query.replace(selection[1], fields.join(', '));
204
+ return rewritten;
144
205
  }
@@ -32,7 +32,7 @@ function toDecoderInput<TDecoder extends DriverValueDecoder<unknown, unknown>>(
32
32
  return value as DecoderInput<TDecoder>;
33
33
  }
34
34
 
35
- function normalizeInet(value: unknown): unknown {
35
+ export function normalizeInet(value: unknown): unknown {
36
36
  if (
37
37
  value &&
38
38
  typeof value === 'object' &&
@@ -70,7 +70,7 @@ function normalizeInet(value: unknown): unknown {
70
70
  return value;
71
71
  }
72
72
 
73
- function normalizeTimestampString(
73
+ export function normalizeTimestampString(
74
74
  value: unknown,
75
75
  withTimezone: boolean
76
76
  ): string | unknown {
@@ -88,7 +88,7 @@ function normalizeTimestampString(
88
88
  return value;
89
89
  }
90
90
 
91
- function normalizeTimestamp(
91
+ export function normalizeTimestamp(
92
92
  value: unknown,
93
93
  withTimezone: boolean
94
94
  ): Date | unknown {
@@ -105,7 +105,7 @@ function normalizeTimestamp(
105
105
  return value;
106
106
  }
107
107
 
108
- function normalizeDateString(value: unknown): string | unknown {
108
+ export function normalizeDateString(value: unknown): string | unknown {
109
109
  if (value instanceof Date) {
110
110
  return value.toISOString().slice(0, 10);
111
111
  }
@@ -115,7 +115,7 @@ function normalizeDateString(value: unknown): string | unknown {
115
115
  return value;
116
116
  }
117
117
 
118
- function normalizeDateValue(value: unknown): Date | unknown {
118
+ export function normalizeDateValue(value: unknown): Date | unknown {
119
119
  if (value instanceof Date) {
120
120
  return value;
121
121
  }
@@ -125,7 +125,7 @@ function normalizeDateValue(value: unknown): Date | unknown {
125
125
  return value;
126
126
  }
127
127
 
128
- function normalizeTime(value: unknown): string | unknown {
128
+ export function normalizeTime(value: unknown): string | unknown {
129
129
  if (typeof value === 'bigint') {
130
130
  const totalMillis = Number(value) / 1000;
131
131
  const date = new Date(totalMillis);
@@ -137,7 +137,7 @@ function normalizeTime(value: unknown): string | unknown {
137
137
  return value;
138
138
  }
139
139
 
140
- function normalizeInterval(value: unknown): string | unknown {
140
+ export function normalizeInterval(value: unknown): string | unknown {
141
141
  if (
142
142
  value &&
143
143
  typeof value === 'object' &&
package/src/utils.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { aliasFields } from './sql/selection.ts';
2
- export { adaptArrayOperators, queryAdapter } from './sql/query-rewriters.ts';
2
+ export { adaptArrayOperators } from './sql/query-rewriters.ts';
3
3
  export { mapResultRow } from './sql/result-mapper.ts';
@@ -0,0 +1,156 @@
1
+ /**
2
+ * DuckDB wrapper value helpers that are safe for client-side bundles.
3
+ * These utilities only tag values; conversion to native bindings lives
4
+ * in value-wrappers.ts to avoid pulling @duckdb/node-api into browsers.
5
+ */
6
+ export const DUCKDB_VALUE_MARKER = Symbol.for('drizzle-duckdb:value');
7
+
8
+ export type DuckDBValueKind =
9
+ | 'list'
10
+ | 'array'
11
+ | 'struct'
12
+ | 'map'
13
+ | 'timestamp'
14
+ | 'blob'
15
+ | 'json';
16
+
17
+ export interface DuckDBValueWrapper<
18
+ TKind extends DuckDBValueKind = DuckDBValueKind,
19
+ TData = unknown,
20
+ > {
21
+ readonly [DUCKDB_VALUE_MARKER]: true;
22
+ readonly kind: TKind;
23
+ readonly data: TData;
24
+ }
25
+
26
+ export interface ListValueWrapper
27
+ extends DuckDBValueWrapper<'list', unknown[]> {
28
+ readonly elementType?: string;
29
+ }
30
+
31
+ export interface ArrayValueWrapper
32
+ extends DuckDBValueWrapper<'array', unknown[]> {
33
+ readonly elementType?: string;
34
+ readonly fixedLength?: number;
35
+ }
36
+
37
+ export interface StructValueWrapper
38
+ extends DuckDBValueWrapper<'struct', Record<string, unknown>> {
39
+ readonly schema?: Record<string, string>;
40
+ }
41
+
42
+ export interface MapValueWrapper
43
+ extends DuckDBValueWrapper<'map', Record<string, unknown>> {
44
+ readonly valueType?: string;
45
+ }
46
+
47
+ export interface TimestampValueWrapper
48
+ extends DuckDBValueWrapper<'timestamp', Date | string | number | bigint> {
49
+ readonly withTimezone: boolean;
50
+ readonly precision?: number;
51
+ }
52
+
53
+ export interface BlobValueWrapper
54
+ extends DuckDBValueWrapper<'blob', Buffer | Uint8Array> {}
55
+
56
+ export interface JsonValueWrapper extends DuckDBValueWrapper<'json', unknown> {}
57
+
58
+ export type AnyDuckDBValueWrapper =
59
+ | ListValueWrapper
60
+ | ArrayValueWrapper
61
+ | StructValueWrapper
62
+ | MapValueWrapper
63
+ | TimestampValueWrapper
64
+ | BlobValueWrapper
65
+ | JsonValueWrapper;
66
+
67
+ export function isDuckDBWrapper(
68
+ value: unknown
69
+ ): value is AnyDuckDBValueWrapper {
70
+ return (
71
+ value !== null &&
72
+ typeof value === 'object' &&
73
+ DUCKDB_VALUE_MARKER in value &&
74
+ (value as DuckDBValueWrapper)[DUCKDB_VALUE_MARKER] === true
75
+ );
76
+ }
77
+
78
+ export function wrapList(
79
+ data: unknown[],
80
+ elementType?: string
81
+ ): ListValueWrapper {
82
+ return {
83
+ [DUCKDB_VALUE_MARKER]: true,
84
+ kind: 'list',
85
+ data,
86
+ elementType,
87
+ };
88
+ }
89
+
90
+ export function wrapArray(
91
+ data: unknown[],
92
+ elementType?: string,
93
+ fixedLength?: number
94
+ ): ArrayValueWrapper {
95
+ return {
96
+ [DUCKDB_VALUE_MARKER]: true,
97
+ kind: 'array',
98
+ data,
99
+ elementType,
100
+ fixedLength,
101
+ };
102
+ }
103
+
104
+ export function wrapStruct(
105
+ data: Record<string, unknown>,
106
+ schema?: Record<string, string>
107
+ ): StructValueWrapper {
108
+ return {
109
+ [DUCKDB_VALUE_MARKER]: true,
110
+ kind: 'struct',
111
+ data,
112
+ schema,
113
+ };
114
+ }
115
+
116
+ export function wrapMap(
117
+ data: Record<string, unknown>,
118
+ valueType?: string
119
+ ): MapValueWrapper {
120
+ return {
121
+ [DUCKDB_VALUE_MARKER]: true,
122
+ kind: 'map',
123
+ data,
124
+ valueType,
125
+ };
126
+ }
127
+
128
+ export function wrapTimestamp(
129
+ data: Date | string | number | bigint,
130
+ withTimezone: boolean,
131
+ precision?: number
132
+ ): TimestampValueWrapper {
133
+ return {
134
+ [DUCKDB_VALUE_MARKER]: true,
135
+ kind: 'timestamp',
136
+ data,
137
+ withTimezone,
138
+ precision,
139
+ };
140
+ }
141
+
142
+ export function wrapBlob(data: Buffer | Uint8Array): BlobValueWrapper {
143
+ return {
144
+ [DUCKDB_VALUE_MARKER]: true,
145
+ kind: 'blob',
146
+ data,
147
+ };
148
+ }
149
+
150
+ export function wrapJson(data: unknown): JsonValueWrapper {
151
+ return {
152
+ [DUCKDB_VALUE_MARKER]: true,
153
+ kind: 'json',
154
+ data,
155
+ };
156
+ }