Repository navigation
Expand file tree
/
Copy pathsqlitx.source.json
More file actions
360 lines (360 loc) · 277 KB
/
Copy pathsqlitx.source.json
File metadata and controls
360 lines (360 loc) · 277 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
{
"formatVersion": "1.0",
"bundleType": "source-code",
"bundledAt": "2025-02-05T16:29:48.346Z",
"projectRoot": "src",
"files": {
"cache.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport type { StatementSync } from \"node:sqlite\"\n\ninterface CacheItem<V> {\n\tvalue: V\n\texpiry?: number\n}\n\nexport type Options<K, V> = {\n\treadonly maxAge?: number\n\treadonly maxSize: number\n\tonEviction?: (key: K, value: V) => void\n}\n\n// We'll create a base class that doesn't extend Map to avoid the iterator issues\nexport class LRUBase<K, V> {\n\t#size = 0\n\t#cache = new Map<K, CacheItem<V>>()\n\t#oldCache = new Map<K, CacheItem<V>>()\n\t#maxSize: number\n\t#maxAge: number\n\t#onEviction?: (key: K, value: V) => void\n\n\tconstructor(options: Options<K, V>) {\n\t\tif (!(options.maxSize && options.maxSize > 0)) {\n\t\t\tthrow new TypeError(\"`maxSize` must be a number greater than 0\")\n\t\t}\n\n\t\tif (typeof options.maxAge === \"number\" && options.maxAge === 0) {\n\t\t\tthrow new TypeError(\"`maxAge` must be a number greater than 0\")\n\t\t}\n\n\t\tthis.#maxSize = options.maxSize\n\t\tthis.#maxAge = options.maxAge || Number.POSITIVE_INFINITY\n\t\tthis.#onEviction = options.onEviction\n\t}\n\n\tprotected get cache(): Map<K, CacheItem<V>> {\n\t\treturn this.#cache\n\t}\n\n\tprotected get oldCache(): Map<K, CacheItem<V>> {\n\t\treturn this.#oldCache\n\t}\n\n\t#emitEvictions(cache: Map<K, CacheItem<V>>): void {\n\t\tif (typeof this.#onEviction !== \"function\") {\n\t\t\treturn\n\t\t}\n\n\t\tfor (const [key, item] of cache) {\n\t\t\tthis.#onEviction(key, item.value)\n\t\t}\n\t}\n\n\t#deleteIfExpired(key: K, item: CacheItem<V>): boolean {\n\t\tif (typeof item.expiry === \"number\" && item.expiry <= Date.now()) {\n\t\t\tif (typeof this.#onEviction === \"function\") {\n\t\t\t\tthis.#onEviction(key, item.value)\n\t\t\t}\n\n\t\t\treturn this.delete(key)\n\t\t}\n\n\t\treturn false\n\t}\n\n\t#getOrDeleteIfExpired(key: K, item: CacheItem<V>): V | undefined {\n\t\tconst deleted = this.#deleteIfExpired(key, item)\n\t\tif (deleted === false) {\n\t\t\treturn item.value\n\t\t}\n\t\treturn undefined\n\t}\n\n\t#getItemValue(key: K, item: CacheItem<V>): V | undefined {\n\t\treturn item.expiry ? this.#getOrDeleteIfExpired(key, item) : item.value\n\t}\n\n\t#peek(key: K, cache: Map<K, CacheItem<V>>): V | undefined {\n\t\tconst item = cache.get(key)\n\t\tif (!item) {\n\t\t\treturn undefined\n\t\t}\n\t\treturn this.#getItemValue(key, item)\n\t}\n\n\t#set(key: K, value: CacheItem<V>): void {\n\t\tthis.#cache.set(key, value)\n\t\tthis.#size++\n\n\t\tif (this.#size >= this.#maxSize) {\n\t\t\tthis.#size = 0\n\t\t\tthis.#emitEvictions(this.#oldCache)\n\t\t\tthis.#oldCache = this.#cache\n\t\t\tthis.#cache = new Map()\n\t\t}\n\t}\n\n\t#moveToRecent(key: K, item: CacheItem<V>): void {\n\t\tthis.#oldCache.delete(key)\n\t\tthis.#set(key, item)\n\t}\n\n\t*#entriesAscending(): IterableIterator<[K, CacheItem<V>]> {\n\t\tfor (const item of this.#oldCache) {\n\t\t\tconst [key, value] = item\n\t\t\tif (!this.#cache.has(key)) {\n\t\t\t\tconst deleted = this.#deleteIfExpired(key, value)\n\t\t\t\tif (deleted === false) {\n\t\t\t\t\tyield item\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor (const item of this.#cache) {\n\t\t\tconst [key, value] = item\n\t\t\tconst deleted = this.#deleteIfExpired(key, value)\n\t\t\tif (deleted === false) {\n\t\t\t\tyield item\n\t\t\t}\n\t\t}\n\t}\n\n\tget(key: K): V | undefined {\n\t\tif (this.#cache.has(key)) {\n\t\t\tconst item = this.#cache.get(key)\n\t\t\tif (!item) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\treturn this.#getItemValue(key, item)\n\t\t}\n\n\t\tif (this.#oldCache.has(key)) {\n\t\t\tconst item = this.#oldCache.get(key)\n\t\t\tif (!item) {\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t\tif (this.#deleteIfExpired(key, item) === false) {\n\t\t\t\tthis.#moveToRecent(key, item)\n\t\t\t\treturn item.value\n\t\t\t}\n\t\t}\n\t\treturn undefined\n\t}\n\n\tset(key: K, value: V, options: { maxAge?: number } = {}): this {\n\t\tconst maxAge = options.maxAge ?? this.#maxAge\n\n\t\tconst expiry =\n\t\t\ttypeof maxAge === \"number\" && maxAge !== Number.POSITIVE_INFINITY\n\t\t\t\t? Date.now() + maxAge\n\t\t\t\t: undefined\n\n\t\tif (this.#cache.has(key)) {\n\t\t\tthis.#cache.set(key, {\n\t\t\t\tvalue,\n\t\t\t\texpiry,\n\t\t\t})\n\t\t} else {\n\t\t\tthis.#set(key, { value, expiry })\n\t\t}\n\n\t\treturn this\n\t}\n\n\thas(key: K): boolean {\n\t\tif (this.#cache.has(key)) {\n\t\t\tconst item = this.#cache.get(key)\n\t\t\tif (!item) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn !this.#deleteIfExpired(key, item)\n\t\t}\n\n\t\tif (this.#oldCache.has(key)) {\n\t\t\tconst item = this.#oldCache.get(key)\n\t\t\tif (!item) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn !this.#deleteIfExpired(key, item)\n\t\t}\n\n\t\treturn false\n\t}\n\n\tpeek(key: K): V | undefined {\n\t\tif (this.#cache.has(key)) {\n\t\t\treturn this.#peek(key, this.#cache)\n\t\t}\n\n\t\tif (this.#oldCache.has(key)) {\n\t\t\treturn this.#peek(key, this.#oldCache)\n\t\t}\n\t\treturn undefined\n\t}\n\n\tdelete(key: K): boolean {\n\t\tconst deleted = this.#cache.delete(key)\n\t\tif (deleted) {\n\t\t\tthis.#size--\n\t\t}\n\n\t\treturn this.#oldCache.delete(key) || deleted\n\t}\n\n\tclear(): void {\n\t\tthis.#cache.clear()\n\t\tthis.#oldCache.clear()\n\t\tthis.#size = 0\n\t}\n\n\tresize(maxSize: number): void {\n\t\tif (!(maxSize && maxSize > 0)) {\n\t\t\tthrow new TypeError(\"`maxSize` must be a number greater than 0\")\n\t\t}\n\n\t\tconst items = [...this.#entriesAscending()]\n\t\tconst removeCount = items.length - maxSize\n\t\tif (removeCount < 0) {\n\t\t\tthis.#cache = new Map(items)\n\t\t\tthis.#oldCache = new Map()\n\t\t\tthis.#size = items.length\n\t\t} else {\n\t\t\tif (removeCount > 0) {\n\t\t\t\tthis.#emitEvictions(new Map(items.slice(0, removeCount)))\n\t\t\t}\n\n\t\t\tthis.#oldCache = new Map(items.slice(removeCount))\n\t\t\tthis.#cache = new Map()\n\t\t\tthis.#size = 0\n\t\t}\n\n\t\tthis.#maxSize = maxSize\n\t}\n\n\t*entriesDescending(): IterableIterator<[K, V]> {\n\t\tconst cacheItems = [...this.#cache]\n\t\tfor (let i = cacheItems.length - 1; i >= 0; --i) {\n\t\t\tconst item = cacheItems[i]\n\t\t\tif (item) {\n\t\t\t\tconst [key, value] = item\n\t\t\t\tconst deleted = this.#deleteIfExpired(key, value)\n\t\t\t\tif (deleted === false) {\n\t\t\t\t\tyield [key, value.value]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconst oldItems = [...this.#oldCache]\n\t\tfor (let i = oldItems.length - 1; i >= 0; --i) {\n\t\t\tconst item = oldItems[i]\n\t\t\tif (item) {\n\t\t\t\tconst [key, value] = item\n\t\t\t\tif (!this.#cache.has(key)) {\n\t\t\t\t\tconst deleted = this.#deleteIfExpired(key, value)\n\t\t\t\t\tif (deleted === false) {\n\t\t\t\t\t\tyield [key, value.value]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t*entriesAscending(): IterableIterator<[K, V]> {\n\t\tfor (const [key, value] of this.#entriesAscending()) {\n\t\t\tyield [key, value.value]\n\t\t}\n\t}\n\n\tget size(): number {\n\t\tif (!this.#size) {\n\t\t\treturn this.#oldCache.size\n\t\t}\n\n\t\tlet oldCacheSize = 0\n\t\tfor (const key of this.#oldCache.keys()) {\n\t\t\tif (!this.#cache.has(key)) {\n\t\t\t\toldCacheSize++\n\t\t\t}\n\t\t}\n\n\t\treturn Math.min(this.#size + oldCacheSize, this.#maxSize)\n\t}\n\n\tget maxSize(): number {\n\t\treturn this.#maxSize\n\t}\n}\n\n// Create a proxy class that delegates to Map for iterator methods\nexport default class QuickLRU<K, V> implements Map<K, V> {\n\treadonly #base: LRUBase<K, V>\n\treadonly #map: Map<K, V>\n\n\tconstructor(options: Options<K, V>) {\n\t\tthis.#base = new LRUBase(options)\n\t\tthis.#map = new Map()\n\t}\n\n\tclear(): void {\n\t\tthis.#base.clear()\n\t\tthis.#map.clear()\n\t}\n\n\tdelete(key: K): boolean {\n\t\treturn this.#base.delete(key)\n\t}\n\n\tforEach(\n\t\tcallbackfn: (value: V, key: K, map: Map<K, V>) => void,\n\t\tthisArg?: unknown\n\t): void {\n\t\tthis.#map.forEach(callbackfn, thisArg)\n\t}\n\n\tget(key: K): V | undefined {\n\t\treturn this.#base.get(key)\n\t}\n\n\thas(key: K): boolean {\n\t\treturn this.#base.has(key)\n\t}\n\n\tset(key: K, value: V): this {\n\t\tthis.#base.set(key, value)\n\t\tthis.#map.set(key, value)\n\t\treturn this\n\t}\n\n\tget size(): number {\n\t\treturn this.#base.size\n\t}\n\n\tget maxSize(): number {\n\t\treturn this.#base.maxSize\n\t}\n\n\tpeek(key: K): V | undefined {\n\t\treturn this.#base.peek(key)\n\t}\n\n\tresize(maxSize: number): void {\n\t\tthis.#base.resize(maxSize)\n\t}\n\n\t*entriesAscending(): IterableIterator<[K, V]> {\n\t\tyield* this.#base.entriesAscending()\n\t}\n\n\t*entriesDescending(): IterableIterator<[K, V]> {\n\t\tyield* this.#base.entriesDescending()\n\t}\n\n\t// Delegate Map iterator methods to the internal Map instance\n\tentries() {\n\t\treturn this.#map.entries()\n\t}\n\n\tkeys() {\n\t\treturn this.#map.keys()\n\t}\n\n\tvalues() {\n\t\treturn this.#map.values()\n\t}\n\n\t[Symbol.iterator]() {\n\t\treturn this.#map[Symbol.iterator]()\n\t}\n\n\tget [Symbol.toStringTag](): string {\n\t\treturn \"QuickLRU\"\n\t}\n}\n\n// Define the cache options schema\nexport interface StatementCacheOptions {\n\tmaxSize: number\n\tmaxAge?: number\n}\n\nexport interface StatementCache {\n\tget(sql: string): StatementSync | undefined\n\tset(sql: string, statement: StatementSync): void\n\tdelete(sql: string): void\n\tclear(): void\n\tgetStats(): CacheStats\n}\n\nexport interface CacheStats {\n\thits: number\n\tmisses: number\n\tsize: number\n\tevictions: number\n\ttotalQueries: number\n}\n\nclass EnhancedStatementCache implements StatementCache {\n\tprivate cache: QuickLRU<string, StatementSync>\n\tprivate stats: CacheStats = {\n\t\thits: 0,\n\t\tmisses: 0,\n\t\tsize: 0,\n\t\tevictions: 0,\n\t\ttotalQueries: 0,\n\t}\n\n\tconstructor(options: StatementCacheOptions) {\n\t\tthis.cache = new QuickLRU({\n\t\t\tmaxSize: options.maxSize,\n\t\t\tmaxAge: options.maxAge,\n\t\t\tonEviction: () => {\n\t\t\t\tthis.stats.evictions++\n\t\t\t},\n\t\t})\n\t}\n\n\tget(sql: string): StatementSync | undefined {\n\t\tthis.stats.totalQueries++\n\t\tconst stmt = this.cache.get(sql)\n\t\tif (stmt) {\n\t\t\tthis.stats.hits++\n\t\t} else {\n\t\t\tthis.stats.misses++\n\t\t}\n\t\treturn stmt\n\t}\n\n\tset(sql: string, statement: StatementSync): void {\n\t\tthis.cache.set(sql, statement)\n\t\tthis.stats.size = this.cache.size\n\t}\n\n\tdelete(sql: string): void {\n\t\tthis.cache.delete(sql)\n\t\tthis.stats.size = this.cache.size\n\t}\n\n\tclear(): void {\n\t\tthis.cache.clear()\n\t\tthis.stats.size = 0\n\t}\n\n\tgetStats(): CacheStats {\n\t\treturn { ...this.stats }\n\t}\n}\n\nexport function createStatementCache(options?: StatementCacheOptions) {\n\tif (!options) {\n\t\treturn undefined\n\t}\n\n\tif (options.maxSize <= 0) {\n\t\t// Add runtime validation since we removed schema validation\n\t\tthrow new TypeError(\"maxSize must be a positive number\")\n\t}\n\n\tif (\n\t\toptions.maxAge !== undefined &&\n\t\t(options.maxAge <= 0)\n\t) {\n\t\tthrow new TypeError(\"maxAge must be a positive number\")\n\t}\n\n\treturn new EnhancedStatementCache(options)\n}\n",
"metadata": {
"size": 9933,
"modified": 1738725761193.2834,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"cols.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { test, describe } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { buildColsStatement } from \"#context\"\n\ninterface TestUser {\n\tid: number\n\tname: string\n\tmetadata: object\n\tsettings: {\n\t\ttheme: string\n\t\tnotifications: boolean\n\t}\n\tactive: boolean\n}\n\ndescribe(\"buildColsStatement\", () => {\n\ttest(\"handles '*' selector\", () => {\n\t\tconst sql = buildColsStatement<TestUser>(\"*\")\n\t\tassert.equal(sql, \"*\")\n\t})\n\n\ttest(\"handles basic column selection\", () => {\n\t\tconst sql = buildColsStatement<TestUser>([\"id\", \"name\", \"active\"])\n\t\tassert.equal(sql, \"id, name, active\")\n\t})\n\n\ttest(\"handles JSON extraction\", () => {\n\t\tconst sql = buildColsStatement<TestUser>([\"id\", \"metadata<-json\"])\n\t\tassert.equal(sql, \"id, json_extract(metadata, '$') as metadata\")\n\t})\n\n\ttest(\"handles JSON insertion\", () => {\n\t\tconst sql = buildColsStatement<TestUser>([\"id\", \"settings->json\"])\n\t\tassert.equal(sql, \"id, jsonb(settings)\")\n\t})\n\n\ttest(\"handles mixed JSON operations\", () => {\n\t\tconst sql = buildColsStatement<TestUser>([\n\t\t\t\"id\",\n\t\t\t\"metadata<-json\",\n\t\t\t\"settings->json\",\n\t\t\t\"active\",\n\t\t])\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"id, json_extract(metadata, '$') as metadata, jsonb(settings), active\"\n\t\t)\n\t})\n\n\ttest(\"removes duplicate columns while preserving order\", () => {\n\t\tconst sql = buildColsStatement<TestUser>([\n\t\t\t\"id\",\n\t\t\t\"name\",\n\t\t\t\"id\",\n\t\t\t\"metadata<-json\",\n\t\t\t\"name\",\n\t\t\t\"metadata<-json\",\n\t\t])\n\t\tassert.equal(sql, \"id, name, json_extract(metadata, '$') as metadata\")\n\t})\n\n\ttest(\"throws on invalid input\", () => {\n\t\tassert.throws(\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t() => buildColsStatement<TestUser>({} as any),\n\t\t\t{\n\t\t\t\tname: \"NodeSqliteError\",\n\t\t\t\tmessage: /Columns must be '\\*' or an array of columns/,\n\t\t\t}\n\t\t)\n\t})\n})\n\ninterface ComplexTable {\n\tid: number\n\tname: string\n\tmetadata: {\n\t\ttags: string[]\n\t\towner: {\n\t\t\tid: number\n\t\t\trole: string\n\t\t}\n\t}\n\tsettings: {\n\t\ttheme: {\n\t\t\tdark: boolean\n\t\t\tcolors: {\n\t\t\t\tprimary: string\n\t\t\t\tsecondary: string\n\t\t\t}\n\t\t}\n\t\tnotifications: boolean\n\t}\n\tstats: {\n\t\tviews: number\n\t\tlikes: number[]\n\t}\n\tactive: boolean\n}\n\ndescribe(\"buildColsStatement complex scenarios\", () => {\n\ttest(\"handles complex JSON fields\", () => {\n\t\tconst sql = buildColsStatement<ComplexTable>([\n\t\t\t\"id\",\n\t\t\t\"name\",\n\t\t\t\"metadata->json\",\n\t\t\t\"settings<-json\",\n\t\t\t\"stats->json\",\n\t\t\t\"active\",\n\t\t])\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"id, name, jsonb(metadata), json_extract(settings, '$') as settings, jsonb(stats), active\"\n\t\t)\n\t})\n\n\ttest(\"handles multiple JSON extractions\", () => {\n\t\tconst sql = buildColsStatement<ComplexTable>([\n\t\t\t\"metadata<-json\",\n\t\t\t\"settings<-json\",\n\t\t\t\"stats<-json\",\n\t\t])\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"json_extract(metadata, '$') as metadata, json_extract(settings, '$') as settings, json_extract(stats, '$') as stats\"\n\t\t)\n\t})\n\n\ttest(\"handles multiple JSON insertions\", () => {\n\t\tconst sql = buildColsStatement<ComplexTable>([\n\t\t\t\"metadata->json\",\n\t\t\t\"settings->json\",\n\t\t\t\"stats->json\",\n\t\t])\n\t\tassert.equal(sql, \"jsonb(metadata), jsonb(settings), jsonb(stats)\")\n\t})\n})\n",
"metadata": {
"size": 3178,
"modified": 1738725761193.3894,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"context.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { test, describe } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport {\n\tcombineContexts,\n\tvalidateContextCombination,\n\tvalidateSqlContext,\n} from \"./context.js\"\nimport { buildSchema } from \"#schema.js\"\nimport type { Schema, SqlContext } from \"#types.js\"\n\ntype TestUser = {\n\tid: number\n\tname: string\n\tage: number\n\temail: string\n\tcreatedAt: string\n\tisActive: boolean\n\tmetadata: Record<string, unknown>\n}\n\ndescribe(\"SQL Context Validation\", async () => {\n\tdescribe(\"Basic Structure\", () => {\n\t\ttest(\"accepts empty object\", () => {\n\t\t\tconst context = {}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"rejects non-object values\", () => {\n\t\t\tconst values = [null, undefined, 42, \"string\", true, []]\n\t\t\tfor (const value of values) {\n\t\t\t\tconst errors = validateSqlContext<TestUser>(value)\n\t\t\t\tassert.equal(errors.length, 1)\n\t\t\t\tassert.equal(errors[0].message, \"SqlContext must be an object\")\n\t\t\t}\n\t\t})\n\n\t\ttest(\"rejects unknown properties\", () => {\n\t\t\tconst context = {\n\t\t\t\tunknownProp: \"value\",\n\t\t\t\tanotherUnknown: 123,\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 2)\n\t\t\tassert.ok(errors.every(e => e.message.startsWith(\"Unknown property:\")))\n\t\t})\n\t})\n\n\tdescribe(\"Values and Set Validation\", () => {\n\t\ttest(\"accepts '*' for values\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\tvalues: \"*\",\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"accepts valid parameter operators\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\tvalues: [\"$name\", \"$age\", \"$email\"],\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"accepts toJson operators\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\tvalues: [\"$metadata->json\"],\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"accepts valid JSON columns configuration\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\tvalues: [\"*\", { jsonColumns: [\"metadata\"] }],\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"rejects invalid parameter operators\", () => {\n\t\t\tconst context = {\n\t\t\t\tvalues: [\"name\", \"no-at-sign\", \"$invalid.wrong\"],\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.ok(errors.length > 0)\n\t\t\tassert.ok(\n\t\t\t\terrors.every(e =>\n\t\t\t\t\te.message.includes(\"Invalid parameter operator format\")\n\t\t\t\t)\n\t\t\t)\n\t\t})\n\n\t\ttest(\"rejects invalid JSON columns configuration\", () => {\n\t\t\tconst context = {\n\t\t\t\tvalues: [\"*\", { wrongKey: [\"metadata\"] }],\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.ok(errors.length > 0)\n\t\t})\n\t})\n\n\tdescribe(\"Where Clause Validation\", () => {\n\t\ttest(\"accepts valid single condition\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\twhere: \"age != $age\",\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"accepts IS NULL conditions\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\twhere: \"email IS NULL\",\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"accepts valid compound conditions\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\twhere: [\"age != $age\", \"AND\", \"age != $createdAt\"],\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"rejects invalid conditions\", () => {\n\t\t\tconst invalidConditions = [\n\t\t\t\t\"invalid condition\",\n\t\t\t\t\"age >== $minAge\",\n\t\t\t\t\"name LIKES $pattern\",\n\t\t\t\t[\"age >= $minAge\", \"INVALID_OP\", \"name LIKE $pattern\"],\n\t\t\t]\n\n\t\t\tfor (const condition of invalidConditions) {\n\t\t\t\tconst context = { where: condition }\n\t\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\t\tassert.ok(errors.length > 0)\n\t\t\t}\n\t\t})\n\t})\n\n\tdescribe(\"Order By Validation\", () => {\n\t\ttest(\"accepts valid order by clause\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\torderBy: {\n\t\t\t\t\tname: \"ASC\",\n\t\t\t\t\tage: \"DESC\",\n\t\t\t\t},\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"rejects invalid order directions\", () => {\n\t\t\tconst context = {\n\t\t\t\torderBy: {\n\t\t\t\t\tname: \"ASCENDING\",\n\t\t\t\t\tage: \"DOWN\",\n\t\t\t\t},\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 2)\n\t\t\tassert.ok(\n\t\t\t\terrors.every(e => e.message.includes(\"Order direction must be\"))\n\t\t\t)\n\t\t})\n\t})\n\n\tdescribe(\"Limit and Offset Validation\", () => {\n\t\ttest(\"accepts valid limit and offset\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\tlimit: 10,\n\t\t\t\toffset: 20,\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"rejects non-numeric limit and offset\", () => {\n\t\t\tconst context = {\n\t\t\t\tlimit: \"10\",\n\t\t\t\toffset: \"20\",\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 2)\n\t\t\tassert.ok(errors.some(e => e.message.includes(\"limit must be a number\")))\n\t\t\tassert.ok(errors.some(e => e.message.includes(\"offset must be a number\")))\n\t\t})\n\t})\n\n\tdescribe(\"Returning Clause Validation\", () => {\n\t\ttest(\"accepts '*' as returning value\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\treturning: \"*\",\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"accepts array of column names\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\treturning: [\"id\", \"name\", \"age\"],\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"rejects invalid returning values\", () => {\n\t\t\tconst invalidReturning = [\n\t\t\t\t42,\n\t\t\t\t{ columns: [\"id\", \"name\"] },\n\t\t\t\t[\"id\", 42, \"name\"],\n\t\t\t]\n\n\t\t\tfor (const returning of invalidReturning) {\n\t\t\t\tconst context = { returning }\n\t\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\t\tassert.ok(errors.length > 0)\n\t\t\t}\n\t\t})\n\t})\n\n\tdescribe(\"Complex Scenarios\", () => {\n\t\ttest(\"accepts valid complex context\", () => {\n\t\t\tconst context: SqlContext<TestUser> = {\n\t\t\t\tvalues: [\"$name\", \"$age\", \"$metadata->json\"],\n\t\t\t\twhere: [\"age != $createdAt\", \"AND\", \"isActive > $age\"],\n\t\t\t\torderBy: { name: \"ASC\", createdAt: \"DESC\" },\n\t\t\t\tlimit: 10,\n\t\t\t\toffset: 20,\n\t\t\t\treturning: [\"id\", \"name\", \"age\"],\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"accumulates multiple errors\", () => {\n\t\t\tconst context = {\n\t\t\t\tvalues: [\"invalid\", 123],\n\t\t\t\twhere: \"invalid where\",\n\t\t\t\torderBy: { name: \"INVALID\" },\n\t\t\t\tlimit: \"10\",\n\t\t\t\treturning: [42],\n\t\t\t}\n\t\t\tconst errors = validateSqlContext<TestUser>(context)\n\t\t\tassert.ok(errors.length > 3, \"Should collect multiple validation errors\")\n\t\t})\n\t})\n})\n\ndescribe(\"Context Combination Validation\", () => {\n\ttest(\"rejects duplicate unique clauses\", () => {\n\t\tconst contexts: SqlContext<TestUser>[] = [\n\t\t\t{ values: [\"$name\", \"$age\"] },\n\t\t\t{ values: [\"$email\"] },\n\t\t]\n\t\tconst errors = validateContextCombination(contexts)\n\t\tassert.equal(errors.length, 1)\n\t})\n\n\ttest(\"rejects incompatible clause combinations\", () => {\n\t\tconst contexts: SqlContext<TestUser>[] = [\n\t\t\t{ values: [\"$name\"] },\n\t\t\t{\n\t\t\t\tset: {\n\t\t\t\t\tage: \"$age\",\n\t\t\t\t},\n\t\t\t},\n\t\t]\n\t\tconst errors = validateContextCombination(contexts)\n\t\tassert.equal(errors.length, 1)\n\t})\n\n\ttest(\"allows valid combinations\", () => {\n\t\tconst contexts: SqlContext<TestUser>[] = [\n\t\t\t{ where: \"age != $age\" },\n\t\t\t{ orderBy: { name: \"ASC\" } },\n\t\t\t{ limit: 10, offset: 20 },\n\t\t]\n\t\tconst errors = validateContextCombination(contexts)\n\t\tassert.equal(errors.length, 0)\n\t})\n\n\ttest(\"combines where clauses correctly\", () => {\n\t\tconst contexts: SqlContext<TestUser>[] = [\n\t\t\t{ where: \"age != $createdAt\" },\n\t\t\t{ where: \"name LIKE $age\" },\n\t\t]\n\t\tconst combined = combineContexts(contexts)\n\t\tassert.deepEqual(combined.where, [\n\t\t\t\"age != $createdAt\",\n\t\t\t\"AND\",\n\t\t\t\"name LIKE $age\",\n\t\t])\n\t})\n})\n\ndescribe(\"Foreign Key Column Definitions\", () => {\n\ttest(\"validates basic foreign key\", () => {\n\t\tinterface TestTable {\n\t\t\tid: number\n\t\t\tuserId: number\n\t\t}\n\n\t\tconst columns: Schema<TestTable> = {\n\t\t\tid: \"INTEGER PRIMARY KEY\",\n\t\t\tuserId: \"INTEGER NOT NULL\",\n\t\t\t$$foreignKeys: [\n\t\t\t\t{\n\t\t\t\t\tkey: \"userId\",\n\t\t\t\t\treferences: {\n\t\t\t\t\t\ttable: \"users\",\n\t\t\t\t\t\tcolumns: [\"id\"],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t}\n\n\t\tconst sql = buildSchema(columns)\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"(\\n id INTEGER PRIMARY KEY,\\n userId INTEGER NOT NULL,\\n FOREIGN KEY(userId) REFERENCES users(id)\\n)\"\n\t\t)\n\t})\n\n\ttest(\"validates composite foreign key\", () => {\n\t\tinterface TestTable {\n\t\t\tid: number\n\t\t\tfirstName: string\n\t\t\tlastName: string\n\t\t}\n\n\t\tconst columns: Schema<TestTable> = {\n\t\t\tid: \"INTEGER PRIMARY KEY\",\n\t\t\tfirstName: \"TEXT NOT NULL\",\n\t\t\tlastName: \"TEXT NOT NULL\",\n\t\t\t$$foreignKeys: [\n\t\t\t\t{\n\t\t\t\t\tkey: \"firstName,lastName\",\n\t\t\t\t\treferences: {\n\t\t\t\t\t\ttable: \"users\",\n\t\t\t\t\t\tcolumns: [\"first\", \"last\"],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t],\n\t\t}\n\n\t\tconst sql = buildSchema(columns)\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"(\\n id INTEGER PRIMARY KEY,\\n firstName TEXT NOT NULL,\\n lastName TEXT NOT NULL,\\n FOREIGN KEY(firstName, lastName) REFERENCES users(first, last)\\n)\"\n\t\t)\n\t})\n\n\ttest(\"validates foreign key with actions\", () => {\n\t\tinterface TestTable {\n\t\t\tid: number\n\t\t\tdepartmentId: number\n\t\t}\n\n\t\tconst columns: Schema<TestTable> = {\n\t\t\tid: \"INTEGER PRIMARY KEY\",\n\t\t\tdepartmentId: \"INTEGER NOT NULL\",\n\t\t\t$$foreignKeys: [\n\t\t\t\t{\n\t\t\t\t\tkey: \"departmentId\",\n\t\t\t\t\treferences: {\n\t\t\t\t\t\ttable: \"departments\",\n\t\t\t\t\t\tcolumns: [\"id\"],\n\t\t\t\t\t},\n\t\t\t\t\tonDelete: \"CASCADE\",\n\t\t\t\t\tonUpdate: \"SET NULL\",\n\t\t\t\t},\n\t\t\t],\n\t\t}\n\n\t\tconst sql = buildSchema(columns)\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"(\\n id INTEGER PRIMARY KEY,\\n departmentId INTEGER NOT NULL,\\n FOREIGN KEY(departmentId) REFERENCES departments(id) ON DELETE CASCADE ON UPDATE SET NULL\\n)\"\n\t\t)\n\t})\n\n\ttest(\"validates multiple foreign keys\", () => {\n\t\tinterface TestTable {\n\t\t\tid: number\n\t\t\tuserId: number\n\t\t\tgroupId: number\n\t\t}\n\n\t\tconst columns: Schema<TestTable> = {\n\t\t\tid: \"INTEGER PRIMARY KEY\",\n\t\t\tuserId: \"INTEGER NOT NULL\",\n\t\t\tgroupId: \"INTEGER NOT NULL\",\n\t\t\t$$foreignKeys: [\n\t\t\t\t{\n\t\t\t\t\tkey: \"userId\",\n\t\t\t\t\treferences: {\n\t\t\t\t\t\ttable: \"users\",\n\t\t\t\t\t\tcolumns: [\"id\"],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tkey: \"groupId\",\n\t\t\t\t\treferences: {\n\t\t\t\t\t\ttable: \"groups\",\n\t\t\t\t\t\tcolumns: [\"id\"],\n\t\t\t\t\t},\n\t\t\t\t\tonDelete: \"CASCADE\",\n\t\t\t\t},\n\t\t\t],\n\t\t}\n\n\t\tconst sql = buildSchema(columns)\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"(\\n id INTEGER PRIMARY KEY,\\n userId INTEGER NOT NULL,\\n groupId INTEGER NOT NULL,\\n FOREIGN KEY(userId) REFERENCES users(id),\\n FOREIGN KEY(groupId) REFERENCES groups(id) ON DELETE CASCADE\\n)\"\n\t\t)\n\t})\n\n\ttest(\"validates deferrable foreign key\", () => {\n\t\tinterface TestTable {\n\t\t\tid: number\n\t\t\tparentId: number\n\t\t}\n\n\t\tconst columns: Schema<TestTable> = {\n\t\t\tid: \"INTEGER PRIMARY KEY\",\n\t\t\tparentId: \"INTEGER\",\n\t\t\t$$foreignKeys: [\n\t\t\t\t{\n\t\t\t\t\tkey: \"parentId\",\n\t\t\t\t\treferences: {\n\t\t\t\t\t\ttable: \"test_table\",\n\t\t\t\t\t\tcolumns: [\"id\"],\n\t\t\t\t\t},\n\t\t\t\t\tdeferrable: \"DEFERRABLE INITIALLY DEFERRED\",\n\t\t\t\t},\n\t\t\t],\n\t\t}\n\n\t\tconst sql = buildSchema(columns)\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"(\\n id INTEGER PRIMARY KEY,\\n parentId INTEGER,\\n FOREIGN KEY(parentId) REFERENCES test_table(id) DEFERRABLE INITIALLY DEFERRED\\n)\"\n\t\t)\n\t})\n})\n",
"metadata": {
"size": 11452,
"modified": 1738725761193.523,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"context.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { validateSchema } from \"#schema.js\"\nimport { NodeSqliteError, SqlitePrimaryResultCode } from \"#errors\"\nimport {\n\ttype ColumnOptions,\n\tisRawValue,\n\ttype SqlContext,\n\ttype DataRow,\n\ttype WhereClause,\n} from \"#types\"\nimport { validationErr, type ValidationError } from \"#validate\"\nimport { validateWhereClause } from \"#where\"\n\nexport function validateSqlContext<P extends DataRow, R = P>(\n\tvalue: unknown\n): ValidationError[] {\n\tconst errors: ValidationError[] = []\n\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\treturn [validationErr({ msg: \"SqlContext must be an object\" })]\n\t}\n\n\tconst context = value as Record<string, unknown>\n\n\tfor (const key in context) {\n\t\tswitch (key) {\n\t\t\tcase \"values\": {\n\t\t\t\tconst valueErrors = validateInsertOrSetOptions<P>(context[key])\n\t\t\t\tif (valueErrors.length > 0) {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\t...valueErrors.map(err => ({\n\t\t\t\t\t\t\t...err,\n\t\t\t\t\t\t\tpath: `values${err.path ? `.${err.path}` : \"\"}`,\n\t\t\t\t\t\t}))\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcase \"set\": {\n\t\t\t\tconst setErrors = validateSetOptions<P>(context[key])\n\t\t\t\tif (setErrors.length > 0) {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\t...setErrors.map(err => ({\n\t\t\t\t\t\t\t...err,\n\t\t\t\t\t\t\tpath: `set${err.path ? `.${err.path}` : \"\"}`,\n\t\t\t\t\t\t}))\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tcase \"columns\": {\n\t\t\t\tconst value = context[key]\n\t\t\t\tif (value !== \"*\" && !Array.isArray(value)) {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\tmsg: \"columns must be '*' or an array\",\n\t\t\t\t\t\t\tpath: \"columns\",\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t} else if (Array.isArray(value)) {\n\t\t\t\t\tif (!value.every(item => typeof item === \"string\")) {\n\t\t\t\t\t\terrors.push(\n\t\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\t\tmsg: \"columns array must contain only strings\",\n\t\t\t\t\t\t\t\tpath: \"columns\",\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t\t// Validate format of each column spec\n\t\t\t\t\tvalue.forEach((col, index) => {\n\t\t\t\t\t\tif (!isValidColumnSpec(col)) {\n\t\t\t\t\t\t\terrors.push(\n\t\t\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\t\t\tmsg: \"Invalid column format\",\n\t\t\t\t\t\t\t\t\tpath: `columns[${index}]`,\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tcase \"where\": {\n\t\t\t\tconst whereErrors = validateWhereClause<P>(\n\t\t\t\t\tcontext[key] as WhereClause<P>\n\t\t\t\t)\n\t\t\t\tif (whereErrors.length > 0) {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\t...whereErrors.map(err => ({\n\t\t\t\t\t\t\t...err,\n\t\t\t\t\t\t\tpath: `where${err.path ? `.${err.path}` : \"\"}`,\n\t\t\t\t\t\t}))\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tcase \"orderBy\": {\n\t\t\t\tconst orderErrors = validateOrderByClause(context[key])\n\t\t\t\tif (orderErrors.length > 0) {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\t...orderErrors.map(err => ({\n\t\t\t\t\t\t\t...err,\n\t\t\t\t\t\t\tpath: `orderBy${err.path ? `.${err.path}` : \"\"}`,\n\t\t\t\t\t\t}))\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tcase \"limit\":\n\t\t\tcase \"offset\":\n\t\t\t\tif (typeof context[key] !== \"number\") {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\tmsg: `${key} must be a number`,\n\t\t\t\t\t\t\tpath: key,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\tcase \"schema\": {\n\t\t\t\tconst columnErrors = validateSchema<P>(context[key])\n\t\t\t\tif (columnErrors.length > 0) {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\t...columnErrors.map(err => ({\n\t\t\t\t\t\t\t...err,\n\t\t\t\t\t\t\tpath: `schema${err.path ? `.${err.path}` : \"\"}`,\n\t\t\t\t\t\t}))\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tcase \"returning\": {\n\t\t\t\tconst value = context[key]\n\t\t\t\tif (value !== \"*\" && !Array.isArray(value)) {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\tmsg: \"returning must be '*' or an array\",\n\t\t\t\t\t\t\tpath: \"returning\",\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t} else if (Array.isArray(value)) {\n\t\t\t\t\t// Check for [\"*\", { jsonColumns: [...] }] format\n\t\t\t\t\tif (value.length === 2 && value[0] === \"*\") {\n\t\t\t\t\t\tconst config = value[1]\n\t\t\t\t\t\tif (!isJsonColumns(config)) {\n\t\t\t\t\t\t\terrors.push(\n\t\t\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\t\t\tmsg: \"jsonColumns must be a non-empty array of strings\",\n\t\t\t\t\t\t\t\t\tpath: \"returning[1]\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Regular column array validation\n\t\t\t\t\t\tif (!value.every(item => typeof item === \"string\")) {\n\t\t\t\t\t\t\terrors.push(\n\t\t\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\t\t\tmsg: \"returning array must contain only strings\",\n\t\t\t\t\t\t\t\t\tpath: \"returning\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// Check for duplicates\n\t\t\t\t\t\tconst seen = new Set<string>()\n\t\t\t\t\t\tconst duplicates = value.filter(item => {\n\t\t\t\t\t\t\tif (seen.has(item)) {\n\t\t\t\t\t\t\t\treturn true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tseen.add(item)\n\t\t\t\t\t\t\treturn false\n\t\t\t\t\t\t})\n\t\t\t\t\t\tif (duplicates.length > 0) {\n\t\t\t\t\t\t\terrors.push(\n\t\t\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\t\t\tmsg: `Duplicate columns in RETURNING clause: ${duplicates.join(\", \")}`,\n\t\t\t\t\t\t\t\t\tpath: \"returning\",\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\terrors.push(\n\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\tmsg: `Unknown property: ${key}`,\n\t\t\t\t\t\tpath: key,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t}\n\t}\n\n\treturn errors\n}\n\nfunction isValidColumnSpec(value: string): boolean {\n\treturn (\n\t\t!value.includes(\" \") && // No spaces allowed\n\t\t(value.endsWith(\"->json\") ||\n\t\t\tvalue.endsWith(\"<-json\") ||\n\t\t\t!value.includes(\"->\")) // Basic column or JSON operation\n\t)\n}\n\nfunction validateInsertOrSetOptions<P extends DataRow>(\n\tvalue: unknown\n): ValidationError[] {\n\tif (value === \"*\") {\n\t\treturn []\n\t}\n\n\tif (!Array.isArray(value)) {\n\t\treturn [validationErr({ msg: \"Must be '*' or an array\" })]\n\t}\n\n\t// Check if it's a tuple with configuration\n\tif (value.length === 2 && value[0] === \"*\") {\n\t\tconst [, config] = value\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\treturn [\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Second element must be a configuration object\",\n\t\t\t\t\tpath: \"[1]\",\n\t\t\t\t}),\n\t\t\t]\n\t\t}\n\n\t\t// Check for at least one valid config option\n\t\tif (!(\"jsonColumns\" in config) && !(\"batch\" in config)) {\n\t\t\treturn [\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Configuration must include either jsonColumns or batch\",\n\t\t\t\t\tpath: \"[1]\",\n\t\t\t\t}),\n\t\t\t]\n\t\t}\n\n\t\tif (\n\t\t\t\"jsonColumns\" in config &&\n\t\t\t(!Array.isArray(config.jsonColumns) ||\n\t\t\t\t!config.jsonColumns.every((col: unknown) => typeof col === \"string\"))\n\t\t) {\n\t\t\treturn [\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"jsonColumns must be an array of strings\",\n\t\t\t\t\tpath: \"[1].jsonColumns\",\n\t\t\t\t}),\n\t\t\t]\n\t\t}\n\n\t\tif (\"batch\" in config && typeof config.batch !== \"boolean\") {\n\t\t\treturn [\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"batch must be a boolean value\",\n\t\t\t\t\tpath: \"[1].batch\",\n\t\t\t\t}),\n\t\t\t]\n\t\t}\n\n\t\treturn []\n\t}\n\n\t// Validate parameter operators array\n\tconst errors: ValidationError[] = []\n\tvalue.forEach((item, index) => {\n\t\tif (typeof item !== \"string\") {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Parameter operator must be a string\",\n\t\t\t\t\tpath: `[${index}]`,\n\t\t\t\t})\n\t\t\t)\n\t\t} else if (!isValidValueType(item)) {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Invalid parameter operator format\",\n\t\t\t\t\tpath: `[${index}]`,\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t})\n\n\treturn errors\n}\n\nfunction validateOrderByClause(value: unknown): ValidationError[] {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn [\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"orderBy must be an object\",\n\t\t\t}),\n\t\t]\n\t}\n\n\tconst errors: ValidationError[] = []\n\tfor (const [key, direction] of Object.entries(value)) {\n\t\tif (direction !== \"ASC\" && direction !== \"DESC\") {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Order direction must be 'ASC' or 'DESC'\",\n\t\t\t\t\tpath: key,\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t}\n\n\treturn errors\n}\n\nfunction isValidValueType(value: string): boolean {\n\treturn (\n\t\tvalue.startsWith(\"$\") &&\n\t\t(value.includes(\".toJson\") ? value.endsWith(\".toJson\") : true)\n\t)\n}\n\nexport function isSqlContext<P extends DataRow, R = P>(\n\tvalue: unknown\n): value is SqlContext<P, R> {\n\treturn validateSqlContext<P>(value).length === 0\n}\n\ntype ContextValidationError = {\n\ttype: \"DUPLICATE_CLAUSE\" | \"INCOMPATIBLE_CLAUSE\" | \"INVALID_COMBINATION\"\n\tmessage: string\n\tclauses?: string[]\n}\n\nexport function validateContextCombination<P extends DataRow, R = P>(\n\tcontexts: SqlContext<P, R>[]\n): ContextValidationError[] {\n\tconst errors: ContextValidationError[] = []\n\n\t// Track which clauses we've seen\n\tconst seenClauses = new Set<keyof SqlContext<P>>()\n\n\t// These clauses can only appear once\n\tconst uniqueClauses = new Set([\n\t\t\"values\",\n\t\t\"set\",\n\t\t\"returning\",\n\t\t\"limit\",\n\t\t\"offset\",\n\t])\n\n\t// Track clause combinations that don't make sense together\n\tconst incompatiblePairs = new Map([\n\t\t[\"values\", new Set([\"set\"])],\n\t\t[\"set\", new Set([\"values\"])],\n\t])\n\n\t// Check for duplicate clauses and track what we've seen\n\tfor (const context of contexts) {\n\t\tfor (const [clause, value] of Object.entries(context)) {\n\t\t\tif (value === undefined) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconst clauseKey = clause as keyof SqlContext<P, R>\n\n\t\t\t// Check if this is a unique clause that we've seen before\n\t\t\tif (uniqueClauses.has(clauseKey) && seenClauses.has(clauseKey)) {\n\t\t\t\terrors.push({\n\t\t\t\t\ttype: \"DUPLICATE_CLAUSE\",\n\t\t\t\t\tmessage: `Clause \"${clause}\" cannot appear multiple times in a SQL statement`,\n\t\t\t\t\tclauses: [clause],\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Check for incompatible clause combinations\n\t\t\tconst incompatibleWith = incompatiblePairs.get(clause)\n\t\t\tif (incompatibleWith) {\n\t\t\t\tfor (const otherClause of incompatibleWith) {\n\t\t\t\t\tif (seenClauses.has(otherClause as keyof SqlContext<P, R>)) {\n\t\t\t\t\t\terrors.push({\n\t\t\t\t\t\t\ttype: \"INCOMPATIBLE_CLAUSE\",\n\t\t\t\t\t\t\tmessage: `Clauses \"${clause}\" and \"${otherClause}\" cannot be used together`,\n\t\t\t\t\t\t\tclauses: [clause, otherClause],\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tseenClauses.add(clauseKey)\n\t\t}\n\t}\n\n\treturn errors\n}\n\nexport function combineContexts<P extends DataRow, R = P>(\n\tcontexts: SqlContext<P, R>[]\n): SqlContext<P, R> {\n\t// First validate the combination\n\tconst errors = validateContextCombination(contexts)\n\tif (errors.length > 0) {\n\t\tthrow new NodeSqliteError(\n\t\t\t\"ERR_SQLITE_CONTEXT\",\n\t\t\tSqlitePrimaryResultCode.SQLITE_MISUSE,\n\t\t\t\"Invalid SQL context combination\",\n\t\t\terrors.map(e => e.message).join(\"\\n\"),\n\t\t\tundefined\n\t\t)\n\t}\n\n\t// Helper function to combine where clauses safely\n\tconst combineWhereClauses = (\n\t\tclause1: WhereClause<P> | undefined,\n\t\tclause2: WhereClause<P> | undefined\n\t): WhereClause<P> | undefined => {\n\t\tif (!clause1) {\n\t\t\treturn clause2\n\t\t}\n\t\tif (!clause2) {\n\t\t\treturn clause1\n\t\t}\n\n\t\tconst clause1Array = Array.isArray(clause1) ? clause1 : [clause1]\n\t\tconst clause2Array = Array.isArray(clause2) ? clause2 : [clause2]\n\n\t\t// Ensure we're not exceeding the maximum allowed conditions\n\t\treturn [...clause1Array, \"AND\", ...clause2Array] as WhereClause<P>\n\t}\n\n\t// Helper function to combine orderBy clauses safely\n\tconst combineOrderByClauses = (\n\t\torderBy1: Partial<Record<keyof P, \"ASC\" | \"DESC\">> | undefined,\n\t\torderBy2: Partial<Record<keyof P, \"ASC\" | \"DESC\">> | undefined\n\t): Partial<Record<keyof P, \"ASC\" | \"DESC\">> | undefined => {\n\t\tif (!orderBy1) {\n\t\t\treturn orderBy2\n\t\t}\n\t\tif (!orderBy2) {\n\t\t\treturn orderBy1\n\t\t}\n\n\t\treturn {\n\t\t\t...orderBy1,\n\t\t\t...orderBy2,\n\t\t} as Partial<Record<keyof P, \"ASC\" | \"DESC\">>\n\t}\n\n\treturn contexts.reduce<SqlContext<P, R>>(\n\t\t(combined, current) => {\n\t\t\t// Create new object with explicit property assignments\n\t\t\tconst result: SqlContext<P, R> = {}\n\n\t\t\t// Assign values from combined if they exist\n\t\t\t// sourcery skip: use-braces\n\t\t\tif (combined.values !== undefined) result.values = combined.values\n\t\t\tif (combined.set !== undefined) result.set = combined.set\n\t\t\tif (combined.limit !== undefined) result.limit = combined.limit\n\t\t\tif (combined.offset !== undefined) result.offset = combined.offset\n\t\t\tif (combined.returning !== undefined)\n\t\t\t\tresult.returning = combined.returning\n\n\t\t\t// Assign values from current if they exist\n\t\t\tif (current.values !== undefined) result.values = current.values\n\t\t\tif (current.set !== undefined) result.set = current.set\n\t\t\tif (current.limit !== undefined) result.limit = current.limit\n\t\t\tif (current.offset !== undefined) result.offset = current.offset\n\t\t\tif (current.returning !== undefined) result.returning = current.returning\n\n\t\t\t// Handle special cases with combine functions\n\t\t\tresult.where = combineWhereClauses(combined.where, current.where)\n\t\t\tresult.orderBy = combineOrderByClauses(combined.orderBy, current.orderBy)\n\n\t\t\treturn result\n\t\t},\n\t\t{} as SqlContext<P, R>\n\t)\n}\n\nexport function buildColsStatement<P extends DataRow>(\n\tcols: ColumnOptions<P>\n): string {\n\tif (cols === \"*\") {\n\t\treturn \"*\"\n\t}\n\n\t// Handle array of column specifications\n\tif (Array.isArray(cols)) {\n\t\t// Remove duplicates while preserving order\n\t\tconst seen = new Set<string>()\n\t\treturn cols\n\t\t\t.filter(col => {\n\t\t\t\tconst colStr = String(col)\n\t\t\t\tif (seen.has(colStr)) {\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\tseen.add(colStr)\n\t\t\t\treturn true\n\t\t\t})\n\t\t\t.map(col => {\n\t\t\t\tif (typeof col === \"string\") {\n\t\t\t\t\tif (col.endsWith(\"->json\")) {\n\t\t\t\t\t\tconst columnName = col.slice(0, -6)\n\t\t\t\t\t\treturn `jsonb(${columnName})`\n\t\t\t\t\t}\n\t\t\t\t\tif (col.endsWith(\"<-json\")) {\n\t\t\t\t\t\tconst columnName = col.slice(0, -6)\n\t\t\t\t\t\treturn `json_extract(${columnName}, '$') as ${columnName}`\n\t\t\t\t\t}\n\t\t\t\t\treturn col\n\t\t\t\t}\n\t\t\t\treturn String(col)\n\t\t\t})\n\t\t\t.join(\", \")\n\t}\n\n\tthrow new NodeSqliteError(\n\t\t\"ERR_SQLITE_PARAM\",\n\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\"Invalid columns format\",\n\t\t\"Columns must be '*' or an array of columns\",\n\t\tundefined\n\t)\n}\n\nexport function validateSetOptions<P extends DataRow>(\n\tvalue: unknown\n): ValidationError[] {\n\tconst errors: ValidationError[] = []\n\n\t// Handle \"*\" case\n\tif (value === \"*\") {\n\t\treturn []\n\t}\n\n\t// Handle [\"*\", { jsonColumns: [...] }] case\n\tif (Array.isArray(value)) {\n\t\tif (value.length !== 2 || value[0] !== \"*\") {\n\t\t\treturn [\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Array format must be ['*', { jsonColumns: [...] }]\",\n\t\t\t\t\tpath: \"\",\n\t\t\t\t}),\n\t\t\t]\n\t\t}\n\n\t\tconst [, config] = value\n\t\tif (!config || typeof config !== \"object\" || !(\"jsonColumns\" in config)) {\n\t\t\treturn [\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Second element must be an object with jsonColumns\",\n\t\t\t\t\tpath: \"[1]\",\n\t\t\t\t}),\n\t\t\t]\n\t\t}\n\n\t\tconst { jsonColumns } = config\n\t\tif (!Array.isArray(jsonColumns) || jsonColumns.length === 0) {\n\t\t\treturn [\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"jsonColumns must be a non-empty array\",\n\t\t\t\t\tpath: \"[1].jsonColumns\",\n\t\t\t\t}),\n\t\t\t]\n\t\t}\n\n\t\tif (!jsonColumns.every(col => typeof col === \"string\")) {\n\t\t\treturn [\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"jsonColumns must contain only strings\",\n\t\t\t\t\tpath: \"[1].jsonColumns\",\n\t\t\t\t}),\n\t\t\t]\n\t\t}\n\n\t\treturn []\n\t}\n\n\t// Handle object format\n\tif (typeof value === \"object\" && value !== null) {\n\t\tconst entries = Object.entries(value)\n\n\t\tfor (const [key, val] of entries) {\n\t\t\tif (isRawValue(val)) {\n\t\t\t\t// RawValue is always valid\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (typeof val !== \"string\") {\n\t\t\t\terrors.push(\n\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\tmsg: `Value for '${key}' must be a string parameter or RawValue`,\n\t\t\t\t\t\tpath: key,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Validate parameter format\n\t\t\tif (val.startsWith(\"$\")) {\n\t\t\t\t// Check for ->json format\n\t\t\t\tif (val.endsWith(\"->json\")) {\n\t\t\t\t\tconst paramPart = val.slice(1, -6)\n\t\t\t\t\tif (!paramPart) {\n\t\t\t\t\t\terrors.push(\n\t\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\t\tmsg: \"Invalid JSON parameter format\",\n\t\t\t\t\t\t\t\tpath: key,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t// Regular parameter\n\t\t\t\t\tconst paramPart = val.slice(1)\n\t\t\t\t\tif (!paramPart) {\n\t\t\t\t\t\terrors.push(\n\t\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\t\tmsg: \"Invalid parameter format\",\n\t\t\t\t\t\t\t\tpath: key,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\terrors.push(\n\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\tmsg: \"Value must start with $ or be a RawValue\",\n\t\t\t\t\t\tpath: key,\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\treturn errors\n\t}\n\n\treturn [\n\t\tvalidationErr({\n\t\t\tmsg: \"SET must be an object, '*', or ['*', { jsonColumns: [...] }]\",\n\t\t\tpath: \"\",\n\t\t}),\n\t]\n}\n\nexport const isJsonColumns = (\n\tvalue: unknown\n): value is { jsonColumns: string[] } =>\n\ttypeof value === \"object\" &&\n\tvalue !== null &&\n\tArray.isArray((value as { jsonColumns: unknown }).jsonColumns) &&\n\t(value as { jsonColumns: unknown[] }).jsonColumns.length > 0 &&\n\t(value as { jsonColumns: unknown[] }).jsonColumns.every(\n\t\tcol => typeof col === \"string\"\n\t)\n",
"metadata": {
"size": 15741,
"modified": 1738725761193.6345,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"database.json.test.ts": {
"content": "import { test, beforeEach, afterEach } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { DB } from \"#database\"\n\nlet db: DB\n\nbeforeEach(() => {\n\tdb = new DB({\n\t\tlocation: \":memory:\",\n\t\tenvironment: \"testing\",\n\t})\n\n\tdb.exec(`\n CREATE TABLE json_test (\n id INTEGER PRIMARY KEY,\n simple_object BLOB,\n nested_object BLOB,\n array_data BLOB,\n mixed_data BLOB,\n nullable_json BLOB\n )\n `)\n})\n\nafterEach(() => {\n\tdb.close()\n})\n\ntest(\"handles simple object JSON storage and retrieval\", { only: true }, () => {\n\tinterface SimpleObject {\n\t\tname: string\n\t\tage: number\n\t\tactive: boolean\n\t}\n\n\tconst simpleObject: SimpleObject = {\n\t\tname: \"John\",\n\t\tage: 30,\n\t\tactive: true,\n\t}\n\n\tconst insertData = db.sql<{ data: SimpleObject }>`\n INSERT INTO json_test (simple_object)\n VALUES (${\"$data->json\"})\n `\n\n\tinsertData.run({ data: simpleObject })\n\n\tconst getData = db.sql<Record<string, never>>`\n SELECT json_extract(simple_object, '$') as data\n FROM json_test\n `\n\n\tconst result = getData.all<{ data: SimpleObject }>()\n\tassert.deepEqual(result[0].data, simpleObject)\n})\n\ntest(\"handles nested object JSON storage and retrieval\", () => {\n\tinterface NestedObject {\n\t\tuser: {\n\t\t\tname: string\n\t\t\taddress: {\n\t\t\t\tstreet: string\n\t\t\t\tcity: string\n\t\t\t\tcoords: { lat: number; lng: number }\n\t\t\t}\n\t\t}\n\t\tpreferences: {\n\t\t\ttheme: {\n\t\t\t\tdark: boolean\n\t\t\t\tcolors: { primary: string; secondary: string }\n\t\t\t}\n\t\t}\n\t}\n\n\tconst nestedObject: NestedObject = {\n\t\tuser: {\n\t\t\tname: \"John\",\n\t\t\taddress: {\n\t\t\t\tstreet: \"123 Main St\",\n\t\t\t\tcity: \"Boston\",\n\t\t\t\tcoords: {\n\t\t\t\t\tlat: 42.3601,\n\t\t\t\t\tlng: -71.0589,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tpreferences: {\n\t\t\ttheme: {\n\t\t\t\tdark: true,\n\t\t\t\tcolors: {\n\t\t\t\t\tprimary: \"#000000\",\n\t\t\t\t\tsecondary: \"#ffffff\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tconst insertData = db.sql<{ data: NestedObject }>`\n INSERT INTO json_test (nested_object)\n VALUES (${\"$data->json\"})\n `\n\n\tinsertData.run({ data: nestedObject })\n\n\tconst getData = db.sql<Record<string, never>>`\n SELECT json_extract(nested_object, '$') as data\n FROM json_test\n `\n\n\tconst result = getData.all<{ data: NestedObject }>()\n\tassert.deepEqual(result[0].data, nestedObject)\n})\n\ntest(\"handles multiple row JSON operations\", () => {\n\tinterface RowData {\n\t\tid: number\n\t\tvalue: string\n\t}\n\n\tconst rows: RowData[] = [\n\t\t{ id: 1, value: \"first\" },\n\t\t{ id: 2, value: \"second\" },\n\t]\n\n\tdb.exec(`\n CREATE TABLE multi_test (\n id INTEGER PRIMARY KEY,\n data JSON,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `)\n\n\tconst insertRows = db.sql<{ data: RowData }>`\n INSERT INTO multi_test (data)\n VALUES (${\"$data->json\"})\n `\n\n\tfor (const row of rows) {\n\t\tinsertRows.run({ data: row })\n\t}\n\n\tconst getRows = db.sql<Record<string, never>>`\n SELECT\n id,\n json_extract(data, '$') as data,\n created_at\n FROM multi_test\n ORDER BY id\n `\n\n\tconst result = getRows.all<{ id: number; data: RowData; created_at: string }>(\n\t\t{}\n\t)\n\tassert.equal(result.length, 2)\n\tassert.deepEqual(result[0].data, rows[0])\n\tassert.deepEqual(result[1].data, rows[1])\n})\n\ntest(\"handles JSON path queries\", () => {\n\tinterface TestData {\n\t\tusers: Array<{\n\t\t\tid: number\n\t\t\tname: string\n\t\t\tsettings: { theme: string }\n\t\t}>\n\t\tconfig: {\n\t\t\tversion: string\n\t\t\tfeatures: {\n\t\t\t\tflag1: boolean\n\t\t\t\tflag2: boolean\n\t\t\t}\n\t\t}\n\t}\n\n\tconst data: TestData = {\n\t\tusers: [\n\t\t\t{ id: 1, name: \"John\", settings: { theme: \"dark\" } },\n\t\t\t{ id: 2, name: \"Jane\", settings: { theme: \"light\" } },\n\t\t],\n\t\tconfig: {\n\t\t\tversion: \"1.0\",\n\t\t\tfeatures: {\n\t\t\t\tflag1: true,\n\t\t\t\tflag2: false,\n\t\t\t},\n\t\t},\n\t}\n\n\tconst insertData = db.sql<{ data: TestData }>`\n INSERT INTO json_test (nested_object)\n VALUES (${\"$data->json\"})\n `\n\n\tinsertData.all({ data })\n\n\tconst getData = db.sql<Record<string, never>>`\n SELECT\n json_extract(nested_object, '$') as data,\n json_extract(nested_object, '$.users[0].name') as first_user_name,\n json_extract(nested_object, '$.config.version') as version,\n json_extract(nested_object, '$.users[1].settings.theme') as second_user_theme,\n json_extract(nested_object, '$.config.features.flag1') as feature_flag\n FROM json_test\n `\n\n\tconst result = getData.all<{\n\t\tdata: TestData\n\t\tfirst_user_name: string\n\t\tversion: string\n\t\tsecond_user_theme: string\n\t\tfeature_flag: number\n\t}>()\n\n\tassert.deepEqual(result[0].data, data)\n\tassert.strictEqual(result[0].first_user_name, \"John\")\n\tassert.strictEqual(result[0].version, \"1.0\")\n\tassert.strictEqual(result[0].second_user_theme, \"light\")\n\tassert.strictEqual(result[0].feature_flag, 1)\n})\n\ntest(\"handles array of objects with mixed types\", () => {\n\tinterface ComplexArray {\n\t\titems: Array<{\n\t\t\tid: number\n\t\t\tname: string\n\t\t\tmetadata: {\n\t\t\t\ttags: string[]\n\t\t\t\tcounts: { [key: string]: number }\n\t\t\t\tactive: boolean\n\t\t\t\tlastUpdated: string\n\t\t\t}\n\t\t}>\n\t}\n\n\tconst testData: ComplexArray = {\n\t\titems: [\n\t\t\t{\n\t\t\t\tid: 1,\n\t\t\t\tname: \"Item 1\",\n\t\t\t\tmetadata: {\n\t\t\t\t\ttags: [\"important\", \"urgent\"],\n\t\t\t\t\tcounts: { views: 100, shares: 50 },\n\t\t\t\t\tactive: true,\n\t\t\t\t\tlastUpdated: \"2025-01-20\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tid: 2,\n\t\t\t\tname: \"Item 2\",\n\t\t\t\tmetadata: {\n\t\t\t\t\ttags: [\"archived\"],\n\t\t\t\t\tcounts: { views: 75, shares: 25 },\n\t\t\t\t\tactive: false,\n\t\t\t\t\tlastUpdated: \"2025-01-19\",\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t}\n\n\tconst insertData = db.sql<{ data: ComplexArray }>`\n INSERT INTO json_test (array_data)\n VALUES (${\"$data->json\"})\n `\n\n\tinsertData.run({ data: testData })\n\n\tconst getData = db.sql<Record<string, never>>`\n SELECT\n json_extract(array_data, '$') as full_data,\n json_extract(array_data, '$.items[0].metadata.tags[0]') as first_tag,\n json_extract(array_data, '$.items[0].metadata.counts.views') as view_count,\n json_extract(array_data, '$.items[1].metadata.active') as is_active\n FROM json_test\n `\n\n\tconst result = getData.all<{\n\t\tfull_data: ComplexArray\n\t\tfirst_tag: string\n\t\tview_count: number\n\t\tis_active: number\n\t}>()\n\n\tassert.deepEqual(result[0].full_data, testData)\n\tassert.strictEqual(result[0].first_tag, \"important\")\n\tassert.strictEqual(result[0].view_count, 100)\n\tassert.strictEqual(result[0].is_active, 0)\n})\n\ntest(\"handles null and empty JSON values\", () => {\n\tinterface NullableData {\n\t\trequired: string\n\t\toptional?: string\n\t\tnullValue: null\n\t\temptyObject: Record<string, never>\n\t\temptyArray: never[]\n\t}\n\n\tconst testData: NullableData = {\n\t\trequired: \"present\",\n\t\tnullValue: null,\n\t\temptyObject: {},\n\t\temptyArray: [],\n\t}\n\n\tconst insertData = db.sql<{ data: NullableData }>`\n INSERT INTO json_test (nullable_json)\n VALUES (${\"$data->json\"})\n `\n\n\tinsertData.run({ data: testData })\n\n\tconst getData = db.sql<Record<string, never>>`\n SELECT\n json_extract(nullable_json, '$') as data,\n json_extract(nullable_json, '$.optional') as missing_value,\n json_extract(nullable_json, '$.nullValue') as null_value,\n json_extract(nullable_json, '$.emptyObject') as empty_object,\n json_extract(nullable_json, '$.emptyArray') as empty_array\n FROM json_test\n `\n\n\tconst result = getData.all<{\n\t\tdata: NullableData\n\t\tmissing_value: unknown\n\t\tnull_value: null\n\t\tempty_object: Record<string, never>\n\t\tempty_array: never[]\n\t}>()\n\n\tassert.deepEqual(result[0].data, testData)\n\tassert.strictEqual(result[0].missing_value, null)\n\tassert.strictEqual(result[0].null_value, null)\n\tassert.deepEqual(result[0].empty_object, {})\n\tassert.deepEqual(result[0].empty_array, [])\n})\n\ntest(\"handles JSON updates\", () => {\n\tinterface UpdateData {\n\t\tcounter: number\n\t\titems: string[]\n\t\tmetadata: {\n\t\t\tlastModified: string\n\t\t\tmodifiedBy: string\n\t\t}\n\t}\n\n\tconst initialData: UpdateData = {\n\t\tcounter: 1,\n\t\titems: [\"item1\"],\n\t\tmetadata: {\n\t\t\tlastModified: \"2025-01-20\",\n\t\t\tmodifiedBy: \"user1\",\n\t\t},\n\t}\n\n\t// Insert initial data\n\tconst insertData = db.sql<{ data: UpdateData }>`\n INSERT INTO json_test (mixed_data)\n VALUES (${\"$data->json\"})\n `\n\n\tinsertData.run({ data: initialData })\n\n\t// Update the JSON data\n\tconst updatedData: UpdateData = {\n\t\tcounter: 2,\n\t\titems: [\"item1\", \"item2\"],\n\t\tmetadata: {\n\t\t\tlastModified: \"2025-01-21\",\n\t\t\tmodifiedBy: \"user2\",\n\t\t},\n\t}\n\n\tconst updateData = db.sql<{ data: UpdateData }>`\n UPDATE json_test\n SET mixed_data = ${\"$data->json\"}\n `\n\n\tupdateData.run({ data: updatedData })\n\n\t// Verify the update\n\tconst getData = db.sql<Record<string, never>>`\n SELECT json_extract(mixed_data, '$') as data\n FROM json_test\n `\n\n\tconst result = getData.all<{ data: UpdateData }>()\n\tassert.deepEqual(result[0].data, updatedData)\n})\n\ntest(\"handles nested JSON array operations\", () => {\n\tinterface NestedArrayData {\n\t\tmatrix: number[][]\n\t\tobjects: Array<{\n\t\t\tid: number\n\t\t\tnested: {\n\t\t\t\tvalues: string[]\n\t\t\t}\n\t\t}>\n\t}\n\n\tconst testData: NestedArrayData = {\n\t\tmatrix: [\n\t\t\t[1, 2, 3],\n\t\t\t[4, 5, 6],\n\t\t\t[7, 8, 9],\n\t\t],\n\t\tobjects: [\n\t\t\t{ id: 1, nested: { values: [\"a\", \"b\"] } },\n\t\t\t{ id: 2, nested: { values: [\"c\", \"d\"] } },\n\t\t],\n\t}\n\n\tconst insertData = db.sql<{ data: NestedArrayData }>`\n INSERT INTO json_test (array_data)\n VALUES (${\"$data->json\"})`\n\n\tinsertData.run({\n\t\tdata: testData,\n\t})\n\n\tconst getData = db.sql<Record<string, never>>`\n SELECT\n json_extract(array_data, '$') as data,\n json_extract(array_data, '$.matrix[1][1]') as center_value,\n json_extract(array_data, '$.objects[1].nested.values[0]') as nested_value\n FROM json_test`\n\n\tconst result = getData.all<{\n\t\tdata: NestedArrayData\n\t\tcenter_value: number\n\t\tnested_value: string\n\t}>()\n\n\tassert.deepEqual(result[0].data, testData)\n\tassert.strictEqual(result[0].center_value, 5)\n\tassert.strictEqual(result[0].nested_value, \"c\")\n})\n\ntest(\"handles fromJson when querying stored JSON data\", () => {\n\tinterface UserData {\n\t\tname: string\n\t\tsettings: {\n\t\t\ttheme: string\n\t\t\tnotifications: boolean\n\t\t}\n\t}\n\n\tconst userData: UserData = {\n\t\tname: \"John\",\n\t\tsettings: {\n\t\t\ttheme: \"dark\",\n\t\t\tnotifications: true,\n\t\t},\n\t}\n\n\t// First store the JSON data using toJson\n\tconst insertUser = db.sql<{ user_data: UserData }>`\n INSERT INTO json_test (simple_object)\n VALUES (${\"$user_data->json\"})`\n\n\tinsertUser.run({\n\t\tuser_data: userData,\n\t})\n\n\t// Now query it back using fromJson\n\tconst getUser = db.sql<Record<string, never>>`\n SELECT ${\"$simple_object<-json\"} as user_data\n FROM json_test`\n\n\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\tconst result = getUser.all<any>()\n\tassert.deepEqual(result[0].user_data, userData)\n})\n\ntest(\"handles fromJson with nested JSON fields\", () => {\n\tinterface ComplexData {\n\t\tid: number\n\t\tmetadata: {\n\t\t\ttags: string[]\n\t\t\ttimestamp: string\n\t\t\tnested: {\n\t\t\t\tcount: number\n\t\t\t\tflags: {\n\t\t\t\t\tactive: boolean\n\t\t\t\t\tfeatured: boolean\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tconst testData: ComplexData = {\n\t\tid: 1,\n\t\tmetadata: {\n\t\t\ttags: [\"test\", \"json\"],\n\t\t\ttimestamp: \"2025-01-20\",\n\t\t\tnested: {\n\t\t\t\tcount: 42,\n\t\t\t\tflags: {\n\t\t\t\t\tactive: true,\n\t\t\t\t\tfeatured: false,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// Store the data\n\tconst insertData = db.sql<{ data: ComplexData }>`\n INSERT INTO json_test (nested_object)\n VALUES (${\"$data->json\"})`\n\n\tinsertData.run({\n\t\tdata: testData,\n\t})\n\n\t// Query specific nested fields using fromJson\n\tconst getData = db.sql<Record<string, never>>`\n SELECT\n ${\"$nested_object<-json\"} as full_data,\n json(json_extract(nested_object, '$.metadata.tags')) as tags,\n json(json_extract(nested_object, '$.metadata.nested.flags')) as flags\n FROM json_test\n`\n\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\tconst result = getData.all<any>()\n\tassert.deepEqual(result[0].full_data, testData)\n\tassert.deepEqual(result[0].tags, [\"test\", \"json\"])\n\tassert.deepEqual(result[0].flags, {\n\t\tactive: true,\n\t\tfeatured: false,\n\t})\n})\n\ntest(\"handles mixed query with multiple fromJson operations\", () => {\n\tinterface UserProfile {\n\t\tname: string\n\t\tpreferences: { theme: string }\n\t}\n\n\tinterface UserMetadata {\n\t\tlastLogin: string\n\t\tdevices: string[]\n\t}\n\n\tconst profile: UserProfile = {\n\t\tname: \"Jane\",\n\t\tpreferences: { theme: \"light\" },\n\t}\n\n\tconst metadata: UserMetadata = {\n\t\tlastLogin: \"2025-01-20\",\n\t\tdevices: [\"desktop\", \"mobile\"],\n\t}\n\n\t// Store both JSON objects\n\tconst insertData = db.sql<{ profile: UserProfile; meta: UserMetadata }>`\n INSERT INTO json_test (simple_object, nested_object)\n VALUES (${\"$profile->json\"}, ${\"$meta->json\"})`\n\n\tinsertData.run({\n\t\tprofile,\n\t\tmeta: metadata,\n\t})\n\n\t// Query both JSON fields using fromJson\n\tconst getData = db.sql<Record<string, never>>`\n SELECT\n ${\"$simple_object<-json\"} as profile,\n ${\"$nested_object<-json\"} as metadata\n FROM json_test`\n\n\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\tconst result = getData.all<any>()\n\tassert.deepEqual(result[0].profile, profile)\n\tassert.deepEqual(result[0].metadata, metadata)\n})\n",
"metadata": {
"size": 12783,
"modified": 1738725761193.7544,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"database.test.ts": {
"content": "import { test, beforeEach, afterEach } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { DB } from \"./database\"\nimport { NodeSqliteError, SqlitePrimaryResultCode } from \"./errors\"\n\nlet db: DB\n\nbeforeEach(() => {\n\tdb = new DB({\n\t\tlocation: \":memory:\",\n\t\tenvironment: \"testing\",\n\t\t// logger: new ConsoleLogger(LogLevel.DEBUG), // Changed to DEBUG level\n\t})\n\n\tdb.exec(\"DROP TABLE IF EXISTS posts;\")\n\tdb.exec(\"DROP TABLE IF EXISTS users;\")\n\n\tdb.exec(`\n CREATE TABLE users (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n age INTEGER NOT NULL,\n email TEXT UNIQUE\n );\n `)\n\n\tdb.exec(`\n CREATE TABLE posts (\n id INTEGER PRIMARY KEY,\n title TEXT NOT NULL,\n user_id INTEGER,\n FOREIGN KEY(user_id) REFERENCES users(id)\n );\n `)\n})\n\nafterEach(() => {\n\tdb.close()\n})\n\ntest(\"executes basic SELECT query\", () => {\n\tconst insertUser = db.sql<{ name: string; age: number; email: string }>`\n INSERT INTO users (name, age, email)\n VALUES (${\"$name\"}, ${\"$age\"}, ${\"$email\"})\n `\n\n\tinsertUser.run({\n\t\tname: \"John\",\n\t\tage: 30,\n\t\temail: \"john$example.com\",\n\t})\n\tinsertUser.run({\n\t\tname: \"Jane\",\n\t\tage: 25,\n\t\temail: \"jane$example.com\",\n\t})\n\n\tconst users = db.sql<\n\t\t{ minAge: number },\n\t\t{\n\t\t\tname: string\n\t\t\tage: number\n\t\t\temail: string\n\t\t}\n\t>`SELECT name, age, email\n FROM users\n WHERE age >= ${\"$minAge\"}\n `\n\n\tconst results = users.all({\n\t\tminAge: 28,\n\t})\n\n\tassert.equal(results.length, 1)\n\tassert.equal(results[0].name, \"John\")\n\tassert.equal(results[0].age, 30)\n})\n\ntest(\"handles syntax errors\", () => {\n\tconst query = db.sql<Record<string, never>>`SELEC * FORM users`\n\n\tassert.throws(\n\t\t() => query.all(), // Execute the query to trigger the error\n\t\terror =>\n\t\t\terror instanceof NodeSqliteError &&\n\t\t\tNodeSqliteError.fromNodeSqlite(error).getPrimaryResultCode() ===\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR\n\t)\n})\n\ntest(\"handles complex WHERE conditions\", () => {\n\tconst insertUser = db.sql<{ name: string; age: number; email: string }>`\n INSERT INTO users (name, age, email)\n VALUES (${\"$name\"}, ${\"$age\"}, ${\"$email\"})\n `\n\n\tinsertUser.run({\n\t\tname: \"John\",\n\t\tage: 30,\n\t\temail: \"john$example.com\",\n\t})\n\tinsertUser.run({\n\t\tname: \"Jane\",\n\t\tage: 25,\n\t\temail: \"jane$example.com\",\n\t})\n\tinsertUser.run({\n\t\tname: \"Bob\",\n\t\tage: 35,\n\t\temail: \"bob$example.com\",\n\t})\n\n\tconst getUsersQuery = db.sql<{ minAge: number; nameLike: string }>`\n SELECT * FROM users\n WHERE age >= ${\"$minAge\"}\n AND name LIKE ${\"$nameLike\"}\n `\n\n\tconst results = getUsersQuery.all<{ name: string; age: number }>({\n\t\tminAge: 25,\n\t\tnameLike: \"J%\",\n\t})\n\n\tassert.equal(results.length, 2)\n\tassert.ok(results.every(user => user.name.startsWith(\"J\")))\n})\n\ntest(\"performs INSERT operation\", () => {\n\tconst insertUser = db.sql<{ name: string; age: number; email: string }>`\n INSERT INTO users (name, age, email)\n VALUES (${\"$name\"}, ${\"$age\"}, ${\"$email\"})\n `\n\n\tconst result = insertUser.run({\n\t\tname: \"John\",\n\t\tage: 30,\n\t\temail: \"john$example.com\",\n\t})\n\n\tassert.equal(result.changes, 1)\n\tassert.ok(result.lastInsertRowid > 0)\n})\n\ntest(\"performs UPDATE operation\", () => {\n\tconst insertUser = db.sql<{ name: string; age: number; email: string }>`\n INSERT INTO users (name, age, email)\n VALUES (${\"$name\"}, ${\"$age\"}, ${\"$email\"})\n `\n\n\tconst inserted = insertUser.run({\n\t\tname: \"John\",\n\t\tage: 30,\n\t\temail: \"john$example.com\",\n\t})\n\n\tconst updateUser = db.sql<{ id: number | bigint; newAge: number }>`\n UPDATE users\n SET age = ${\"$newAge\"}\n WHERE id = ${\"$id\"}\n `\n\n\tconst result = updateUser.run({\n\t\tid: inserted.lastInsertRowid,\n\t\tnewAge: 31,\n\t})\n\n\tassert.equal(result.changes, 1)\n})\n\ntest(\"performs DELETE operation\", () => {\n\tconst insertUser = db.sql<{ name: string; age: number; email: string }>`\n INSERT INTO users (name, age, email)\n VALUES (${\"$name\"}, ${\"$age\"}, ${\"$email\"})\n `\n\n\tconst inserted = insertUser.run({\n\t\tname: \"John\",\n\t\tage: 30,\n\t\temail: \"john$example.com\",\n\t})\n\n\tconst deleteUser = db.sql<{ id: number | bigint }>`\n DELETE FROM users\n WHERE id = ${\"$id\"}\n `\n\n\tconst result = deleteUser.run({ id: inserted.lastInsertRowid })\n\tassert.equal(result.changes, 1)\n})\n\ntest(\"handles unique constraint violations\", () => {\n\tconst insertUser = db.sql<{ name: string; age: number; email: string }>`\n INSERT INTO users (name, age, email)\n VALUES (${\"$name\"}, ${\"$age\"}, ${\"$email\"})\n `\n\n\tinsertUser.run({\n\t\tname: \"John\",\n\t\tage: 30,\n\t\temail: \"john$example.com\",\n\t})\n\n\tassert.throws(\n\t\t() =>\n\t\t\tinsertUser.run({\n\t\t\t\tname: \"Jane\",\n\t\t\t\tage: 25,\n\t\t\t\temail: \"john$example.com\",\n\t\t\t}),\n\t\terror =>\n\t\t\terror instanceof NodeSqliteError &&\n\t\t\terror.message.includes(\"UNIQUE constraint\")\n\t)\n})\n\ntest(\"handles foreign key constraints\", () => {\n\tconst insertPost = db.sql<{ title: string; userId: number }>`\n INSERT INTO posts (title, user_id)\n VALUES (${\"$title\"}, ${\"$userId\"})\n `\n\n\tassert.throws(\n\t\t() =>\n\t\t\tinsertPost.run({\n\t\t\t\ttitle: \"Test Post\",\n\t\t\t\tuserId: 999,\n\t\t\t}),\n\t\terror =>\n\t\t\terror instanceof NodeSqliteError &&\n\t\t\terror.message.includes(\"FOREIGN KEY constraint\")\n\t)\n})\n\ntest(\"enforces NOT NULL constraints\", () => {\n\tconst insertUser = db.sql<{\n\t\tname: string | null\n\t\tage: number\n\t}>`\n INSERT INTO users (name, age)\n VALUES (${\"$name\"}, ${\"$age\"})\n `\n\n\tassert.throws(\n\t\t() =>\n\t\t\tinsertUser.run({\n\t\t\t\tname: null,\n\t\t\t\tage: 30,\n\t\t\t}),\n\t\t{\n\t\t\tname: \"NodeSqliteError\",\n\t\t}\n\t)\n})\n\ntest(\"caches prepared statements\", () => {\n\tconst dbWithCache = new DB({\n\t\tlocation: \":memory:\",\n\t\tstatementCache: { maxSize: 10 },\n\t})\n\n\tconst query = dbWithCache.sql<{ minAge: number }>`\n SELECT * FROM users\n WHERE age > ${\"$minAge\"}\n `\n\n\t// Need to create the table first\n\tdbWithCache.exec(`\n CREATE TABLE users (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n age INTEGER NOT NULL,\n email TEXT UNIQUE\n );\n `)\n\n\tquery.all<unknown[]>({ minAge: 20 })\n\tquery.all<unknown[]>({ minAge: 25 })\n\tquery.all<unknown[]>({ minAge: 30 })\n\n\tconst stats = dbWithCache.getCacheStats()\n\tassert.ok(stats)\n\tassert.ok(stats.hits > 0)\n\n\tdbWithCache.close()\n})\n\ntest(\"clears statement cache\", () => {\n\tconst dbWithCache = new DB({\n\t\tlocation: \":memory:\",\n\t\tstatementCache: { maxSize: 10 },\n\t})\n\n\tdbWithCache.clearStatementCache()\n\tconst stats = dbWithCache.getCacheStats()\n\tassert.ok(stats)\n\tassert.equal(stats.size, 0)\n\n\tdbWithCache.close()\n})\n",
"metadata": {
"size": 6702,
"modified": 1738725761193.8801,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"database.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { DatabaseSync, type StatementSync } from \"node:sqlite\"\nimport {\n\tNodeSqliteError,\n\tSqlitePrimaryResultCode,\n\tisNodeSqliteError,\n} from \"#errors\"\nimport {\n\tcreateStatementCache,\n\ttype StatementCache,\n\ttype CacheStats,\n} from \"#cache\"\nimport { join } from \"node:path\"\nimport {\n\ttype PragmaConfig,\n\tPragmaDefaults,\n\tgetPragmaStatements,\n} from \"#pragmas\"\nimport { tmpdir } from \"node:os\"\nimport { accessSync, renameSync, unlinkSync } from \"node:fs\"\nimport { type Logger, NoopLogger } from \"#logger\"\nimport { createXStatementSync, Sql } from \"#sql\"\nimport type {\n\tCleanupPragmas,\n\tDataRow,\n\tDBOptions,\n\tFormatterConfig,\n\tSqlTemplateValues,\n} from \"#types\"\nimport { buildIndexStatement, type IndexDef } from \"#idx.js\"\nimport stringify from \"#stringify.js\"\n\n/**\n * Type-safe SQLite database wrapper with prepared statement caching, SQL template literals,\n * and JSON support.\n */\nexport class DB {\n\t#db: DatabaseSync\n\treadonly #statementCache?: StatementCache\n\treadonly #location: string\n\treadonly #logger: Logger\n\treadonly #formatConfig?: FormatterConfig | false\n\t/**\n\t * Creates a new database connection with optional configuration.\n\t * @param options Database configuration options\n\t * @throws {NodeSqliteError} If database cannot be opened or initialized\n\t */\n\n\tconstructor(options: DBOptions = {}) {\n\t\tconst location = options.location ?? \":memory:\"\n\t\tthis.#location = location\n\t\tthis.#logger = options.logger ?? new NoopLogger()\n\t\tthis.#formatConfig = options.format\n\n\t\tthis.#logger.debug(\"Initializing database\", { location })\n\n\t\ttry {\n\t\t\tthis.#db = new DatabaseSync(location, { open: true })\n\t\t\tthis.#logger.info(\"Database opened successfully\", { location })\n\n\t\t\t// Configure pragmas based on environment and custom settings\n\t\t\tconst environment = options.environment || \"development\"\n\t\t\tthis.#logger.debug(\"Configuring pragmas\", { environment })\n\n\t\t\tconst defaultPragmas = PragmaDefaults[environment]\n\t\t\tconst customPragmas = options.pragma || {}\n\t\t\tconst finalPragmas: PragmaConfig = {\n\t\t\t\t...defaultPragmas,\n\t\t\t\t...customPragmas,\n\t\t\t}\n\t\t\tthis.#configurePragmas(finalPragmas)\n\n\t\t\t// Initialize statement cache if enabled\n\t\t\tif (options.statementCache) {\n\t\t\t\tthis.#logger.debug(\"Initializing statement cache\")\n\t\t\t\tif (typeof options.statementCache === \"object\") {\n\t\t\t\t\tthis.#statementCache = createStatementCache(options.statementCache)\n\t\t\t\t\tthis.#logger.debug(\n\t\t\t\t\t\t\"Created statement cache with custom options\",\n\t\t\t\t\t\toptions.statementCache\n\t\t\t\t\t)\n\t\t\t\t} else {\n\t\t\t\t\tthis.#statementCache = createStatementCache({ maxSize: 1000 })\n\t\t\t\t\tthis.#logger.debug(\"Created statement cache with default options\")\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#logger.error(\"Failed to initialize database\", error)\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_OPEN\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_CANTOPEN,\n\t\t\t\t\"Cannot open database\",\n\t\t\t\t`Failed to open database at ${location}`,\n\t\t\t\terror instanceof Error ? error : undefined\n\t\t\t)\n\t\t}\n\t}\n\t/**\n\t * Prepares an SQL statement with optional caching.\n\t * @param sql The SQL statement to prepare\n\t * @returns Prepared statement\n\t * @throws {NodeSqliteError} If statement preparation fails\n\t */\n\n\tprepareStatement(sql: string): StatementSync {\n\t\tthis.#logger.debug(\"Preparing statement\", { sql })\n\t\ttry {\n\t\t\tif (this.#statementCache) {\n\t\t\t\tconst cached = this.#statementCache.get(sql)\n\t\t\t\tif (cached) {\n\t\t\t\t\tthis.#logger.trace(\"Statement cache hit\", { sql })\n\t\t\t\t\treturn cached\n\t\t\t\t}\n\t\t\t\tthis.#logger.trace(\"Statement cache miss\", { sql })\n\t\t\t}\n\n\t\t\tconst stmt = this.#db.prepare(sql)\n\t\t\tthis.#logger.trace(\"Statement prepared successfully\", { sql })\n\n\t\t\tif (this.#statementCache) {\n\t\t\t\tthis.#statementCache.set(sql, stmt)\n\t\t\t\tthis.#logger.trace(\"Statement cached\", { sql })\n\t\t\t}\n\n\t\t\treturn stmt\n\t\t} catch (error) {\n\t\t\tif (\n\t\t\t\tthis.#statementCache &&\n\t\t\t\terror instanceof Error &&\n\t\t\t\terror.message.toLowerCase().includes(\"memory\")\n\t\t\t) {\n\t\t\t\tthis.#logger.warn(\"Memory pressure detected, clearing statement cache\")\n\t\t\t\tthis.#statementCache.clear()\n\t\t\t}\n\n\t\t\tthis.#logger.error(\"Failed to prepare statement\", { sql, error })\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_PREPARE\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\"Failed to prepare statement\",\n\t\t\t\terror instanceof Error ? error.message : String(error),\n\t\t\t\terror instanceof Error ? error : undefined\n\t\t\t)\n\t\t}\n\t}\n\t/**\n\t * Creates a type-safe SQL query builder using template literals.\n\t * @param strings SQL template strings\n\t * @param params SQL template parameters and contexts\n\t * @returns Type-safe statement executor\n\t */\n\tsql<P extends DataRow, R = P>(\n\t\tstrings: TemplateStringsArray,\n\t\t...params: SqlTemplateValues<P, R>\n\t) {\n\t\tconst builder = new Sql<P, R>({\n\t\t\tstrings,\n\t\t\tparamOperators: params,\n\t\t\tformatterConfig: this.#formatConfig,\n\t\t})\n\t\treturn createXStatementSync<P, R>({\n\t\t\tbuild: finalParams => {\n\t\t\t\tconst { sql, namedParams, hasJsonColumns } =\n\t\t\t\t\tbuilder.prepare(finalParams)\n\n\t\t\t\tconst stmt = this.prepareStatement(sql)\n\n\t\t\t\treturn { stmt, namedParams, hasJsonColumns }\n\t\t\t},\n\t\t\tprepare: sql => this.prepareStatement(sql),\n\t\t\tsql: builder,\n\t\t})\n\t}\n\n\tcreateIndex<T extends DataRow>(def: IndexDef<T>): void {\n\t\tthis.#logger.debug(\"Creating index\", stringify(def))\n\t\tconst stmt = buildIndexStatement(def)\n\t\tthis.exec(stmt)\n\t\tthis.#logger.info(\"Index created successfully\", stringify(def))\n\t}\n\n\t/**\n\t * Creates a backup of the database.\n\t * @param filename Path where backup will be saved\n\t * @throws {NodeSqliteError} If backup creation fails\n\t */\n\n\tbackup(filename: string): void {\n\t\tthis.#logger.info(\"Starting database backup\", { filename })\n\t\ttry {\n\t\t\tthis.#db.exec(`VACUUM INTO '${filename}'`)\n\t\t\tthis.#logger.info(\"Database backup completed successfully\", {\n\t\t\t\tfilename,\n\t\t\t})\n\t\t} catch (error) {\n\t\t\tthis.#logger.error(\"Backup failed\", { filename, error })\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_BACKUP\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_CANTOPEN,\n\t\t\t\t\"Cannot create backup file\",\n\t\t\t\t`Failed to create backup at ${filename}. Check permissions and ensure directory exists.`,\n\t\t\t\terror instanceof Error ? error : undefined\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Restores database from a backup file.\n\t * @param filename Path to backup file\n\t * @throws {NodeSqliteError} If restore fails or file is inaccessible\n\t */\n\n\trestore(filename: string): void {\n\t\tthis.#logger.info(\"Starting database restore\", { filename })\n\t\ttry {\n\t\t\tif (this.#location === \":memory:\") {\n\t\t\t\tthis.#logger.error(\"Cannot restore in-memory database\")\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_RESTORE\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_MISUSE,\n\t\t\t\t\t\"Cannot restore in-memory database\",\n\t\t\t\t\t\"Restore operation is not supported for in-memory databases\",\n\t\t\t\t\tundefined\n\t\t\t\t)\n\t\t\t}\n\n\t\t\ttry {\n\t\t\t\taccessSync(filename)\n\t\t\t} catch (error) {\n\t\t\t\tthis.#logger.error(\"Backup file inaccessible\", { filename, error })\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_CANTOPEN\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_CANTOPEN,\n\t\t\t\t\t\"Cannot open backup file\",\n\t\t\t\t\t`Failed to restore from ${filename}. File may not exist or be inaccessible.`,\n\t\t\t\t\terror instanceof Error ? error : undefined\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tthis.#logger.debug(\"Creating temporary backup\")\n\t\t\tthis.close()\n\n\t\t\tconst tempBackup = join(tmpdir(), `temp-${Date.now()}.db`)\n\t\t\ttry {\n\t\t\t\trenameSync(this.#location, tempBackup)\n\t\t\t\tthis.#logger.debug(\"Created temporary backup\", { tempBackup })\n\n\t\t\t\trenameSync(filename, this.#location)\n\t\t\t\tthis.#logger.debug(\"Moved new database into place\")\n\n\t\t\t\tthis.#db = new DatabaseSync(this.#location, { open: true })\n\t\t\t\tthis.#logger.debug(\"Opened restored database\")\n\n\t\t\t\tunlinkSync(tempBackup)\n\t\t\t\tthis.#logger.debug(\"Removed temporary backup\")\n\n\t\t\t\tthis.#logger.info(\"Database restore completed successfully\")\n\t\t\t} catch (error) {\n\t\t\t\tthis.#logger.error(\"Restore failed, attempting rollback\", { error })\n\t\t\t\tif (error instanceof Error && error.message.includes(\"ENOENT\")) {\n\t\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\t\"ERR_SQLITE_CANTOPEN\",\n\t\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_CANTOPEN,\n\t\t\t\t\t\t\"Cannot open backup file\",\n\t\t\t\t\t\t`Failed to restore from ${filename}`,\n\t\t\t\t\t\terror\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow error\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthrow error instanceof NodeSqliteError\n\t\t\t\t? error\n\t\t\t\t: NodeSqliteError.fromNodeSqlite(\n\t\t\t\t\t\terror instanceof Error ? error : new Error(String(error))\n\t\t\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Executes raw SQL directly.\n\t * @param sql SQL statement to execute\n\t * @throws {NodeSqliteError} If execution fails\n\t */\n\n\texec(sql: string): void {\n\t\ttry {\n\t\t\tthis.#logger.debug(\"Executing raw SQL\", { sql })\n\t\t\tthis.#db.exec(sql)\n\t\t\tthis.#logger.trace(\"Raw SQL executed successfully\", { sql })\n\t\t} catch (error) {\n\t\t\tthis.#logger.error(\"Raw SQL execution failed\", { sql, error })\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_EXEC\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\"Execution failed\",\n\t\t\t\terror instanceof Error ? error.message : String(error),\n\t\t\t\terror instanceof Error ? error : undefined\n\t\t\t)\n\t\t}\n\t}\n\n\t/**\n\t * Retrieves prepared statement cache statistics.\n\t * @returns Cache statistics if caching is enabled, undefined otherwise\n\t */\n\n\tgetCacheStats(): CacheStats | undefined {\n\t\tthis.#logger.debug(\"Retrieving cache statistics\")\n\t\treturn this.#statementCache?.getStats()\n\t}\n\n\t/**\n\t * Clears the prepared statement cache if enabled.\n\t */\n\n\tclearStatementCache(): void {\n\t\tif (this.#statementCache) {\n\t\t\tthis.#logger.debug(\"Clearing statement cache\")\n\t\t\tthis.#statementCache.clear()\n\t\t\tthis.#logger.debug(\"Statement cache cleared\")\n\t\t}\n\t}\n\n\t/**\n\t * Closes database connection and optionally runs cleanup pragmas.\n\t * @param pragmas Optional cleanup operations to perform before closing\n\t */\n\tclose(pragmas?: CleanupPragmas): void {\n\t\tthis.#logger.info(\"Closing database connection\", pragmas)\n\n\t\ttry {\n\t\t\tif (pragmas) {\n\t\t\t\tif (pragmas.optimize) {\n\t\t\t\t\tthis.#db.exec(\"PRAGMA optimize;\")\n\t\t\t\t}\n\t\t\t\tif (pragmas.shrinkMemory) {\n\t\t\t\t\tthis.#db.exec(\"PRAGMA shrink_memory;\")\n\t\t\t\t}\n\t\t\t\tif (pragmas.walCheckpoint) {\n\t\t\t\t\tthis.#db.exec(`PRAGMA wal_checkpoint(${pragmas.walCheckpoint});`)\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#logger.error(\"Error executing cleanup pragmas\", error)\n\t\t} finally {\n\t\t\tthis.clearStatementCache()\n\t\t\tthis.#db.close()\n\t\t\tthis.#logger.info(\"Database connection closed\")\n\t\t}\n\t}\n\n\t/**\n\t * Configures database pragmas.\n\t * @param config PRAGMA configuration settings\n\t * @throws {NodeSqliteError} If pragma configuration fails\n\t * @private\n\t */\n\n\t#configurePragmas(config: PragmaConfig): void {\n\t\ttry {\n\t\t\tthis.#logger.debug(\"Configuring pragmas\", config)\n\t\t\tconst statements = getPragmaStatements(config)\n\n\t\t\tfor (const stmt of statements) {\n\t\t\t\tthis.#logger.trace(\"Executing pragma statement\", { stmt })\n\t\t\t\tthis.#db.exec(stmt)\n\t\t\t}\n\n\t\t\tthis.#logger.debug(\"Pragma configuration completed\")\n\t\t} catch (error) {\n\t\t\tthis.#logger.error(\"Failed to configure pragmas\", { error })\n\t\t\tif (isNodeSqliteError(error)) {\n\t\t\t\tif (\n\t\t\t\t\tNodeSqliteError.fromNodeSqlite(error).getPrimaryResultCode() ===\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_BUSY\n\t\t\t\t) {\n\t\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\t\"ERR_SQLITE_BUSY\",\n\t\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_BUSY,\n\t\t\t\t\t\t\"Database is locked while configuring pragmas\",\n\t\t\t\t\t\t\"Failed to configure database pragmas: database is locked\",\n\t\t\t\t\t\terror\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tthrow NodeSqliteError.fromNodeSqlite(\n\t\t\t\terror instanceof Error ? error : new Error(String(error))\n\t\t\t)\n\t\t}\n\t}\n}\n",
"metadata": {
"size": 11670,
"modified": 1738736127027.1494,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"errors.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n/**\n * SQLite primary result codes (least significant 8 bits)\n * @see https://www.sqlite.org/rescode.html\n */\nexport enum SqlitePrimaryResultCode {\n\tSQLITE_OK = 0,\n\tSQLITE_ERROR = 1,\n\tSQLITE_INTERNAL = 2,\n\tSQLITE_PERM = 3,\n\tSQLITE_ABORT = 4,\n\tSQLITE_BUSY = 5,\n\tSQLITE_LOCKED = 6,\n\tSQLITE_NOMEM = 7,\n\tSQLITE_READONLY = 8,\n\tSQLITE_INTERRUPT = 9,\n\tSQLITE_IOERR = 10,\n\tSQLITE_CORRUPT = 11,\n\tSQLITE_NOTFOUND = 12,\n\tSQLITE_FULL = 13,\n\tSQLITE_CANTOPEN = 14,\n\tSQLITE_PROTOCOL = 15,\n\tSQLITE_SCHEMA = 17,\n\tSQLITE_CONSTRAINT = 19,\n\tSQLITE_MISMATCH = 20,\n\tSQLITE_MISUSE = 21,\n}\n\n/**\n * Maps error codes to their type strings for better error reporting\n */\nexport const SqliteErrorTypes = {\n\t[SqlitePrimaryResultCode.SQLITE_OK]: \"OK\",\n\t[SqlitePrimaryResultCode.SQLITE_ERROR]: \"ERROR\",\n\t[SqlitePrimaryResultCode.SQLITE_INTERNAL]: \"INTERNAL_ERROR\",\n\t[SqlitePrimaryResultCode.SQLITE_PERM]: \"PERMISSION_DENIED\",\n\t[SqlitePrimaryResultCode.SQLITE_ABORT]: \"OPERATION_ABORTED\",\n\t[SqlitePrimaryResultCode.SQLITE_BUSY]: \"DATABASE_BUSY\",\n\t[SqlitePrimaryResultCode.SQLITE_LOCKED]: \"DATABASE_LOCKED\",\n\t[SqlitePrimaryResultCode.SQLITE_NOMEM]: \"OUT_OF_MEMORY\",\n\t[SqlitePrimaryResultCode.SQLITE_READONLY]: \"DATABASE_READONLY\",\n\t[SqlitePrimaryResultCode.SQLITE_INTERRUPT]: \"OPERATION_INTERRUPTED\",\n\t[SqlitePrimaryResultCode.SQLITE_IOERR]: \"IO_ERROR\",\n\t[SqlitePrimaryResultCode.SQLITE_CORRUPT]: \"DATABASE_CORRUPT\",\n\t[SqlitePrimaryResultCode.SQLITE_NOTFOUND]: \"NOT_FOUND\",\n\t[SqlitePrimaryResultCode.SQLITE_FULL]: \"DATABASE_FULL\",\n\t[SqlitePrimaryResultCode.SQLITE_CANTOPEN]: \"CANNOT_OPEN\",\n\t[SqlitePrimaryResultCode.SQLITE_PROTOCOL]: \"PROTOCOL_ERROR\",\n\t[SqlitePrimaryResultCode.SQLITE_SCHEMA]: \"SCHEMA_CHANGED\",\n\t[SqlitePrimaryResultCode.SQLITE_CONSTRAINT]: \"CONSTRAINT_VIOLATION\",\n\t[SqlitePrimaryResultCode.SQLITE_MISMATCH]: \"TYPE_MISMATCH\",\n\t[SqlitePrimaryResultCode.SQLITE_MISUSE]: \"LIBRARY_MISUSE\",\n} as const\n\nexport type SqliteErrorType =\n\t(typeof SqliteErrorTypes)[keyof typeof SqliteErrorTypes]\n\n/**\n * Error interface for node:sqlite errors\n */\nexport interface NodeSqliteErrorData {\n\tcode: string\n\terrcode: number\n\terrstr: string\n\tmessage: string\n\terrorType: SqliteErrorType\n\toriginalError?: Error\n}\n\n/**\n * Custom error class for node:sqlite errors with enhanced type information\n */\nexport class NodeSqliteError extends Error implements NodeSqliteErrorData {\n\tpublic readonly errorType: SqliteErrorType\n\n\tconstructor(\n\t\tpublic readonly code: string,\n\t\tpublic readonly errcode: number,\n\t\tpublic readonly errstr: string,\n\t\tmessage: string,\n\t\tpublic readonly originalError?: Error\n\t) {\n\t\tsuper(message)\n\t\tthis.name = \"NodeSqliteError\"\n\t\tthis.errorType =\n\t\t\tSqliteErrorTypes[errcode as keyof typeof SqliteErrorTypes] || \"ERROR\"\n\t\tObject.setPrototypeOf(this, NodeSqliteError.prototype)\n\t}\n\n\t/**\n\t * Gets the primary result code (least significant 8 bits)\n\t */\n\tgetPrimaryResultCode(): SqlitePrimaryResultCode {\n\t\treturn this.errcode & 0xff\n\t}\n\n\toverride toString(): string {\n\t\treturn `NodeSqliteError: [${this.errorType}] ${this.message} (code: ${this.code}, errcode: ${this.errcode})`\n\t}\n\n\tstatic fromNodeSqlite(\n\t\terror: Error & {\n\t\t\t\t\tcode?: string\n\t\t\terrcode?: number\n\t\t\terrstr?: string\n\t\t}\n\t): NodeSqliteError {\n\t\treturn new NodeSqliteError(\n\t\t\terror.code || \"ERR_SQLITE_ERROR\",\n\t\t\terror.errcode || SqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\terror.errstr || \"unknown error\",\n\t\t\terror.message,\n\t\t\terror\n\t\t)\n\t}\n}\n\nexport function isNodeSqliteError(error: unknown): error is NodeSqliteError {\n\treturn (\n\t\terror instanceof NodeSqliteError ||\n\t\t(error instanceof Error &&\n\t\t\t\"code\" in error &&\n\t\t\t\"errcode\" in error &&\n\t\t\t\"errstr\" in error &&\n\t\t\terror.code === \"ERR_SQLITE_ERROR\")\n\t)\n}\n",
"metadata": {
"size": 3816,
"modified": 1738739967545.2815,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"fk.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { test, describe } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport {\n\tvalidateForeignKeys,\n\tisForeignKeys,\n\tbuildForeignKeyStatement,\n} from \"#fk\"\n\ndescribe(\"Foreign Key Validation\", () => {\n\ttest(\"validates basic foreign key\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t}\n\t\tassert.equal(validateForeignKeys(fk).length, 0)\n\t\tassert.equal(isForeignKeys(fk), true)\n\t})\n\n\ttest(\"validates composite key\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"firstName,lastName\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"first\", \"last\"],\n\t\t\t},\n\t\t}\n\t\tassert.equal(validateForeignKeys(fk).length, 0)\n\t})\n\n\ttest(\"rejects more than 3 keys\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"a,b,c,d\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t}\n\t\tconst errors = validateForeignKeys(fk)\n\t\tassert.equal(errors.length, 1)\n\t\tassert.equal(\n\t\t\terrors[0].message,\n\t\t\t\"Maximum of 3 keys allowed in foreign key constraint\"\n\t\t)\n\t})\n\n\ttest(\"validates actions\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t\tonDelete: \"CASCADE\",\n\t\t\tonUpdate: \"SET NULL\",\n\t\t}\n\t\tassert.equal(validateForeignKeys(fk).length, 0)\n\t})\n\n\ttest(\"rejects invalid actions\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t\tonDelete: \"INVALID\",\n\t\t\tonUpdate: \"WRONG\",\n\t\t}\n\t\tconst errors = validateForeignKeys(fk)\n\t\tassert.equal(errors.length, 2)\n\t\tassert.ok(errors[0].message.includes(\"Invalid ON DELETE\"))\n\t\tassert.ok(errors[1].message.includes(\"Invalid ON UPDATE\"))\n\t})\n\n\ttest(\"validates deferrable status\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t\tdeferrable: \"DEFERRABLE INITIALLY DEFERRED\",\n\t\t}\n\t\tassert.equal(validateForeignKeys(fk).length, 0)\n\t})\n\n\ttest(\"rejects invalid deferrable status\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t\tdeferrable: \"INVALID\",\n\t\t}\n\t\tconst errors = validateForeignKeys(fk)\n\t\tassert.equal(errors.length, 1)\n\t\tassert.ok(errors[0].message.includes(\"Invalid deferrable status\"))\n\t})\n\n\ttest(\"requires references\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t}\n\t\tconst errors = validateForeignKeys(fk)\n\t\tassert.equal(errors.length, 1)\n\t\tassert.equal(errors[0].message, \"References is required\")\n\t})\n\n\ttest(\"validates references structure\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t\treferences: {\n\t\t\t\ttable: 123,\n\t\t\t\tcolumns: \"not an array\",\n\t\t\t},\n\t\t}\n\t\tconst errors = validateForeignKeys(fk)\n\t\tassert.equal(errors.length, 2)\n\t\tassert.ok(errors[0].message.includes(\"table must be a string\"))\n\t\tassert.ok(errors[1].message.includes(\"columns must be an array\"))\n\t})\n})\n\ndescribe(\"Foreign Key Statement Building\", () => {\n\ttest(\"builds basic foreign key\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t}\n\t\tconst sql = buildForeignKeyStatement([fk])\n\t\tassert.equal(sql, \"FOREIGN KEY(userId) REFERENCES users(id)\")\n\t})\n\n\ttest(\"builds composite foreign key\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"firstName,lastName\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"first\", \"last\"],\n\t\t\t},\n\t\t}\n\t\tconst sql = buildForeignKeyStatement([fk])\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"FOREIGN KEY(firstName, lastName) REFERENCES users(first, last)\"\n\t\t)\n\t})\n\n\ttest(\"includes ON DELETE and ON UPDATE\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t\tonDelete: \"CASCADE\",\n\t\t\tonUpdate: \"SET NULL\",\n\t\t}\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\tconst sql = buildForeignKeyStatement([fk as any])\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"FOREIGN KEY(userId) REFERENCES users(id) ON DELETE CASCADE ON UPDATE SET NULL\"\n\t\t)\n\t})\n\n\ttest(\"includes deferrable status\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"userId\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t\tdeferrable: \"DEFERRABLE INITIALLY DEFERRED\",\n\t\t}\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\tconst sql = buildForeignKeyStatement([fk as any])\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"FOREIGN KEY(userId) REFERENCES users(id) DEFERRABLE INITIALLY DEFERRED\"\n\t\t)\n\t})\n\n\ttest(\"handles multiple foreign keys\", () => {\n\t\tconst fks = [\n\t\t\t{\n\t\t\t\tkey: \"userId\",\n\t\t\t\treferences: {\n\t\t\t\t\ttable: \"users\",\n\t\t\t\t\tcolumns: [\"id\"],\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tkey: \"groupId\",\n\t\t\t\treferences: {\n\t\t\t\t\ttable: \"groups\",\n\t\t\t\t\tcolumns: [\"id\"],\n\t\t\t\t},\n\t\t\t\tonDelete: \"CASCADE\",\n\t\t\t},\n\t\t]\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\tconst sql = buildForeignKeyStatement(fks as any)\n\t\tassert.equal(\n\t\t\tsql,\n\t\t\t\"FOREIGN KEY(userId) REFERENCES users(id),\\n FOREIGN KEY(groupId) REFERENCES groups(id) ON DELETE CASCADE\"\n\t\t)\n\t})\n\n\ttest(\"throws on column count mismatch\", () => {\n\t\tconst fk = {\n\t\t\tkey: \"firstName,lastName\",\n\t\t\treferences: {\n\t\t\t\ttable: \"users\",\n\t\t\t\tcolumns: [\"id\"],\n\t\t\t},\n\t\t}\n\t\tassert.throws(() => buildForeignKeyStatement([fk]), {\n\t\t\tname: \"NodeSqliteError\",\n\t\t\tmessage:\n\t\t\t\t\"Foreign key columns count (2) does not match referenced columns count (1)\",\n\t\t})\n\t})\n})\n",
"metadata": {
"size": 5312,
"modified": 1738725761194.1833,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"fk.ts": {
"content": "import { NodeSqliteError, SqlitePrimaryResultCode } from \"#errors.js\"\nimport { validationErr, type ValidationError } from \"#validate.js\"\n\n// biome-ignore lint/suspicious/noExplicitAny: <explanation>\ntype DataRow = { [key: string]: any }\nexport type DeferrableStatus =\n\t| \"NOT DEFERRABLE\"\n\t| \"DEFERRABLE INITIALLY IMMEDIATE\"\n\t| \"DEFERRABLE INITIALLY DEFERRED\"\n\nexport type KeyList<T extends DataRow> =\n\t| keyof T\n\t| `${keyof T & string},${keyof T & string}`\n\t| `${keyof T & string},${keyof T & string},${keyof T & string}`\n\n// Foreign key actions from SQLite spec\nexport type FKAction =\n\t| \"NO ACTION\"\n\t| \"RESTRICT\"\n\t| \"SET NULL\"\n\t| \"SET DEFAULT\"\n\t| \"CASCADE\"\n\nexport type ForeignKeyDef<T extends DataRow> = {\n\tkey: KeyList<T>\n\treferences: { table: string; columns: string[] }\n\tonDelete?: FKAction\n\tonUpdate?: FKAction\n\tdeferrable?: DeferrableStatus\n}\n\nfunction validateKeyList<T extends DataRow>(key: unknown): ValidationError[] {\n\tif (typeof key !== \"string\") {\n\t\treturn [validationErr({ msg: \"Key must be a string\" })]\n\t}\n\n\tconst keys = key.split(\",\").map(k => k.trim())\n\tif (keys.length > 3) {\n\t\treturn [\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"Maximum of 3 keys allowed in foreign key constraint\",\n\t\t\t}),\n\t\t]\n\t}\n\n\treturn []\n}\n\nexport function validateForeignKeys<T extends DataRow>(\n\tvalue: unknown\n): ValidationError[] {\n\tif (!value || typeof value !== \"object\") {\n\t\treturn [validationErr({ msg: \"Foreign key must be an object\" })]\n\t}\n\n\tconst errors: ValidationError[] = []\n\tconst foreignKey = value as ForeignKeyDef<T>\n\n\t// Validate key\n\terrors.push(...validateKeyList(foreignKey.key))\n\n\t// Validate references\n\tif (!foreignKey.references) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"References is required\",\n\t\t\t\tpath: \"references\",\n\t\t\t})\n\t\t)\n\t} else {\n\t\tif (typeof foreignKey.references.table !== \"string\") {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Referenced table must be a string\",\n\t\t\t\t\tpath: \"references.table\",\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\n\t\tif (!Array.isArray(foreignKey.references.columns)) {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Referenced columns must be an array\",\n\t\t\t\t\tpath: \"references.columns\",\n\t\t\t\t})\n\t\t\t)\n\t\t} else if (foreignKey.references.columns.length > 3) {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Maximum of 3 referenced columns allowed\",\n\t\t\t\t\tpath: \"references.columns\",\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t}\n\n\t// Validate actions\n\tif (foreignKey.onDelete && !isValidAction(foreignKey.onDelete)) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"Invalid ON DELETE action\",\n\t\t\t\tpath: \"onDelete\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (foreignKey.onUpdate && !isValidAction(foreignKey.onUpdate)) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"Invalid ON UPDATE action\",\n\t\t\t\tpath: \"onUpdate\",\n\t\t\t})\n\t\t)\n\t}\n\n\t// Validate deferrable\n\tif (foreignKey.deferrable && !isValidDeferrable(foreignKey.deferrable)) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"Invalid deferrable status\",\n\t\t\t\tpath: \"deferrable\",\n\t\t\t})\n\t\t)\n\t}\n\n\treturn errors\n}\n\nfunction isValidAction(action: string): action is FKAction {\n\treturn [\n\t\t\"NO ACTION\",\n\t\t\"RESTRICT\",\n\t\t\"SET NULL\",\n\t\t\"SET DEFAULT\",\n\t\t\"CASCADE\",\n\t].includes(action)\n}\n\nfunction isValidDeferrable(status: string): status is DeferrableStatus {\n\treturn [\n\t\t\"NOT DEFERRABLE\",\n\t\t\"DEFERRABLE INITIALLY IMMEDIATE\",\n\t\t\"DEFERRABLE INITIALLY DEFERRED\",\n\t].includes(status)\n}\n\nexport function isForeignKeys<T extends DataRow>(\n\tvalue: unknown\n): value is ForeignKeyDef<T> {\n\treturn validateForeignKeys<T>(value).length === 0\n}\n\nexport function buildForeignKeyStatement<T extends DataRow>(\n\tforeignKeys: ForeignKeyDef<T>[]\n): string {\n\tif (!foreignKeys?.length) {\n\t\treturn \"\"\n\t}\n\n\treturn foreignKeys\n\t\t.map(fk => {\n\t\t\tconst key = fk.key as string\n\t\t\tconst keyColumns = key.split(\",\").map(k => k.trim())\n\t\t\tconst refColumns = fk.references.columns\n\n\t\t\tif (keyColumns.length !== refColumns.length) {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_FOREIGN_KEY\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Column count mismatch\",\n\t\t\t\t\t`Foreign key columns count (${keyColumns.length}) does not match referenced columns count (${refColumns.length})`,\n\t\t\t\t\tundefined\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tlet sql = `FOREIGN KEY(${keyColumns.join(\", \")}) REFERENCES ${fk.references.table}(${refColumns.join(\", \")})`\n\n\t\t\tif (fk.onDelete) {\n\t\t\t\tsql += ` ON DELETE ${fk.onDelete}`\n\t\t\t}\n\n\t\t\tif (fk.onUpdate) {\n\t\t\t\tsql += ` ON UPDATE ${fk.onUpdate}`\n\t\t\t}\n\n\t\t\tif (fk.deferrable) {\n\t\t\t\tsql += ` ${fk.deferrable}`\n\t\t\t}\n\n\t\t\treturn sql\n\t\t})\n\t\t.join(\",\\n \")\n}\n",
"metadata": {
"size": 4431,
"modified": 1738725761194.282,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"idx.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { test, describe, beforeEach, afterEach } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { DB } from \"#database\"\nimport { NodeSqliteError } from \"#errors\"\nimport {\n\tvalidateIndexDef,\n\tbuildIndexStatement,\n\tcreateIndexName,\n\ttype IndexDef,\n} from \"./idx\"\n\ninterface TestTable {\n\tid: number\n\tname: string\n\temail: string\n\tage: number\n\tcreated_at: string\n\tmetadata: object\n}\n\ndescribe(\"Index Creation and Validation\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({ location: \":memory:\" })\n\t\tdb.exec(`CREATE TABLE test_table (\n id INTEGER PRIMARY KEY,\n name TEXT,\n email TEXT,\n age INTEGER,\n created_at TEXT,\n metadata TEXT\n )\n `)\n\n\t\tdb.exec(`CREATE TABLE users (\n id INTEGER PRIMARY KEY,\n name TEXT,\n email TEXT,\n age INTEGER\n )`)\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\tdescribe(\"validateIndexDef\", () => {\n\t\ttest(\"validates simple index definition\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_name\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name\"],\n\t\t\t}\n\t\t\tconst errors = validateIndexDef(def)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"validates complex index definition\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_composite\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name ASC\", \"age DESC COLLATE NOCASE\"],\n\t\t\t\twhere: \"WHERE age > 18\",\n\t\t\t\toptions: { unique: true, ifNotExists: true },\n\t\t\t}\n\t\t\tconst errors = validateIndexDef(def)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"rejects invalid index name\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\tname: \"invalid_name\" as any,\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name\"],\n\t\t\t}\n\t\t\tconst errors = validateIndexDef(def)\n\t\t\tassert.equal(errors.length, 1)\n\t\t\tassert.ok(\n\t\t\t\terrors[0].message.includes(\"must end with '_idx' or start with 'idx_'\")\n\t\t\t)\n\t\t})\n\n\t\ttest(\"rejects missing table name\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test\",\n\t\t\t\ttableName: \"\",\n\t\t\t\tcolumns: [\"name\"],\n\t\t\t}\n\t\t\tconst errors = validateIndexDef(def)\n\t\t\tassert.equal(errors.length, 1)\n\t\t\tassert.ok(errors[0].message.includes(\"Table name is required\"))\n\t\t})\n\n\t\ttest(\"rejects empty columns array\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [],\n\t\t\t}\n\t\t\tconst errors = validateIndexDef(def)\n\t\t\tassert.equal(errors.length, 1)\n\t\t\tassert.ok(errors[0].message.includes(\"must have at least one column\"))\n\t\t})\n\n\t\ttest(\"validates expression index\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_expr\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name(LOWER(name))\"],\n\t\t\t}\n\t\t\tconst errors = validateIndexDef(def)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"validates index with WHERE clause\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_partial\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"email\"],\n\t\t\t\twhere: \"WHERE email IS NOT NULL\",\n\t\t\t}\n\t\t\tconst errors = validateIndexDef(def)\n\t\t\tassert.equal(errors.length, 0)\n\t\t})\n\n\t\ttest(\"rejects invalid WHERE clause\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name\"],\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\twhere: \"INVALID WHERE\" as any,\n\t\t\t}\n\t\t\tconst errors = validateIndexDef(def)\n\t\t\tassert.equal(errors.length, 1)\n\t\t\tassert.ok(errors[0].message.includes(\"must start with 'WHERE'\"))\n\t\t})\n\t})\n\n\tdescribe(\"buildIndexStatement\", () => {\n\t\ttest(\"builds basic index\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_basic\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name\"],\n\t\t\t}\n\t\t\tconst sql = buildIndexStatement(def)\n\t\t\tassert.equal(sql, \"CREATE INDEX idx_test_basic ON test_table (name)\")\n\t\t})\n\n\t\ttest(\"builds unique index\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_unique\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"email\"],\n\t\t\t\toptions: { unique: true },\n\t\t\t}\n\t\t\tconst sql = buildIndexStatement(def)\n\t\t\tassert.equal(\n\t\t\t\tsql,\n\t\t\t\t\"CREATE UNIQUE INDEX idx_test_unique ON test_table (email)\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"builds IF NOT EXISTS index\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_if_not_exists\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name\"],\n\t\t\t\toptions: { ifNotExists: true },\n\t\t\t}\n\t\t\tconst sql = buildIndexStatement(def)\n\t\t\tassert.equal(\n\t\t\t\tsql,\n\t\t\t\t\"CREATE INDEX IF NOT EXISTS idx_test_if_not_exists ON test_table (name)\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"builds composite index with sort orders\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_composite\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name ASC\", \"age DESC\"],\n\t\t\t}\n\t\t\tconst sql = buildIndexStatement(def)\n\t\t\tassert.equal(\n\t\t\t\tsql,\n\t\t\t\t\"CREATE INDEX idx_test_composite ON test_table (name ASC, age DESC)\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"builds index with collation\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_collate\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name COLLATE NOCASE\"],\n\t\t\t}\n\t\t\tconst sql = buildIndexStatement(def)\n\t\t\tassert.equal(\n\t\t\t\tsql,\n\t\t\t\t\"CREATE INDEX idx_test_collate ON test_table (name COLLATE NOCASE)\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"builds partial index\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_partial\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"age\"],\n\t\t\t\twhere: \"WHERE age >= 18\",\n\t\t\t}\n\t\t\tconst sql = buildIndexStatement(def)\n\t\t\tassert.equal(\n\t\t\t\tsql,\n\t\t\t\t\"CREATE INDEX idx_test_partial ON test_table (age)\\n WHERE age >= 18\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"builds expression index\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_expr\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"name(LOWER(name))\"],\n\t\t\t}\n\t\t\tconst sql = buildIndexStatement(def)\n\t\t\tassert.equal(\n\t\t\t\tsql,\n\t\t\t\t\"CREATE INDEX idx_test_expr ON test_table (LOWER(name))\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"builds complex index with all options\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\tname: \"idx_test_complex\",\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [\"email COLLATE NOCASE\", \"created_at DESC\", \"age\"],\n\t\t\t\twhere: \"WHERE email IS NOT NULL\",\n\t\t\t\toptions: {\n\t\t\t\t\tunique: true,\n\t\t\t\t\tifNotExists: true,\n\t\t\t\t},\n\t\t\t}\n\t\t\tconst sql = buildIndexStatement(def)\n\t\t\tassert.equal(\n\t\t\t\tsql,\n\t\t\t\t\"CREATE UNIQUE INDEX IF NOT EXISTS idx_test_complex ON test_table (email COLLATE NOCASE, created_at DESC, age)\\n WHERE email IS NOT NULL\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"throws on invalid index definition\", () => {\n\t\t\tconst def: IndexDef<TestTable> = {\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\tname: \"invalid_name\" as any,\n\t\t\t\ttableName: \"test_table\",\n\t\t\t\tcolumns: [],\n\t\t\t}\n\t\t\tassert.throws(\n\t\t\t\t() => buildIndexStatement(def),\n\t\t\t\t(err: unknown) => {\n\t\t\t\t\tassert(err instanceof NodeSqliteError)\n\t\t\t\t\tassert.equal(err.code, \"ERR_SQLITE_INDEX\")\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t)\n\t\t})\n\t})\n\n\tdescribe(\"createIndexName\", () => {\n\t\ttest(\"creates valid index name\", () => {\n\t\t\tconst name = createIndexName(\"users\", \"email\", \"name\")\n\t\t\tassert.equal(name, \"idx_users_email_name\")\n\t\t})\n\n\t\ttest(\"creates valid index name with single column\", () => {\n\t\t\tconst name = createIndexName(\"users\", \"id\")\n\t\t\tassert.equal(name, \"idx_users_id\")\n\t\t})\n\t})\n})\ninterface TestTable {\n\tid: number\n\tname: string\n\temail: string\n\tage: number\n\tcity: string\n\tstate: string\n\tcountry: string\n\tcreated_at: string\n\tmetadata: object\n}\n\ndescribe(\"Database Index Creation (createIndex method)\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({ location: \":memory:\" })\n\t\tdb.exec(`\n CREATE TABLE test_table (\n id INTEGER PRIMARY KEY,\n name TEXT,\n email TEXT UNIQUE,\n age INTEGER,\n city TEXT,\n state TEXT,\n country TEXT,\n created_at TEXT,\n metadata TEXT\n )\n `)\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\t// Helper function to check if an index exists\n\tfunction indexExists(db: DB, indexName: string): boolean {\n\t\tconst result = db.sql<{ name: string }>`\n SELECT name FROM sqlite_master WHERE type='index' AND name=${\"$name\"}\n `.get({ name: indexName })\n\t\treturn !!result\n\t}\n\n\ttest(\"creates a simple index\", () => {\n\t\tconst indexDef: IndexDef<TestTable> = {\n\t\t\tname: \"idx_test_table_name\",\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [\"name\"],\n\t\t}\n\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_name\"))\n\t})\n\n\ttest(\"creates a unique index\", () => {\n\t\tconst indexDef: IndexDef<TestTable> = {\n\t\t\tname: \"idx_test_table_email\",\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [\"email\"],\n\t\t\toptions: { unique: true },\n\t\t}\n\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_email\"))\n\t})\n\n\ttest(\"creates an index with ifNotExists option\", () => {\n\t\tconst indexDef: IndexDef<TestTable> = {\n\t\t\tname: \"idx_test_table_age\",\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [\"age\"],\n\t\t\toptions: { ifNotExists: true },\n\t\t}\n\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_age\"))\n\n\t\t// Try creating again - should not throw an error\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_age\"))\n\t})\n\n\ttest(\"creates a composite index\", () => {\n\t\tconst indexDef: IndexDef<TestTable> = {\n\t\t\tname: \"idx_test_table_city_state\",\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [\"city\", \"state\"],\n\t\t}\n\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_city_state\"))\n\t})\n\n\ttest(\"creates an index with ordering\", () => {\n\t\tconst indexDef: IndexDef<TestTable> = {\n\t\t\tname: \"idx_test_table_name_age\",\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [\"name ASC\", \"age DESC\"],\n\t\t}\n\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_name_age\"))\n\t})\n\n\ttest(\"creates an index with a WHERE clause\", () => {\n\t\tconst indexDef: IndexDef<TestTable> = {\n\t\t\tname: \"idx_test_table_age\",\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [\"age\"],\n\t\t\twhere: \"WHERE age > 18\",\n\t\t}\n\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_age\"))\n\t})\n\n\ttest(\"creates an index with COLLATE option\", () => {\n\t\tconst indexDef: IndexDef<TestTable> = {\n\t\t\tname: \"idx_test_table_name_collate\",\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [\"name COLLATE NOCASE\"],\n\t\t}\n\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_name_collate\"))\n\t})\n\ttest(\"creates an index with an expression\", () => {\n\t\tconst indexDef: IndexDef<TestTable> = {\n\t\t\tname: \"idx_test_table_name_lower\",\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [\"name(LOWER(name))\"],\n\t\t}\n\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_name_lower\"))\n\t})\n\n\ttest(\"throws an error on invalid index definition\", () => {\n\t\tconst invalidIndexDef = {\n\t\t\tname: \"invalid_index\", // Invalid name\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [], // Empty columns\n\t\t}\n\t\tassert.throws(\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t() => db.createIndex(invalidIndexDef as any),\n\t\t\t(err: unknown) => {\n\t\t\t\tassert(err instanceof NodeSqliteError)\n\t\t\t\tassert.equal(err.code, \"ERR_SQLITE_INDEX\")\n\t\t\t\treturn true\n\t\t\t}\n\t\t)\n\t})\n\ttest(\"throws an error on duplicate index name\", () => {\n\t\tconst indexDef: IndexDef<TestTable> = {\n\t\t\tname: \"idx_test_table_name\",\n\t\t\ttableName: \"test_table\",\n\t\t\tcolumns: [\"name\"],\n\t\t}\n\n\t\tdb.createIndex(indexDef)\n\t\tassert.ok(indexExists(db, \"idx_test_table_name\"))\n\n\t\t// Try creating again - should throw an error\n\t\tassert.throws(\n\t\t\t() => db.createIndex(indexDef),\n\t\t\t(err: unknown) => {\n\t\t\t\tassert(err instanceof Error)\n\t\t\t\tassert.equal(err.message, \"index idx_test_table_name already exists\")\n\t\t\t\treturn true\n\t\t\t}\n\t\t)\n\t})\n})\n",
"metadata": {
"size": 11809,
"modified": 1738725761194.4124,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"idx.ts": {
"content": "import { NodeSqliteError, SqlitePrimaryResultCode } from \"#errors.js\"\nimport type { DataRow } from \"#types.js\"\nimport { validationErr, type ValidationError } from \"#validate.js\"\n\ntype IndexColumn<T extends DataRow> =\n\t`${keyof T & string}${\"\" | \" ASC\" | \" DESC\"}${\"\" | ` COLLATE ${string}`}`\n\ntype WhereExpr = `WHERE ${string}`\n\ntype IndexName = `${string}_idx` | `idx_${string}`\n\ntype IndexOptions = {\n\tunique?: boolean\n\tifNotExists?: boolean\n}\n\nexport type IndexDef<T extends DataRow> = {\n\tname: IndexName\n\ttableName: string\n\tcolumns: IndexColumn<T>[] | [`${keyof T & string}(${string})`]\n\twhere?: WhereExpr\n\toptions?: IndexOptions\n}\n\nexport function validateIndexDef<T extends DataRow>(\n\tdef: IndexDef<T>\n): ValidationError[] {\n\tconst errors: ValidationError[] = []\n\n\tif (!def.name.endsWith(\"_idx\") && !def.name.startsWith(\"idx_\")) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"Index name must end with '_idx' or start with 'idx_'\",\n\t\t\t\tpath: \"name\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (!def.tableName) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"Table name is required\",\n\t\t\t\tpath: \"tableName\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (!Array.isArray(def.columns) || def.columns.length === 0) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"Index must have at least one column\",\n\t\t\t\tpath: \"columns\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (def.where && !def.where.startsWith(\"WHERE \")) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"WHERE clause must start with 'WHERE'\",\n\t\t\t\tpath: \"where\",\n\t\t\t})\n\t\t)\n\t}\n\n\treturn errors\n}\n\nexport function buildIndexStatement<T extends DataRow>(\n\tdef: IndexDef<T>\n): string {\n\tconst errors = validateIndexDef(def)\n\tif (errors.length > 0) {\n\t\tthrow new NodeSqliteError(\n\t\t\t\"ERR_SQLITE_INDEX\",\n\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\"Invalid index definition\",\n\t\t\terrors.map(e => e.message).join(\"\\n\"),\n\t\t\tundefined\n\t\t)\n\t}\n\n\t// Extract expressions from column definitions\n\tconst columns = def.columns\n\t\t.map(col => {\n\t\t\tconst match =\n\t\t\t\ttypeof col === \"string\" ? col.match(/^(\\w+)\\((.*)\\)$/) : null\n\t\t\treturn match ? match[2] : col\n\t\t})\n\t\t.join(\", \")\n\n\tconst unique = def.options?.unique ? \"UNIQUE \" : \"\"\n\tconst ifNotExists = def.options?.ifNotExists ? \"IF NOT EXISTS \" : \"\"\n\tconst where = def.where ? `\\n ${def.where}` : \"\"\n\n\treturn `CREATE ${unique}INDEX ${ifNotExists}${def.name} ON ${def.tableName} (${columns})${where}`\n}\n\nexport function createIndexName(\n\tprefix: string,\n\t...columns: string[]\n): IndexName {\n\treturn `idx_${prefix}_${columns.join(\"_\")}` as IndexName\n}\n",
"metadata": {
"size": 2455,
"modified": 1738725761194.4927,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"index.ts": {
"content": "/**\n * @module @takinprofit/sqlitex\n *\n * A type-safe SQLite query builder and database wrapper for Node.js\n *\n * Features:\n * - Type-safe SQL template literals\n * - Prepared statement caching\n * - JSON column support\n * - Strongly typed table schemas\n * - SQL query composition\n * - Full SQLite PRAGMA configuration\n *\n * @example\n * ```ts\n * import { DB } from '@takinprofit/sqlitex'\n *\n * const db = new DB({ location: ':memory:' })\n *\n * // Type-safe queries\n * const users = db.sql<{id: number}>`\n * SELECT * FROM users\n * WHERE id = ${'$id'}\n * `\n * const result = users.get({ id: 1 })\n * ```\n *\n * @see {@link https://github.com/takinprofit/sqlitex/blob/main/README.md|Documentation}\n */\nexport type { ValidationError } from \"#validate\"\nexport * from \"#types\"\nexport { Sql, raw } from \"#sql\"\nexport type { XStatementSync } from \"#sql\"\n\nexport type { DeferrableStatus, FKAction, ForeignKeyDef } from \"#fk\"\n\nexport { PragmaDefaults } from \"#pragmas\"\nexport type {\n\tJournalMode,\n\tJournalModes,\n\tSynchronousMode,\n\tSynchronousModes,\n\tTempStore,\n\tTempStores,\n\tLockingMode,\n\tLockingModes,\n} from \"#pragmas\"\n\nexport { DB } from \"#database\"\n\nexport * from \"#logger\"\nexport * from \"#errors\"\n",
"metadata": {
"size": 1192,
"modified": 1738725761194.5752,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"logger.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nexport enum LogLevel {\n\tERROR = \"error\",\n\tWARN = \"warn\",\n\tINFO = \"info\",\n\tDEBUG = \"debug\",\n\tTRACE = \"trace\",\n}\n\nexport type LogFn = (message: string, ...meta: unknown[]) => void\n\nexport interface Logger {\n\terror: LogFn\n\twarn: LogFn\n\tinfo: LogFn\n\tdebug: LogFn\n\ttrace: LogFn\n}\n\nexport interface LogMessage {\n\tlevel: LogLevel\n\tmessage: string\n\ttimestamp: string\n\tmeta?: unknown[]\n}\n\n/**\n * Default logger that writes to console with timestamps\n */\nexport class ConsoleLogger implements Logger {\n\t#minLevel: LogLevel\n\n\tconstructor(minLevel: LogLevel = LogLevel.INFO) {\n\t\tthis.#minLevel = minLevel\n\t}\n\n\t#shouldLog(level: LogLevel): boolean {\n\t\tconst levels = Object.values(LogLevel)\n\t\treturn levels.indexOf(level) <= levels.indexOf(this.#minLevel)\n\t}\n\n\t#formatMessage(\n\t\tlevel: LogLevel,\n\t\tmessage: string,\n\t\tmeta: unknown[] = []\n\t): LogMessage {\n\t\treturn {\n\t\t\tlevel,\n\t\t\tmessage,\n\t\t\ttimestamp: new Date().toISOString(),\n\t\t\tmeta: meta.length > 0 ? meta : undefined,\n\t\t}\n\t}\n\n\terror(message: string, ...meta: unknown[]): void {\n\t\tif (this.#shouldLog(LogLevel.ERROR)) {\n\t\t\tconsole.error(this.#formatMessage(LogLevel.ERROR, message, meta))\n\t\t}\n\t}\n\n\twarn(message: string, ...meta: unknown[]): void {\n\t\tif (this.#shouldLog(LogLevel.WARN)) {\n\t\t\tconsole.warn(this.#formatMessage(LogLevel.WARN, message, meta))\n\t\t}\n\t}\n\n\tinfo(message: string, ...meta: unknown[]): void {\n\t\tif (this.#shouldLog(LogLevel.INFO)) {\n\t\t\tconsole.info(this.#formatMessage(LogLevel.INFO, message, meta))\n\t\t}\n\t}\n\n\tdebug(message: string, ...meta: unknown[]): void {\n\t\tif (this.#shouldLog(LogLevel.DEBUG)) {\n\t\t\tconsole.debug(this.#formatMessage(LogLevel.DEBUG, message, meta))\n\t\t}\n\t}\n\n\ttrace(message: string, ...meta: unknown[]): void {\n\t\tif (this.#shouldLog(LogLevel.TRACE)) {\n\t\t\tconsole.trace(this.#formatMessage(LogLevel.TRACE, message, meta))\n\t\t}\n\t}\n}\n\n/**\n * No-op logger that does nothing\n */\nexport class NoopLogger implements Logger {\n\terror = () => {}\n\twarn = () => {}\n\tinfo = () => {}\n\tdebug = () => {}\n\ttrace = () => {}\n}\n",
"metadata": {
"size": 2146,
"modified": 1738725761194.6577,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"order-by.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { test, describe, beforeEach, afterEach } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { buildOrderByStatement } from \"#order-by\"\nimport { NodeSqliteError } from \"#errors\"\nimport { DB } from \"#database\"\n\ndescribe(\"buildOrderByStatement\", () => {\n\ttest(\"generates single column order\", () => {\n\t\tconst sql = buildOrderByStatement({ name: \"ASC\" })\n\t\tassert.equal(sql, \"ORDER BY name ASC\")\n\t})\n\n\ttest(\"generates multi-column order\", () => {\n\t\tconst sql = buildOrderByStatement({\n\t\t\tage: \"DESC\",\n\t\t\tname: \"ASC\",\n\t\t})\n\t\tassert.equal(sql, \"ORDER BY age DESC, name ASC\")\n\t})\n\n\ttest(\"throws on invalid direction\", () => {\n\t\tassert.throws(\n\t\t\t() => buildOrderByStatement({ name: \"ASCENDING\" as \"ASC\" | \"DESC\" }),\n\t\t\t(err: unknown) => {\n\t\t\t\tassert(err instanceof NodeSqliteError)\n\t\t\t\tassert(err.message.includes(\"Sort direction must be\"))\n\t\t\t\treturn true\n\t\t\t}\n\t\t)\n\t})\n\n\ttest(\"throws on empty orderBy\", () => {\n\t\tassert.throws(\n\t\t\t() => buildOrderByStatement(),\n\t\t\t(err: unknown) => {\n\t\t\t\tassert(err instanceof NodeSqliteError)\n\t\t\t\tassert(err.message.includes(\"must be a non-empty object\"))\n\t\t\t\treturn true\n\t\t\t}\n\t\t)\n\t})\n})\n\ndescribe(\"OrderBy Context SQL Generation\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({ location: \":memory:\" })\n\t\tdb.exec(`\n CREATE TABLE users (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n age INTEGER NOT NULL,\n email TEXT UNIQUE\n );\n `)\n\n\t\tconst insert = db.sql<{ name: string; age: number; email: string }>`\n INSERT INTO users (name, age, email)\n VALUES (${\"$name\"}, ${\"$age\"}, ${\"$email\"})\n `\n\n\t\tinsert.run({ name: \"John\", age: 30, email: \"john@example.com\" })\n\t\tinsert.run({ name: \"Alice\", age: 25, email: \"alice@example.com\" })\n\t\tinsert.run({ name: \"Bob\", age: 35, email: \"bob@example.com\" })\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\ttest(\"generates and executes formatted ORDER BY clause\", () => {\n\t\tconst query = db.sql<Record<string, never>>`\n SELECT * FROM users\n ${{ orderBy: { name: \"ASC\", age: \"DESC\" } }}\n `\n\n\t\tconst results = query.all<{ name: string; age: number }>()\n\n\t\tassert.equal(\n\t\t\tquery.sourceSQL().trim(),\n\t\t\t\"SELECT *\\nFROM users\\nORDER BY name ASC,\\n age DESC\"\n\t\t)\n\n\t\t// Verify order\n\t\tassert.equal(results[0].name, \"Alice\")\n\t\tassert.equal(results[1].name, \"Bob\")\n\t\tassert.equal(results[2].name, \"John\")\n\t})\n})\n",
"metadata": {
"size": 2531,
"modified": 1738725761194.7378,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"order-by.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport type { DataRow } from \"#types\"\nimport { NodeSqliteError, SqlitePrimaryResultCode } from \"#errors\"\n\nexport function buildOrderByStatement<P extends DataRow>(\n\torderBy?: Partial<Record<keyof P, \"ASC\" | \"DESC\">>\n): string {\n\tif (!orderBy || Object.keys(orderBy).length === 0) {\n\t\tthrow new NodeSqliteError(\n\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\"Invalid orderBy configuration\",\n\t\t\t\"OrderBy must be a non-empty object with column names as keys and 'ASC' or 'DESC' as values\",\n\t\t\tundefined\n\t\t)\n\t}\n\n\tconst orderClauses = Object.entries(orderBy).map(([column, direction]) => {\n\t\tif (direction !== \"ASC\" && direction !== \"DESC\") {\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\"Invalid sort direction\",\n\t\t\t\t`Sort direction must be 'ASC' or 'DESC', got '${direction}'`,\n\t\t\t\tundefined\n\t\t\t)\n\t\t}\n\t\treturn `${column} ${direction}`\n\t})\n\n\treturn `ORDER BY ${orderClauses.join(\", \")}`\n}\n",
"metadata": {
"size": 1119,
"modified": 1738725761194.8147,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"pragma.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { test, describe } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport {\n\ttype PragmaConfig,\n\tPragmaDefaults,\n\tgetPragmaStatements,\n} from \"./pragmas.js\"\n\ndescribe(\"SQLite Pragma Configuration\", async () => {\n\ttest(\"getPragmaStatements should generate correct PRAGMA statements\", () => {\n\t\tconst config: PragmaConfig = {\n\t\t\tjournalMode: \"WAL\",\n\t\t\tsynchronous: \"NORMAL\",\n\t\t\tcacheSize: -64000,\n\t\t\ttempStore: \"MEMORY\",\n\t\t\tforeignKeys: true,\n\t\t}\n\n\t\tconst statements = getPragmaStatements(config)\n\n\t\tassert.deepEqual(statements, [\n\t\t\t\"PRAGMA journal_mode=WAL;\",\n\t\t\t\"PRAGMA synchronous=NORMAL;\",\n\t\t\t\"PRAGMA cache_size=-64000;\",\n\t\t\t\"PRAGMA temp_store=MEMORY;\",\n\t\t\t\"PRAGMA foreign_keys=ON;\",\n\t\t])\n\t})\n\n\ttest(\"getPragmaStatements should handle boolean values correctly\", () => {\n\t\tconst config: PragmaConfig = {\n\t\t\tforeignKeys: true,\n\t\t\ttrustedSchema: false,\n\t\t}\n\n\t\tconst statements = getPragmaStatements(config)\n\n\t\tassert.deepEqual(statements, [\n\t\t\t\"PRAGMA foreign_keys=ON;\",\n\t\t\t\"PRAGMA trusted_schema=OFF;\",\n\t\t])\n\t})\n\n\ttest(\"development configuration should be complete and valid\", () => {\n\t\tconst statements = getPragmaStatements(PragmaDefaults.development)\n\n\t\tassert(statements.length > 0)\n\t\tassert(statements.every(stmt => stmt.startsWith(\"PRAGMA \")))\n\t\tassert(statements.every(stmt => stmt.endsWith(\";\")))\n\n\t\t// Check specific development settings\n\t\tassert(statements.includes(\"PRAGMA journal_mode=WAL;\"))\n\t\tassert(statements.includes(\"PRAGMA synchronous=NORMAL;\"))\n\t\tassert(statements.includes(\"PRAGMA cache_size=-64000;\"))\n\t})\n\n\ttest(\"testing configuration should be optimized for speed\", () => {\n\t\tconst statements = getPragmaStatements(PragmaDefaults.testing)\n\n\t\tassert(statements.includes(\"PRAGMA synchronous=OFF;\"))\n\t\tassert(statements.includes(\"PRAGMA locking_mode=EXCLUSIVE;\"))\n\t\tassert(statements.includes(\"PRAGMA temp_store=MEMORY;\"))\n\t})\n\n\ttest(\"production configuration should be optimized for safety\", () => {\n\t\tconst statements = getPragmaStatements(PragmaDefaults.production)\n\n\t\tassert(statements.includes(\"PRAGMA trusted_schema=OFF;\"))\n\t\tassert(statements.includes(\"PRAGMA synchronous=NORMAL;\"))\n\t\tassert(statements.includes(\"PRAGMA foreign_keys=ON;\"))\n\t})\n\n\ttest(\"empty config should generate no statements\", () => {\n\t\tconst statements = getPragmaStatements({})\n\t\tassert.equal(statements.length, 0)\n\t})\n})\n",
"metadata": {
"size": 2497,
"modified": 1738725761194.8943,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"pragmas.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { validationErr, type ValidationError } from \"./validate\"\n\n/**\n * SQLite journal mode settings\n * @see https://www.sqlite.org/pragma.html#pragma_journal_mode\n */\nexport const JournalModes = [\n\t\"DELETE\",\n\t\"TRUNCATE\",\n\t\"PERSIST\",\n\t\"MEMORY\",\n\t\"WAL\",\n\t\"OFF\",\n] as const\nexport type JournalMode = (typeof JournalModes)[number]\n\n/**\n * Synchronization settings for transaction safety\n * @see https://www.sqlite.org/pragma.html#pragma_synchronous\n */\nexport const SynchronousModes = [\"OFF\", \"NORMAL\", \"FULL\", \"EXTRA\"] as const\nexport type SynchronousMode = (typeof SynchronousModes)[number]\n\n/**\n * Temporary storage location settings\n * @see https://www.sqlite.org/pragma.html#pragma_temp_store\n */\nexport const TempStores = [\"DEFAULT\", \"FILE\", \"MEMORY\"] as const\nexport type TempStore = (typeof TempStores)[number]\n\n/**\n * Database locking mode settings\n * @see https://www.sqlite.org/pragma.html#pragma_locking_mode\n */\nexport const LockingModes = [\"NORMAL\", \"EXCLUSIVE\"] as const\nexport type LockingMode = (typeof LockingModes)[number]\n\n/**\n * SQLite PRAGMA configuration options\n */\nexport type PragmaConfig = Partial<{\n\t/** Journal mode for transaction logging */\n\tjournalMode: JournalMode\n\n\t/** Level of transaction synchronization */\n\tsynchronous: SynchronousMode\n\n\t/** Database cache size in kilobytes */\n\tcacheSize: number\n\n\t/** Memory-mapped I/O size in bytes */\n\tmmapSize: number\n\n\t/** Temporary storage location */\n\ttempStore: TempStore\n\n\t/** Database locking mode */\n\tlockingMode: LockingMode\n\n\t/** Busy handler timeout in milliseconds */\n\tbusyTimeout: number\n\n\t/** Enable/disable foreign key constraints */\n\tforeignKeys: boolean\n\n\t/** Write-ahead log auto-checkpoint size */\n\twalAutocheckpoint: number\n\n\t/** Allow/disallow untrusted schema loads */\n\ttrustedSchema: boolean\n}>\n\nexport function validatePragmaConfig(config: unknown): ValidationError[] {\n\tconst errors: ValidationError[] = []\n\n\tif (typeof config !== \"object\" || config === null) {\n\t\treturn [validationErr({ msg: \"PragmaConfig must be an object\" })]\n\t}\n\n\tconst pragmaConfig = config as Record<string, unknown>\n\n\tif (\n\t\t\"journalMode\" in pragmaConfig &&\n\t\t!JournalModes.includes(pragmaConfig.journalMode as JournalMode)\n\t) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: `Invalid journal mode: ${pragmaConfig.journalMode}`,\n\t\t\t\tpath: \"journalMode\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (\n\t\t\"synchronous\" in pragmaConfig &&\n\t\t!SynchronousModes.includes(pragmaConfig.synchronous as SynchronousMode)\n\t) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: `Invalid synchronous mode: ${pragmaConfig.synchronous}`,\n\t\t\t\tpath: \"synchronous\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (\n\t\t\"tempStore\" in pragmaConfig &&\n\t\t!TempStores.includes(pragmaConfig.tempStore as TempStore)\n\t) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: `Invalid temp store: ${pragmaConfig.tempStore}`,\n\t\t\t\tpath: \"tempStore\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (\n\t\t\"lockingMode\" in pragmaConfig &&\n\t\t!LockingModes.includes(pragmaConfig.lockingMode as LockingMode)\n\t) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: `Invalid locking mode: ${pragmaConfig.lockingMode}`,\n\t\t\t\tpath: \"lockingMode\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (\n\t\t\"cacheSize\" in pragmaConfig &&\n\t\ttypeof pragmaConfig.cacheSize !== \"number\"\n\t) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"cacheSize must be a number\",\n\t\t\t\tpath: \"cacheSize\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (\"mmapSize\" in pragmaConfig && typeof pragmaConfig.mmapSize !== \"number\") {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"mmapSize must be a number\",\n\t\t\t\tpath: \"mmapSize\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (\n\t\t\"busyTimeout\" in pragmaConfig &&\n\t\ttypeof pragmaConfig.busyTimeout !== \"number\"\n\t) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"busyTimeout must be a number\",\n\t\t\t\tpath: \"busyTimeout\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (\n\t\t\"foreignKeys\" in pragmaConfig &&\n\t\ttypeof pragmaConfig.foreignKeys !== \"boolean\"\n\t) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"foreignKeys must be a boolean\",\n\t\t\t\tpath: \"foreignKeys\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (\n\t\t\"walAutocheckpoint\" in pragmaConfig &&\n\t\ttypeof pragmaConfig.walAutocheckpoint !== \"number\"\n\t) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"walAutocheckpoint must be a number\",\n\t\t\t\tpath: \"walAutocheckpoint\",\n\t\t\t})\n\t\t)\n\t}\n\n\tif (\n\t\t\"trustedSchema\" in pragmaConfig &&\n\t\ttypeof pragmaConfig.trustedSchema !== \"boolean\"\n\t) {\n\t\terrors.push(\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"trustedSchema must be a boolean\",\n\t\t\t\tpath: \"trustedSchema\",\n\t\t\t})\n\t\t)\n\t}\n\n\treturn errors\n}\n\n/**\n * Default pragma configurations for different environments\n */\nexport const PragmaDefaults: Record<\n\t\"development\" | \"testing\" | \"production\",\n\tPragmaConfig\n> = {\n\t/**\n\t * Development environment defaults - optimized for development workflow\n\t */\n\tdevelopment: {\n\t\tjournalMode: \"WAL\",\n\t\tsynchronous: \"NORMAL\",\n\t\tcacheSize: -64000, // 64MB cache\n\t\ttempStore: \"MEMORY\",\n\t\tmmapSize: 64000000, // 64MB mmap\n\t\tlockingMode: \"NORMAL\",\n\t\tbusyTimeout: 5000,\n\t\tforeignKeys: true,\n\t\twalAutocheckpoint: 1000,\n\t\ttrustedSchema: true,\n\t},\n\n\t/**\n\t * Testing environment defaults - optimized for in-memory testing\n\t */\n\ttesting: {\n\t\tjournalMode: \"WAL\",\n\t\tsynchronous: \"OFF\", // Less durable but faster for testing\n\t\tcacheSize: -32000, // 32MB cache is enough for testing\n\t\ttempStore: \"MEMORY\",\n\t\tlockingMode: \"EXCLUSIVE\", // Reduce lock conflicts\n\t\tbusyTimeout: 5000,\n\t\tforeignKeys: true,\n\t\twalAutocheckpoint: 1000,\n\t\ttrustedSchema: true,\n\t},\n\n\t/**\n\t * Production environment defaults - optimized for durability and performance\n\t */\n\tproduction: {\n\t\tjournalMode: \"WAL\",\n\t\tsynchronous: \"NORMAL\",\n\t\tcacheSize: -64000, // 64MB cache\n\t\ttempStore: \"MEMORY\",\n\t\tmmapSize: 268435456, // 256MB mmap\n\t\tlockingMode: \"NORMAL\",\n\t\tbusyTimeout: 10000,\n\t\tforeignKeys: true,\n\t\twalAutocheckpoint: 1000,\n\t\ttrustedSchema: false, // Safer default for production\n\t},\n}\n\n/**\n * Generates SQLite PRAGMA statements from configuration\n */\nexport function getPragmaStatements(config: PragmaConfig): string[] {\n\tconst statements: string[] = []\n\n\tif (config.journalMode) {\n\t\tstatements.push(`PRAGMA journal_mode=${config.journalMode};`)\n\t}\n\n\tif (config.synchronous) {\n\t\tstatements.push(`PRAGMA synchronous=${config.synchronous};`)\n\t}\n\n\tif (config.cacheSize !== undefined) {\n\t\tstatements.push(`PRAGMA cache_size=${config.cacheSize};`)\n\t}\n\n\tif (config.mmapSize !== undefined) {\n\t\tstatements.push(`PRAGMA mmap_size=${config.mmapSize};`)\n\t}\n\n\tif (config.tempStore) {\n\t\tstatements.push(`PRAGMA temp_store=${config.tempStore};`)\n\t}\n\n\tif (config.lockingMode) {\n\t\tstatements.push(`PRAGMA locking_mode=${config.lockingMode};`)\n\t}\n\n\tif (config.busyTimeout !== undefined) {\n\t\tstatements.push(`PRAGMA busy_timeout=${config.busyTimeout};`)\n\t}\n\n\tif (config.foreignKeys !== undefined) {\n\t\tstatements.push(`PRAGMA foreign_keys=${config.foreignKeys ? \"ON\" : \"OFF\"};`)\n\t}\n\n\tif (config.walAutocheckpoint !== undefined) {\n\t\tstatements.push(`PRAGMA wal_autocheckpoint=${config.walAutocheckpoint};`)\n\t}\n\n\tif (config.trustedSchema !== undefined) {\n\t\tstatements.push(\n\t\t\t`PRAGMA trusted_schema=${config.trustedSchema ? \"ON\" : \"OFF\"};`\n\t\t)\n\t}\n\n\treturn statements\n}\n",
"metadata": {
"size": 7101,
"modified": 1738725761195.131,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"schema.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n// columns.test.ts\nimport { test, describe, beforeEach, afterEach } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { validateSchema, buildSchema } from \"./schema\"\nimport { NodeSqliteError } from \"#errors\"\nimport { DB } from \"#database\"\nimport type { Schema } from \"#types\"\n\ninterface TestUser {\n\tid: number\n\tname?: string\n\tactive: boolean\n\tmetadata: { tags: string[] }\n}\n\n// Updated test file\ndescribe(\"Column Validation\", () => {\n\ttest(\"validates simple column definitions\", () => {\n\t\tconst columns = {\n\t\t\tid: \"INTEGER PRIMARY KEY\",\n\t\t\tname: \"TEXT\",\n\t\t\tactive: \"INTEGER\",\n\t\t\tmetadata: \"BLOB\",\n\t\t}\n\t\tconst errors = validateSchema<TestUser>(columns)\n\t\tassert.equal(errors.length, 0)\n\t})\n\n\ttest(\"validates columns with constraints\", () => {\n\t\tconst columns = {\n\t\t\tid: \"INTEGER PRIMARY KEY AUTOINCREMENT\",\n\t\t\tname: \"TEXT NOT NULL UNIQUE\",\n\t\t\tstatus: \"INTEGER DEFAULT 1\",\n\t\t}\n\t\tconst errors = validateSchema<TestUser>(columns)\n\t\tassert.equal(errors.length, 0)\n\t})\n\n\ttest(\"rejects invalid types\", () => {\n\t\tconst columns = {\n\t\t\tid: \"INVALID\",\n\t\t\tname: \"STRING\",\n\t\t}\n\t\tconst errors = validateSchema<TestUser>(columns)\n\t\tassert.equal(errors.length, 2)\n\t})\n\n\ttest(\"requires valid SQLite type\", () => {\n\t\tconst columns = {\n\t\t\tid: \"PRIMARY KEY\",\n\t\t\tname: \"NOT NULL\",\n\t\t}\n\t\tconst errors = validateSchema<TestUser>(columns)\n\t\tassert.equal(errors.length, 2)\n\t})\n})\n\ntest(\"buildColumnsStatement generates correct SQL DDL\", () => {\n\tinterface TestTable {\n\t\tid: number\n\t\tname: string\n\t\tactive: boolean\n\t\tmetadata: object\n\t}\n\n\tconst columns: Schema<TestTable> = {\n\t\tid: \"INTEGER PRIMARY KEY AUTOINCREMENT\",\n\t\tname: \"TEXT NOT NULL\",\n\t\tactive: \"INTEGER DEFAULT 1\",\n\t\tmetadata: \"BLOB\",\n\t}\n\n\tconst sql = buildSchema(columns)\n\tassert.equal(\n\t\tsql,\n\t\t\"(\\n id INTEGER PRIMARY KEY AUTOINCREMENT,\\n name TEXT NOT NULL,\\n active INTEGER DEFAULT 1,\\n metadata BLOB\\n)\"\n\t)\n})\n\ntest(\"buildColumnsStatement handles complex constraints\", () => {\n\tinterface ComplexTable {\n\t\tid: number\n\t\tref: number\n\t\tcode: string\n\t}\n\n\tconst columns: Schema<ComplexTable> = {\n\t\tid: \"INTEGER PRIMARY KEY CHECK (id > 0) NOT NULL\",\n\t\tref: \"INTEGER FOREIGN KEY REFERENCES users (id)\",\n\t\tcode: \"TEXT UNIQUE DEFAULT 'none'\",\n\t}\n\n\tconst sql = buildSchema(columns)\n\tassert.equal(\n\t\tsql,\n\t\t\"(\\n id INTEGER PRIMARY KEY CHECK (id > 0) NOT NULL,\\n ref INTEGER FOREIGN KEY REFERENCES users (id),\\n code TEXT UNIQUE DEFAULT 'none'\\n)\"\n\t)\n})\n\ntest(\"buildColumnsStatement throws on invalid definitions\", () => {\n\tconst invalidColumns = {\n\t\tid: \"INTEGER INVALID\",\n\t\tname: 123,\n\t}\n\n\tassert.throws(\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t() => buildSchema(invalidColumns as any),\n\t\tNodeSqliteError\n\t)\n})\n\ndescribe(\"Columns Context SQL Generation\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({\n\t\t\tlocation: \":memory:\",\n\t\t\tenvironment: \"testing\",\n\t\t})\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\ttest(\"generates CREATE TABLE with column definitions\", () => {\n\t\tinterface TestTable {\n\t\t\tid: number\n\t\t\tname: string\n\t\t\tactive: boolean\n\t\t\tdata: object\n\t\t}\n\n\t\tconst stmt = db.sql<TestTable>`\n CREATE TABLE test_table ${{\n\t\t\t\tschema: {\n\t\t\t\t\tid: \"INTEGER PRIMARY KEY AUTOINCREMENT\",\n\t\t\t\t\tname: \"TEXT NOT NULL\",\n\t\t\t\t\tactive: \"INTEGER DEFAULT 1\",\n\t\t\t\t\tdata: \"BLOB\",\n\t\t\t\t},\n\t\t\t}};\n `\n\n\t\tassert.equal(\n\t\t\tstmt.sourceSQL({} as TestTable).trim(),\n\t\t\t\"CREATE TABLE test_table (\\n id INTEGER PRIMARY KEY AUTOINCREMENT,\\n name TEXT NOT NULL,\\n active INTEGER DEFAULT 1,\\n data BLOB\\n);\"\n\t\t)\n\t})\n\n\ttest(\"validates CREATE TABLE with VALUES\", () => {\n\t\tinterface TestTable {\n\t\t\tid: number\n\t\t\tname: string\n\t\t}\n\n\t\tconst stmt = db.sql<TestTable>`\n CREATE TABLE test_table (\n id INTEGER PRIMARY KEY,\n name TEXT\n );\n INSERT INTO test_table ${{ values: [\"$id\", \"$name\"] }}\n `\n\n\t\tassert.equal(\n\t\t\tstmt.sourceSQL({ id: 1, name: \"test\" }).trim(),\n\t\t\t\"CREATE TABLE test_table (\\n id INTEGER PRIMARY KEY,\\n name TEXT\\n);\"\n\t\t)\n\t})\n})\n",
"metadata": {
"size": 4097,
"modified": 1738725761195.2483,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"schema.ts": {
"content": "import { NodeSqliteError, SqlitePrimaryResultCode } from \"#errors.js\"\nimport { buildForeignKeyStatement, validateForeignKeys } from \"#fk.js\"\nimport type { DataRow, Schema } from \"#types\"\nimport { validationErr, type ValidationError } from \"#validate\"\n\nconst columnRegex = /^(TEXT|INTEGER|REAL|BLOB)(\\s+.+)?$/\n\nexport function validateSchema<T extends DataRow>(\n\tvalue: unknown\n): ValidationError[] {\n\tif (!value || typeof value !== \"object\") {\n\t\treturn [validationErr({ msg: \"Columns must be an object\" })]\n\t}\n\n\tconst errors: ValidationError[] = []\n\tconst columns = value as Record<string, unknown>\n\n\tfor (const [key, def] of Object.entries(columns)) {\n\t\tif (key === \"$$foreignKeys\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tif (typeof def !== \"string\") {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: `Column '${key}' definition must be a string`,\n\t\t\t\t\tpath: key,\n\t\t\t\t})\n\t\t\t)\n\t\t\tcontinue\n\t\t}\n\n\t\tif (!columnRegex.test(def.trim())) {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: `Invalid column definition format for '${key}'`,\n\t\t\t\t\tpath: key,\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t}\n\n\tif (\"$$foreignKeys\" in columns) {\n\t\t// Validate the foreign keys array\n\t\tconst fks = columns.$$foreignKeys\n\t\tif (!Array.isArray(fks)) {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Foreign keys must be an array\",\n\t\t\t\t\tpath: \"$$foreignKeys\",\n\t\t\t\t})\n\t\t\t)\n\t\t} else {\n\t\t\tfks.forEach((fk, idx) => {\n\t\t\t\tconst fkErrors = validateForeignKeys(fk)\n\t\t\t\terrors.push(\n\t\t\t\t\t...fkErrors.map(err => ({\n\t\t\t\t\t\t...err,\n\t\t\t\t\t\tpath: `$$foreignKeys[${idx}].${err.path || \"\"}`,\n\t\t\t\t\t}))\n\t\t\t\t)\n\t\t\t})\n\t\t}\n\t}\n\n\treturn errors\n}\n\nexport function isValidSchema<T extends DataRow>(\n\tvalue: unknown\n): value is Schema<T> {\n\treturn validateSchema<T>(value).length === 0\n}\n\nexport function buildSchema<T extends DataRow>(columns: Schema<T>): string {\n\tconst errors = validateSchema<T>(columns)\n\tif (errors.length > 0) {\n\t\tthrow new NodeSqliteError(\n\t\t\t\"ERR_SQLITE_COLUMNS\",\n\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\"Invalid column definitions\",\n\t\t\terrors.map(e => e.message).join(\"\\n\"),\n\t\t\tundefined\n\t\t)\n\t}\n\n\tconst columnDefs = Object.entries(columns)\n\t\t.filter(([key]) => key !== \"$$foreignKeys\")\n\t\t.map(([name, def]) => `${name} ${String(def).trim()}`)\n\n\tconst foreignKeys = columns.$$foreignKeys\n\t\t? buildForeignKeyStatement(columns.$$foreignKeys)\n\t\t: null\n\n\tconst allDefs = foreignKeys ? [...columnDefs, foreignKeys] : columnDefs\n\n\treturn `(\\n ${allDefs.join(\",\\n \")}\\n)`\n}\n",
"metadata": {
"size": 2398,
"modified": 1738725761195.3386,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"set.test.ts": {
"content": "// In values.set.test.ts\nimport { test, describe, beforeEach, afterEach } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { DB } from \"#database\"\nimport { raw } from \"#sql\"\n\ndescribe(\"SET Operations\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({ location: \":memory:\" })\n\t\tdb.exec(`\n CREATE TABLE users (\n id INTEGER PRIMARY KEY,\n name TEXT,\n age INTEGER,\n metadata TEXT,\n settings TEXT,\n last_login TEXT,\n is_active BOOLEAN\n );\n\n INSERT INTO users (name, age, metadata, settings, is_active) VALUES\n ('Alice', 30, '{\"role\":\"admin\"}', '{\"theme\":\"dark\"}', true),\n ('Bob', 25, '{\"role\":\"user\"}', '{\"theme\":\"light\"}', true);\n `)\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\tdescribe(\"SQL Generation\", () => {\n\t\ttest(\":set - generates SQL with object value types\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tname: string\n\t\t\t\tage: number\n\t\t\t\tis_active: boolean\n\t\t\t}\n\n\t\t\tconst stmt = db.sql<UserUpdate>`\n UPDATE users ${\n\t\t\t\t\t{\n\t\t\t\t\t\tset: {\n\t\t\t\t\t\t\tname: \"$name\",\n\t\t\t\t\t\t\tage: \"$age\",\n\t\t\t\t\t\t\tis_active: \"$is_active\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t} as any\n\t\t\t\t}\n `\n\n\t\t\tassert.equal(\n\t\t\t\tstmt\n\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t.sourceSQL({ name: \"Carol\", age: 28, is_active: true, id: 1 } as any)\n\t\t\t\t\t.trim(),\n\t\t\t\t\"UPDATE users\\nSET name = $name,\\n age = $age,\\n is_active = $is_active\\nWHERE id = $id\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\":set - generates SQL with JSON operators\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tname: string\n\t\t\t\tmetadata: { role: string }\n\t\t\t\tsettings: { theme: string }\n\t\t\t}\n\n\t\t\tconst stmt = db.sql<UserUpdate>`\n UPDATE users ${\n\t\t\t\t\t{\n\t\t\t\t\t\tset: {\n\t\t\t\t\t\t\tname: \"$name\",\n\t\t\t\t\t\t\tmetadata: \"$metadata->json\",\n\t\t\t\t\t\t\tsettings: \"$settings->json\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t} as any\n\t\t\t\t}\n `\n\n\t\t\tassert.equal(\n\t\t\t\tstmt\n\t\t\t\t\t.sourceSQL({\n\t\t\t\t\t\tname: \"Carol\",\n\t\t\t\t\t\tmetadata: { role: \"admin\" },\n\t\t\t\t\t\tsettings: { theme: \"dark\" },\n\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t} as any)\n\t\t\t\t\t.trim(),\n\t\t\t\t\"UPDATE users\\nSET name = $name,\\n metadata = jsonb($metadata),\\n settings = jsonb($settings)\\nWHERE id = $id\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\":set - generates SQL with raw values\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tname: string\n\t\t\t\tlast_login: string\n\t\t\t}\n\n\t\t\tconst stmt = db.sql<UserUpdate>`\n UPDATE users ${\n\t\t\t\t\t{\n\t\t\t\t\t\tset: {\n\t\t\t\t\t\t\tname: \"$name\",\n\t\t\t\t\t\t\tlast_login: raw`CURRENT_TIMESTAMP`,\n\t\t\t\t\t\t},\n\t\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t} as any\n\t\t\t\t}\n `\n\n\t\t\tassert.equal(\n\t\t\t\tstmt\n\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t.sourceSQL({ name: \"Carol\", id: 1 } as any)\n\t\t\t\t\t.trim(),\n\t\t\t\t\"UPDATE users\\nSET name = $name,\\n last_login = CURRENT_TIMESTAMP\\nWHERE id = $id\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\":set - generates SQL with star notation\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tname: string\n\t\t\t\tage: number\n\t\t\t}\n\n\t\t\tconst stmt = db.sql<UserUpdate>`\n UPDATE users ${\n\t\t\t\t\t{\n\t\t\t\t\t\tset: \"*\",\n\t\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t} as any\n\t\t\t\t}\n `\n\n\t\t\tassert.equal(\n\t\t\t\tstmt\n\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t.sourceSQL({ name: \"Carol\", age: 28, id: 1 } as any)\n\t\t\t\t\t.trim(),\n\t\t\t\t\"UPDATE users\\nSET name = $name,\\n age = $age,\\n id = $id\\nWHERE id = $id\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\":set - generates SQL with jsonColumns array\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tname: string\n\t\t\t\tmetadata: { role: string }\n\t\t\t\tsettings: { theme: string }\n\t\t\t}\n\n\t\t\tconst stmt = db.sql<UserUpdate>`\n UPDATE users ${\n\t\t\t\t\t{\n\t\t\t\t\t\tset: [\"*\", { jsonColumns: [\"metadata\", \"settings\"] }],\n\t\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t} as any\n\t\t\t\t}\n `\n\n\t\t\tassert.equal(\n\t\t\t\tstmt\n\t\t\t\t\t.sourceSQL({\n\t\t\t\t\t\tname: \"Carol\",\n\t\t\t\t\t\tmetadata: { role: \"admin\" },\n\t\t\t\t\t\tsettings: { theme: \"dark\" },\n\t\t\t\t\t\tid: 1,\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t} as any)\n\t\t\t\t\t.trim(),\n\t\t\t\t\"UPDATE users\\nSET name = $name,\\n metadata = jsonb($metadata),\\n settings = jsonb($settings),\\n id = $id\\nWHERE id = $id\"\n\t\t\t)\n\t\t})\n\t})\n\n\tdescribe(\":set - Database Operations\", () => {\n\t\ttest(\":set - updates with basic value types\", () => {\n\t\t\ttype UserUpdate = Partial<{\n\t\t\t\tname: string\n\t\t\t\tage: number\n\t\t\t\tid: number\n\t\t\t}>\n\n\t\t\tconst update = db.sql<UserUpdate>`\n UPDATE users ${{\n\t\t\t\t\tset: {\n\t\t\t\t\t\tname: \"$name\",\n\t\t\t\t\t\tage: \"$age\",\n\t\t\t\t\t},\n\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\treturning: \"*\",\n\t\t\t\t}};\n `\n\n\t\t\tconst result = update.get({\n\t\t\t\tname: \"Carol\",\n\t\t\t\tage: 35,\n\t\t\t\tid: 1,\n\t\t\t})\n\n\t\t\tassert.equal(result?.name, \"Carol\")\n\t\t\tassert.equal(result?.age, 35)\n\t\t})\n\n\t\ttest(\":set - updates with JSON columns\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tmetadata: { role: string }\n\t\t\t\tsettings: { theme: string }\n\t\t\t\tid: number\n\t\t\t}\n\n\t\t\tconst update = db.sql<UserUpdate>`\n UPDATE users ${{\n\t\t\t\t\tset: {\n\t\t\t\t\t\tmetadata: \"$metadata->json\",\n\t\t\t\t\t\tsettings: \"$settings->json\",\n\t\t\t\t\t},\n\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\treturning: [\"*\", { jsonColumns: [\"metadata\", \"settings\"] }],\n\t\t\t\t}}\n `\n\n\t\t\tconsole.log(\n\t\t\t\t`SQL: ${update.sourceSQL({\n\t\t\t\t\tmetadata: { role: \"superadmin\" },\n\t\t\t\t\tsettings: { theme: \"system\" },\n\t\t\t\t\tid: 1,\n\t\t\t\t})}`\n\t\t\t)\n\n\t\t\tconst result = update.get({\n\t\t\t\tmetadata: { role: \"superadmin\" },\n\t\t\t\tsettings: { theme: \"system\" },\n\t\t\t\tid: 1,\n\t\t\t})\n\n\t\t\tassert.deepEqual(result?.metadata, { role: \"superadmin\" })\n\t\t\tassert.deepEqual(result?.settings, { theme: \"system\" })\n\t\t})\n\n\t\ttest(\":set - updates with raw values\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tname: string\n\t\t\t\tid: number\n\t\t\t\tlast_login?: string\n\t\t\t}\n\n\t\t\tconst update = db.sql<UserUpdate>`\n UPDATE users ${\n\t\t\t\t\t{\n\t\t\t\t\t\tset: {\n\t\t\t\t\t\t\tname: \"$name\",\n\t\t\t\t\t\t\tlast_login: raw`CURRENT_TIMESTAMP`,\n\t\t\t\t\t\t},\n\t\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\t\treturning: [\"name\", \"last_login\"],\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t} as any\n\t\t\t\t}\n `\n\n\t\t\tconst result = update.get({\n\t\t\t\tname: \"Carol\",\n\t\t\t\tid: 1,\n\t\t\t})\n\n\t\t\tassert.equal(result?.name, \"Carol\")\n\t\t\tassert.ok(result?.last_login, \"last_login should be set\")\n\t\t})\n\n\t\ttest(\":set - updates using star notation\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tname: string\n\t\t\t\tage: number\n\t\t\t\tid: number\n\t\t\t}\n\n\t\t\tconst update = db.sql<UserUpdate>`\n UPDATE users ${{\n\t\t\t\t\tset: \"*\",\n\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\treturning: \"*\",\n\t\t\t\t}}\n `\n\n\t\t\tconst result = update.get({\n\t\t\t\tname: \"Carol\",\n\t\t\t\tage: 35,\n\t\t\t\tid: 1,\n\t\t\t})\n\n\t\t\tassert.equal(result?.name, \"Carol\")\n\t\t\tassert.equal(result?.age, 35)\n\t\t})\n\n\t\ttest(\":set - updates using jsonColumns\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tname: string\n\t\t\t\tmetadata: { role: string }\n\t\t\t\tsettings: { theme: string }\n\t\t\t\tid: number\n\t\t\t}\n\n\t\t\tconst update = db.sql<UserUpdate>`\n UPDATE users ${{\n\t\t\t\t\tset: [\"*\", { jsonColumns: [\"metadata\", \"settings\"] }],\n\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\treturning: [\"*\", { jsonColumns: [\"metadata\", \"settings\"] }],\n\t\t\t\t}}\n `\n\n\t\t\tconst result = update.get({\n\t\t\t\tname: \"Carol\",\n\t\t\t\tmetadata: { role: \"superadmin\" },\n\t\t\t\tsettings: { theme: \"system\" },\n\t\t\t\tid: 1,\n\t\t\t})\n\n\t\t\tassert.equal(result?.name, \"Carol\")\n\t\t\tassert.deepEqual(result?.metadata, { role: \"superadmin\" })\n\t\t\tassert.deepEqual(result?.settings, { theme: \"system\" })\n\t\t})\n\n\t\ttest(\":set - handles multiple updates\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tis_active: number // Change to number since SQLite uses 0/1\n\t\t\t\tid1: number\n\t\t\t\tid2: number\n\t\t\t}\n\n\t\t\tconst update = db.sql<UserUpdate>`\n UPDATE users ${{\n\t\t\tset: {\n\t\t\t\tis_active: \"$is_active\",\n\t\t\t},\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\twhere: [\"id = $id1\", \"OR\", \"id = $id2\"] as any,\n\t\t\treturning: \"*\",\n\t\t}}\n `\n\n\t\t\tconst results = update.all({\n\t\t\t\tis_active: 0, // Use 0 instead of false\n\t\t\t\tid1: 1,\n\t\t\t\tid2: 2,\n\t\t\t})\n\n\t\t\tassert.equal(results.length, 2)\n\t\t\tassert.ok(results.every(r => r.is_active === 0)) // Check for 0 instead of false\n\t\t})\n\n\t\ttest(\":set - verifies no update on no matching rows\", () => {\n\t\t\ttype UserUpdate = {\n\t\t\t\tname: string\n\t\t\t\tid: number\n\t\t\t}\n\n\t\t\tconst update = db.sql<UserUpdate>`\n UPDATE users ${{\n\t\t\t\t\tset: {\n\t\t\t\t\t\tname: \"$name\",\n\t\t\t\t\t},\n\t\t\t\t\twhere: \"id = $id\",\n\t\t\t\t\treturning: \"*\",\n\t\t\t\t}}\n `\n\n\t\t\tconst result = update.run({\n\t\t\t\tname: \"Carol\",\n\t\t\t\tid: 999, // Non-existent ID\n\t\t\t})\n\n\t\t\tassert.equal(result.changes, 0)\n\t\t})\n\t})\n})\n",
"metadata": {
"size": 8607,
"modified": 1738725761195.4517,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"set.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { validateSetOptions } from \"#context\"\nimport { NodeSqliteError, SqlitePrimaryResultCode } from \"#errors.js\"\nimport { isRawValue, type SetOptions, type DataRow } from \"#types\"\n\n// In values.ts\nexport function buildSetStatement<P extends DataRow>(\n\tset: SetOptions<P>,\n\tparams: P\n): { sql: string; parameterOperators: string[] } {\n\t// Validate the set options first\n\tconst errors = validateSetOptions(set)\n\tif (errors.length > 0) {\n\t\tthrow new NodeSqliteError(\n\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\"Invalid SET options\",\n\t\t\terrors.map(e => e.message).join(\"\\n\"),\n\t\t\tundefined\n\t\t)\n\t}\n\n\t// Handle star format\n\tif (set === \"*\") {\n\t\tconst columns = Object.keys(params)\n\t\tconst paramOps = columns.map(k => `$${k}`)\n\t\treturn {\n\t\t\tsql: `SET ${columns.map((col, i) => `${col} = ${paramOps[i]}`).join(\", \")}`,\n\t\t\tparameterOperators: paramOps,\n\t\t}\n\t}\n\n\t// Handle array format with jsonColumns\n\tif (Array.isArray(set)) {\n\t\tconst [, config] = set\n\t\tconst jsonColumns = new Set(config.jsonColumns)\n\t\tconst columns = Object.keys(params)\n\t\tconst paramOps = columns.map(k => `$${k}`)\n\n\t\tconst setPairs = columns.map((col, i) => {\n\t\t\tif (jsonColumns.has(col)) {\n\t\t\t\treturn `${col} = jsonb(${paramOps[i]})`\n\t\t\t}\n\t\t\treturn `${col} = ${paramOps[i]}`\n\t\t})\n\n\t\treturn {\n\t\t\tsql: `SET ${setPairs.join(\", \")}`,\n\t\t\tparameterOperators: paramOps,\n\t\t}\n\t}\n\n\t// Handle object format\n\tconst entries = Object.entries(set)\n\tconst paramOps: string[] = []\n\tconst setPairs: string[] = []\n\n\tfor (const [col, value] of entries) {\n\t\tif (isRawValue(value)) {\n\t\t\tsetPairs.push(`${col} = ${value.value}`)\n\t\t\tcontinue\n\t\t}\n\n\t\tif (typeof value === \"string\") {\n\t\t\t// Must be a ValueType (ParameterOperator or ToJson) at this point\n\t\t\t// since validation passed\n\t\t\tif (value.endsWith(\"->json\")) {\n\t\t\t\t// Handle ToJson case\n\t\t\t\tconst paramName = value.slice(1, -6)\n\t\t\t\tparamOps.push(`$${paramName}`)\n\t\t\t\tsetPairs.push(`${col} = jsonb($${paramName})`)\n\t\t\t} else {\n\t\t\t\t// Handle ParameterOperator case\n\t\t\t\tconst paramName = value.slice(1)\n\t\t\t\tparamOps.push(`$${paramName}`)\n\t\t\t\tsetPairs.push(`${col} = $${paramName}`)\n\t\t\t}\n\t\t}\n\t\t// No else needed - validation would have caught invalid types\n\t}\n\n\treturn {\n\t\tsql: `SET ${setPairs.join(\", \")}`,\n\t\tparameterOperators: paramOps,\n\t}\n}\n",
"metadata": {
"size": 2422,
"modified": 1738725761195.5288,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"sql.compose.test.ts": {
"content": "import { test, beforeEach, afterEach, describe } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { DB } from \"#database\"\nimport { NodeSqliteError } from \"#errors\"\nimport { raw } from \"#sql.js\"\n\nlet db: DB\n\nbeforeEach(() => {\n\tdb = new DB({ location: \":memory:\" })\n\tdb.exec(`\n CREATE TABLE users (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n email TEXT UNIQUE,\n age INTEGER,\n settings JSON,\n metadata JSON,\n active BOOLEAN DEFAULT true,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP\n );\n\n CREATE TABLE posts (\n id INTEGER PRIMARY KEY,\n user_id INTEGER,\n title TEXT NOT NULL,\n metadata JSON,\n FOREIGN KEY(user_id) REFERENCES users(id)\n );\n `)\n})\n\nafterEach(() => {\n\tdb.close()\n})\n\ndescribe(\"Statement Composition\", () => {\n\ttest(\"composes basic SELECT with WHERE\", () => {\n\t\tconst select = db.sql`SELECT * FROM users`\n\t\tconst where = db.sql`WHERE id = ${\"$id\"}`\n\t\tconst query = db.sql`${select} ${where}`\n\n\t\tassert.equal(\n\t\t\tquery.sourceSQL({ id: 1 }).trim(),\n\t\t\t\"SELECT *\\nFROM users\\nWHERE id = $id\"\n\t\t)\n\t})\n\n\ttest(\"composes SELECT with columns and WHERE\", () => {\n\t\ttype User = {\n\t\t\tid: number\n\t\t\tname: string\n\t\t\temail: string\n\t\t}\n\n\t\tconst select = db.sql<Partial<User>>`\n SELECT ${{ columns: [\"id\", \"name\", \"email\"] }}\n FROM users\n `\n\t\tconst where = db.sql<{ id: number }>`WHERE id = ${\"$id\"}`\n\t\tconst query = db.sql`${select} ${where}`\n\n\t\tassert.equal(\n\t\t\tquery.sourceSQL({ id: 1 }).trim(),\n\t\t\t\"SELECT id,\\n name,\\n email\\nFROM users\\nWHERE id = $id\"\n\t\t)\n\t})\n\n\ttest(\"composes multiple statements with JSON operations\", () => {\n\t\ttype UserWithJSON = {\n\t\t\tid: number\n\t\t\tname: string\n\t\t\tsettings: { theme: string }\n\t\t}\n\n\t\tconst select = db.sql<Partial<UserWithJSON>>`\n SELECT ${{ columns: [\"id\", \"name\", \"settings<-json\"] }}\n FROM users\n `\n\t\tconst where = db.sql<{ id: number }>`WHERE id = ${\"$id\"}`\n\t\tconst query = db.sql`${select} ${where}`\n\n\t\tassert.equal(\n\t\t\tquery.sourceSQL({ id: 1 }).trim(),\n\t\t\t\"SELECT id,\\n name,\\n json_extract(settings, '$') AS settings\\nFROM users\\nWHERE id = $id\"\n\t\t)\n\t})\n\n\ttest(\"composes statements with contexts\", () => {\n\t\ttype QueryParams = {\n\t\t\tminAge: number\n\t\t\tactive: boolean\n\t\t}\n\n\t\tconst select = db.sql`SELECT * FROM users`\n\t\tconst conditions = db.sql<QueryParams>`${\n\t\t\t{\n\t\t\t\twhere: [\"age > $minAge\", \"AND\", \"active = $active\"],\n\t\t\t\torderBy: { name: \"ASC\" },\n\t\t\t\tlimit: 10,\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t} as any\n\t\t}`\n\t\tconst query = db.sql`${select} ${conditions}`\n\n\t\tassert.equal(\n\t\t\tquery.sourceSQL({ minAge: 21, active: true }).trim(),\n\t\t\t\"SELECT *\\nFROM users\\nWHERE age > $minAge\\n AND active = $active\\nORDER BY name ASC\\nLIMIT 10\"\n\t\t)\n\t})\n\n\ttest(\"composes INSERT with VALUES\", () => {\n\t\ttype NewUser = {\n\t\t\tname: string\n\t\t\temail: string\n\t\t\tmetadata: { tags: string[] }\n\t\t}\n\n\t\tconst insert = db.sql`INSERT INTO users`\n\t\tconst values = db.sql<NewUser>`${{\n\t\t\tvalues: [\"$name\", \"$email\", \"$metadata->json\"],\n\t\t}}`\n\t\tconst query = db.sql`${insert} ${values}`\n\n\t\tassert.equal(\n\t\t\tquery\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tname: \"Test User\",\n\t\t\t\t\temail: \"test@example.com\",\n\t\t\t\t\tmetadata: { tags: [\"new\"] },\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"INSERT INTO users (name, email, metadata)\\nVALUES ($name, $email, jsonb($metadata))\"\n\t\t)\n\t})\n\n\ttest(\"composes complex CTE\", () => {\n\t\ttype FilterParams = {\n\t\t\tminAge: number\n\t\t\tactive: boolean\n\t\t}\n\n\t\t// Create base CTE query in one statement to avoid syntax issues\n\t\tconst cteQuery = db.sql<FilterParams>`\n WITH active_users AS (\n SELECT ${\n\t\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t\t\t{ columns: [\"id\", \"name\", \"metadata<-json\"] } as any\n\t\t\t\t\t\t}\n FROM users\n ${\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\twhere: [\"age > $minAge\", \"AND\", \"active = $active\"],\n\t\t\t\t\t\t\t\tlimit: 100,\n\t\t\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t\t\t} as any\n\t\t\t\t\t\t}\n )\n SELECT ${\n\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t{ columns: [\"id\", \"name\"] } as any\n\t\t\t\t}\n FROM active_users\n ${\n\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t{ orderBy: { name: \"ASC\" } } as any\n\t\t\t\t}\n `\n\n\t\tassert.equal(\n\t\t\tcteQuery.sourceSQL({ minAge: 21, active: true }).trim(),\n\t\t\t\"WITH active_users AS (\\n SELECT id,\\n name,\\n json_extract(metadata, '$') AS metadata\\n FROM users\\n WHERE age > $minAge\\n AND active = $active\\n LIMIT 100\\n)\\nSELECT id,\\n name\\nFROM active_users\\nORDER BY name ASC\"\n\t\t)\n\t})\n\ttest(\"handles parameter type checking across composed statements\", () => {\n\t\ttype UserParams = {\n\t\t\tname: string\n\t\t\tage: number\n\t\t}\n\n\t\tconst select = db.sql<UserParams>`\n SELECT * FROM users\n WHERE name = ${\"$name\"}\n `\n\t\t// This should type error if uncommented:\n\t\t// const invalidWhere = db.sql<{ wrongParam: string }>`AND age = ${\"$wrongParam\"}`\n\n\t\tconst validWhere = db.sql<UserParams>`AND age = ${\"$age\"}`\n\t\tconst query = db.sql`${select} ${validWhere}`\n\n\t\tassert.equal(\n\t\t\tquery.sourceSQL({ name: \"Test\", age: 25 }).trim(),\n\t\t\t\"SELECT *\\nFROM users\\nWHERE name = $name\\n AND age = $age\"\n\t\t)\n\t})\n\n\ttest(\"preserves parameter scope in nested compositions\", () => {\n\t\ttype BaseParams = { id: number }\n\t\ttype ExtendedParams = BaseParams & { email: string }\n\n\t\tconst base = db.sql<BaseParams>`SELECT * FROM users WHERE id = ${\"$id\"}`\n\t\tconst extended = db.sql<ExtendedParams>`${base} AND email = ${\"$email\"}`\n\t\tconst final = db.sql`${extended} ORDER BY id`\n\n\t\tassert.equal(\n\t\t\tfinal.sourceSQL({ id: 1, email: \"test@example.com\" }).trim(),\n\t\t\t\"SELECT *\\nFROM users\\nWHERE id = $id\\n AND email = $email\\nORDER BY id\"\n\t\t)\n\t})\n\n\ttest(\"validates contexts in composed statements\", () => {\n\t\tconst select = db.sql`SELECT * FROM users`\n\t\tassert.throws(() => {\n\t\t\tconst invalidContext = db.sql`${\n\t\t\t\t{\n\t\t\t\t\twhere: \"INVALID SYNTAX\",\n\t\t\t\t\torderBy: { nonExistentColumn: \"ASC\" },\n\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t} as any\n\t\t\t}`\n\t\t\tconst query = db.sql`${select} ${invalidContext}`\n\t\t\tquery.sourceSQL()\n\t\t}, NodeSqliteError)\n\t})\n\n\ttest(\"composes raw SQL literals correctly\", () => {\n\t\tconst tableName = \"users\"\n\t\tconst select = db.sql`SELECT * FROM ${raw`${tableName}`}`\n\t\tconst where = db.sql<{ age: number }>`WHERE age > ${\"$age\"}`\n\t\tconst query = db.sql`${select} ${where}`\n\n\t\tassert.equal(\n\t\t\tquery.sourceSQL({ age: 21 }).trim(),\n\t\t\t\"SELECT *\\nFROM users\\nWHERE age > $age\"\n\t\t)\n\t})\n\n\ttest(\"executes composed queries correctly\", () => {\n\t\t// First insert some test data\n\t\tconst insertUser = db.sql<{ name: string; age: number }>`\n INSERT INTO users (name, age) VALUES (${\"$name\"}, ${\"$age\"})\n `\n\t\tinsertUser.run({ name: \"Test User\", age: 25 })\n\n\t\t// Now test the composed query\n\t\tconst select = db.sql`SELECT * FROM users`\n\t\tconst where = db.sql<{ age: number }>`WHERE age = ${\"$age\"}`\n\t\tconst query = db.sql`${select} ${where}`\n\n\t\tconst result = query.get<{ name: string; age: number }>({ age: 25 })\n\t\tassert.equal(result?.name, \"Test User\")\n\t\tassert.equal(result?.age, 25)\n\t})\n})\n\ndescribe(\"Complex Query Integration\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({ location: \":memory:\" })\n\t\tdb.exec(`\n CREATE TABLE executions (\n id INTEGER PRIMARY KEY,\n createdAt TEXT DEFAULT CURRENT_TIMESTAMP,\n updatedAt TEXT DEFAULT CURRENT_TIMESTAMP,\n actionType TEXT NOT NULL,\n triggeredBy TEXT NOT NULL,\n data TEXT NOT NULL,\n loggedAlertId INTEGER,\n status TEXT DEFAULT 'pending'\n );\n\n INSERT INTO executions (actionType, triggeredBy, data, status) VALUES\n ('EMAIL', 'system', '{\"actionType\":\"EMAIL\",\"triggeredBy\":\"system\",\"status\":\"completed\",\"details\":{\"message\":\"Test 1\"}}', 'completed'),\n ('SMS', 'user123', '{\"actionType\":\"SMS\",\"triggeredBy\":\"user123\",\"status\":\"pending\",\"details\":{\"message\":\"Test 2\"}}', 'pending'),\n ('EMAIL', 'system', '{\"actionType\":\"EMAIL\",\"triggeredBy\":\"system\",\"status\":\"completed\",\"details\":{\"message\":\"Test 3\"}}', 'completed');\n `)\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\ttest(\"composes filtered query with json columns\", () => {\n\t\tconst baseSelect = db.sql`\n SELECT ${{ columns: [\"id\", \"actionType\", \"data<-json\", \"status\"] }}\n FROM executions\n `\n\t\tconst whereClause = db.sql`WHERE status = ${\"$status\"}`\n\t\tconst query = db.sql`${baseSelect} ${whereClause}`\n\n\t\tconst results = query.all({ status: \"completed\" })\n\t\tassert.equal(results.length, 2)\n\t\tassert.equal(results[0].data.status, \"completed\")\n\t})\n\n\ttest(\"composes query with filter and sort\", () => {\n\t\tconst baseSelect = db.sql`\n SELECT ${{ columns: [\"id\", \"actionType\", \"data<-json\"] }}\n FROM executions\n `\n\t\tconst whereClause = db.sql`WHERE actionType = ${\"$type\"}`\n\t\tconst orderClause = db.sql`ORDER BY ${raw`createdAt`} DESC`\n\t\tconst query = db.sql`${baseSelect} ${whereClause} ${orderClause}`\n\n\t\tconst results = query.all({ type: \"EMAIL\" })\n\t\tassert.equal(results.length, 2)\n\t\tassert.equal(results[0].actionType, \"EMAIL\")\n\t})\n\n\ttest(\"composes query with pagination\", () => {\n\t\tconst baseSelect = db.sql`\n SELECT ${{ columns: [\"id\", \"actionType\"] }}\n FROM executions\n `\n\t\tconst limitClause = db.sql`${{ limit: 1, offset: 1 }}`\n\t\tconst query = db.sql`${baseSelect} ${limitClause}`\n\n\t\tconst results = query.all()\n\t\tassert.equal(results.length, 1)\n\t})\n\n\ttest(\"composes complex query with multiple conditions\", () => {\n\t\tconst baseSelect = db.sql`\n SELECT ${{ columns: [\"id\", \"actionType\", \"data<-json\"] }}\n FROM executions\n `\n\t\tconst whereClause = db.sql`\n WHERE actionType = ${\"$type\"}\n AND status = ${\"$status\"}\n `\n\t\tconst orderAndLimit = db.sql`\n ORDER BY ${raw`createdAt`} DESC\n ${{ limit: 1 }}\n `\n\t\tconst query = db.sql`${baseSelect} ${whereClause} ${orderAndLimit}`\n\n\t\tconst results = query.all({ type: \"EMAIL\", status: \"completed\" })\n\t\tassert.equal(results.length, 1)\n\t\tassert.equal(results[0].actionType, \"EMAIL\")\n\t\tassert.equal(results[0].data.status, \"completed\")\n\t})\n\n\ttest(\"composes mutation query\", () => {\n\t\tconst updateStmt = db.sql`UPDATE executions`\n\t\tconst setClause = db.sql`\n SET status = ${\"$newStatus\"},\n data = ${\"$data->json\"}\n `\n\t\tconst whereClause = db.sql`WHERE id = ${\"$id\"}`\n\t\tconst query = db.sql`${updateStmt} ${setClause} ${whereClause}`\n\n\t\tconst result = query.run({\n\t\t\tid: 1,\n\t\t\tnewStatus: \"failed\",\n\t\t\tdata: {\n\t\t\t\tactionType: \"EMAIL\",\n\t\t\t\ttriggeredBy: \"system\",\n\t\t\t\tstatus: \"failed\",\n\t\t\t\tdetails: { message: \"Updated\" },\n\t\t\t},\n\t\t})\n\t\tassert.equal(result.changes, 1)\n\t})\n})\n",
"metadata": {
"size": 10590,
"modified": 1738772829858.636,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"sql.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { test, describe, beforeEach, afterEach } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { DB } from \"#database\"\n\nlet db: DB\n\nbeforeEach(() => {\n\tdb = new DB({ location: \":memory:\" })\n\n\tdb.exec(`\n CREATE TABLE test_table (\n id INTEGER PRIMARY KEY,\n name TEXT,\n age INTEGER,\n email TEXT UNIQUE,\n metadata TEXT,\n settings TEXT,\n created_at TEXT DEFAULT CURRENT_TIMESTAMP\n )\n `)\n})\n\nafterEach(() => {\n\tdb.close()\n})\n\ndescribe(\"SQL Context Generation\", () => {\n\ttest(\"combines SELECT with WHERE, ORDER BY, and LIMIT\", () => {\n\t\tconst stmt = db.sql<{ minAge: number }>`\n SELECT * FROM test_table\n ${\n\t\t\t{\n\t\t\t\twhere: \"age > $minAge\",\n\t\t\t\torderBy: { name: \"ASC\" },\n\t\t\t\tlimit: 10,\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t} as any\n\t\t}\n `\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tminAge: 18,\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"SELECT *\\nFROM test_table\\nWHERE age > $minAge\\nORDER BY name ASC\\nLIMIT 10\"\n\t\t)\n\t})\n\n\ttest(\"combines INSERT with VALUES and RETURNING\", () => {\n\t\ttype TestRow = { name: string; age: number; metadata: object }\n\n\t\tconst stmt = db.sql<TestRow>`\n INSERT INTO test_table\n ${{\n\t\t\tvalues: [\"$name\", \"$age\", \"$metadata->json\"],\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\treturning: [\"id\", \"created_at\"] as any,\n\t\t}}\n `\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tname: \"test\",\n\t\t\t\t\tage: 25,\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\ttags: [\"test\"],\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"INSERT INTO test_table (name, age, metadata)\\nVALUES ($name, $age, jsonb($metadata))\\nRETURNING id,\\n created_at\"\n\t\t)\n\t})\n\n\ttest(\"combines UPDATE with SET, WHERE and RETURNING\", () => {\n\t\ttype UpdateRow = { id: number; name: string; metadata: object }\n\n\t\tconst stmt = db.sql<UpdateRow>`\n UPDATE test_table\n ${{\n\t\t\tset: {\n\t\t\t\tname: \"$name\",\n\t\t\t\tmetadata: \"$metadata->json\",\n\t\t\t},\n\t\t\twhere: \"id = $id\",\n\t\t\treturning: \"*\",\n\t\t}}\n `\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tid: 1,\n\t\t\t\t\tname: \"updated\",\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\tupdated: true,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"UPDATE test_table\\nSET name = $name,\\n metadata = jsonb($metadata)\\nWHERE id = $id\\nRETURNING *\"\n\t\t)\n\t})\n\n\ttest(\"combines complex WHERE conditions with ORDER BY and LIMIT/OFFSET\", () => {\n\t\ttype QueryRow = { minAge: number; pattern: string }\n\n\t\tconst stmt = db.sql<QueryRow>`\n SELECT * FROM test_table\n ${{\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\twhere: [\"age > $minAge\", \"AND\", \"name LIKE $pattern\"] as any,\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\torderBy: { age: \"DESC\", name: \"ASC\" } as any,\n\t\t\tlimit: 20,\n\t\t\toffset: 40,\n\t\t}}\n `\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tminAge: 18,\n\t\t\t\t\tpattern: \"test%\",\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"SELECT *\\nFROM test_table\\nWHERE age > $minAge\\n AND name LIKE $pattern\\nORDER BY age DESC,\\n name ASC\\nLIMIT 20 OFFSET 40\"\n\t\t)\n\t})\n\n\ttest(\"combines INSERT with complex JSON values and column constraints\", () => {\n\t\ttype InsertRow = {\n\t\t\tname: string\n\t\t\tmetadata: { tags: string[] }\n\t\t\tsettings: { theme: string }\n\t\t}\n\n\t\tconst stmt = db.sql<InsertRow>`\n INSERT INTO test_table\n ${{\n\t\t\tvalues: [\"$name\", \"$metadata->json\", \"$settings->json\"],\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\treturning: [\"id\", \"created_at\"] as any,\n\t\t}}\n `\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tname: \"test\",\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\ttags: [\"a\", \"b\"],\n\t\t\t\t\t},\n\t\t\t\t\tsettings: {\n\t\t\t\t\t\ttheme: \"dark\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"INSERT INTO test_table (name, metadata, settings)\\nVALUES ($name, jsonb($metadata), jsonb($settings))\\nRETURNING id,\\n created_at\"\n\t\t)\n\t})\n})\n\ndescribe(\"Statement Iterator\", () => {\n\tbeforeEach(() => {\n\t\t// Insert test data\n\t\tdb.exec(`\n INSERT INTO test_table (name, age, email, metadata, settings) VALUES\n ('Alice', 25, 'alice@test.com', '{\"tags\":[\"a\",\"b\"]}', '{\"theme\":\"dark\"}'),\n ('Bob', 30, 'bob@test.com', '{\"tags\":[\"c\"]}', '{\"theme\":\"light\"}'),\n ('Carol', 35, 'carol@test.com', '{\"tags\":[\"d\",\"e\",\"f\"]}', '{\"theme\":\"dark\"}')\n `)\n\t})\n\n\ttest(\"iterates over basic query results\", () => {\n\t\tconst query = db.sql`SELECT name, age FROM test_table ORDER BY age`\n\t\tconst iterator = query.iter<{ name: string; age: number }>()\n\n\t\tlet result = iterator.next()\n\t\tconst expected1 = Object.create(null)\n\t\texpected1.name = \"Alice\"\n\t\texpected1.age = 25\n\t\tassert.deepEqual(result.value, expected1)\n\t\tassert.equal(result.done, false)\n\n\t\tresult = iterator.next()\n\t\tconst expected2 = Object.create(null)\n\t\texpected2.name = \"Bob\"\n\t\texpected2.age = 30\n\t\tassert.deepEqual(result.value, expected2)\n\t\tassert.equal(result.done, false)\n\n\t\tresult = iterator.next()\n\t\tconst expected3 = Object.create(null)\n\t\texpected3.name = \"Carol\"\n\t\texpected3.age = 35\n\t\tassert.deepEqual(result.value, expected3)\n\t\tassert.equal(result.done, false)\n\n\t\tresult = iterator.next()\n\t\tassert.equal(result.done, true)\n\t\tassert.equal(result.value, undefined)\n\t})\n\n\ttest(\"handles JSON column deserialization\", () => {\n\t\tconst query = db.sql`SELECT name, json_extract(metadata, '$') as meta FROM test_table WHERE name = 'Alice'`\n\t\tconst iterator = query.iter<{ name: string; meta: { tags: string[] } }>()\n\n\t\tconst row = iterator.next().value\n\t\tassert.deepEqual(row.meta.tags, [\"a\", \"b\"])\n\t})\n\n\ttest(\"returns empty iterator for no results\", () => {\n\t\tconst query = db.sql`SELECT * FROM test_table WHERE age > 100`\n\t\tconst iterator = query.iter()\n\t\tassert.deepEqual(iterator.next(), { done: true, value: undefined })\n\t})\n\n\ttest(\"iterates with parameterized query\", () => {\n\t\tconst query = db.sql<{ minAge: number }>`\n SELECT name, age FROM test_table\n WHERE age > ${\"$minAge\"}\n ORDER BY age\n `\n\t\tconst iterator = query.iter<{ name: string; age: number }>({\n\t\t\tminAge: 27,\n\t\t})\n\n\t\tlet result = iterator.next()\n\t\tconst expected1 = Object.create(null)\n\t\texpected1.name = \"Bob\"\n\t\texpected1.age = 30\n\t\tassert.deepEqual(result.value, expected1)\n\t\tassert.equal(result.done, false)\n\n\t\tresult = iterator.next()\n\t\tconst expected2 = Object.create(null)\n\t\texpected2.name = \"Carol\"\n\t\texpected2.age = 35\n\t\tassert.deepEqual(result.value, expected2)\n\t\tassert.equal(result.done, false)\n\n\t\tresult = iterator.next()\n\t\tassert.equal(result.done, true)\n\t\tassert.equal(result.value, undefined)\n\t})\n\n\ttest(\"handles multiple JSON columns per row\", () => {\n\t\tconst query = db.sql`\n SELECT name,\n json_extract(metadata, '$') as meta,\n json_extract(settings, '$') as config\n FROM test_table\n WHERE name = 'Alice'\n `\n\t\tconst iterator = query.iter<{\n\t\t\tname: string\n\t\t\tmeta: { tags: string[] }\n\t\t\tconfig: { theme: string }\n\t\t}>()\n\n\t\tconst row = iterator.next().value\n\t\tassert.deepEqual(row, {\n\t\t\tname: \"Alice\",\n\t\t\tmeta: { tags: [\"a\", \"b\"] },\n\t\t\tconfig: { theme: \"dark\" },\n\t\t})\n\t})\n\n\ttest(\"supports for...of iteration over basic results\", () => {\n\t\tconst query = db.sql`SELECT name, age FROM test_table ORDER BY age`\n\t\tconst iterator = query.iter<{ name: string; age: number }>()\n\n\t\tconst expected = [\n\t\t\t{ name: \"Alice\", age: 25 },\n\t\t\t{ name: \"Bob\", age: 30 },\n\t\t\t{ name: \"Carol\", age: 35 },\n\t\t].map(obj => Object.assign(Object.create(null), obj))\n\n\t\tlet i = 0\n\t\tfor (const row of iterator) {\n\t\t\tassert.deepEqual(row, expected[i])\n\t\t\ti++\n\t\t}\n\t\tassert.equal(i, 3)\n\t})\n\n\ttest(\"supports for...of with parameterized query\", () => {\n\t\tconst query = db.sql<{ minAge: number }>`\n SELECT name, age FROM test_table\n WHERE age > ${\"$minAge\"}\n ORDER BY age\n `\n\t\tconst iterator = query.iter<{ name: string; age: number }>({\n\t\t\tminAge: 27,\n\t\t})\n\n\t\tconst expected = [\n\t\t\t{ name: \"Bob\", age: 30 },\n\t\t\t{ name: \"Carol\", age: 35 },\n\t\t].map(obj => Object.assign(Object.create(null), obj))\n\n\t\tlet i = 0\n\t\tfor (const row of iterator) {\n\t\t\tassert.deepEqual(row, expected[i])\n\t\t\ti++\n\t\t}\n\t\tassert.equal(i, 2)\n\t})\n\n\ttest(\"supports for...of with JSON columns\", () => {\n\t\tconst query = db.sql`\n SELECT name,\n json_extract(metadata, '$') as meta,\n json_extract(settings, '$') as config\n FROM test_table WHERE name = 'Alice'\n `\n\n\t\tlet count = 0\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\tfor (const row of query.iter<any>()) {\n\t\t\tassert.equal(row.name, \"Alice\")\n\t\t\tassert.deepEqual(row.meta.tags, [\"a\", \"b\"])\n\t\t\tassert.equal(row.config.theme, \"dark\")\n\t\t\tcount++\n\t\t}\n\t\tassert.equal(count, 1)\n\t})\n\n\ttest(\"supports for...of with empty results\", () => {\n\t\tconst query = db.sql`SELECT * FROM test_table WHERE age > 100`\n\t\tlet count = 0\n\t\tfor (const _ of query.iter()) {\n\t\t\tcount++\n\t\t}\n\t\tassert.equal(count, 0)\n\t})\n\n\ttest(\"supports for...of with spread operator\", () => {\n\t\tconst query = db.sql`SELECT name FROM test_table ORDER BY name`\n\t\tconst names = [...query.iter<{ name: string }>()].map(row => row.name)\n\t\tassert.deepEqual(names, [\"Alice\", \"Bob\", \"Carol\"])\n\t})\n})\n\ntest(\"generates rows from query\", () => {\n\tdb.exec(`\n CREATE TABLE test_generator (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n data JSON\n )\n `)\n\n\tconst insertData = db.sql<{ name: string; data: Record<string, unknown> }>`\n INSERT INTO test_generator (name, data)\n VALUES (${\"$name\"}, ${\"$data->json\"})\n `\n\n\tconst testData = [\n\t\t{ name: \"item1\", data: { value: 1, active: true } },\n\t\t{ name: \"item2\", data: { value: 2, active: false } },\n\t\t{ name: \"item3\", data: { value: 3, active: true } },\n\t]\n\n\tfor (const item of testData) {\n\t\tinsertData.run(item)\n\t}\n\n\tconst query = db.sql<Record<string, never>>`\n SELECT name, json_extract(data, '$') as data\n FROM test_generator\n ORDER BY id\n `\n\n\tconst generator = query.rows<{\n\t\tname: string\n\t\tdata: Record<string, unknown>\n\t}>()\n\n\tlet index = 0\n\tfor (const row of generator) {\n\t\tassert.equal(row.name, testData[index].name)\n\t\tassert.deepEqual(row.data, testData[index].data)\n\t\tindex++\n\t}\n\n\tassert.equal(index, testData.length)\n})\n\ntest(\"generator handles empty results\", () => {\n\tdb.exec(\"CREATE TABLE empty_table (id INTEGER PRIMARY KEY)\")\n\n\tconst query = db.sql<Record<string, never>>`SELECT * FROM empty_table`\n\tconst generator = query.rows()\n\n\tlet count = 0\n\tfor (const _ of generator) {\n\t\tcount++\n\t}\n\n\tassert.equal(count, 0)\n})\n\ntest(\"generator supports early termination\", () => {\n\tdb.exec(`\n CREATE TABLE sequence (\n id INTEGER PRIMARY KEY,\n value INTEGER\n )\n `)\n\n\t// Insert test data\n\tfor (let i = 0; i < 100; i++) {\n\t\tdb.exec(`INSERT INTO sequence (value) VALUES (${i})`)\n\t}\n\n\tconst query = db.sql<Record<string, never>>`\n SELECT * FROM sequence ORDER BY value\n `\n\n\tconst generator = query.rows<{ id: number; value: number }>({})\n\tlet count = 0\n\n\t// Only consume first 50 items\n\tfor (const row of generator) {\n\t\tassert.equal(row.value, count)\n\t\tcount++\n\t\tif (count === 50) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tassert.equal(count, 50)\n})\n\ntest(\"generator handles complex joins with JSON data\", () => {\n\t// Setup tables\n\tdb.exec(`\n CREATE TABLE users (\n id INTEGER PRIMARY KEY,\n name TEXT,\n settings JSON\n );\n\n CREATE TABLE posts (\n id INTEGER PRIMARY KEY,\n user_id INTEGER,\n content TEXT,\n metadata JSON,\n FOREIGN KEY(user_id) REFERENCES users(id)\n );\n `)\n\n\t// Insert test data\n\tconst insertUser = db.sql<{\n\t\tname: string\n\t\tsettings: Record<string, unknown>\n\t}>`\n INSERT INTO users (name, settings)\n VALUES (${\"$name\"}, ${\"$settings->json\"})\n `\n\n\tconst insertPost = db.sql<{\n\t\tuserId: number\n\t\tcontent: string\n\t\tmetadata: Record<string, unknown>\n\t}>`\n INSERT INTO posts (user_id, content, metadata)\n VALUES (${\"$userId\"}, ${\"$content\"}, ${\"$metadata->json\"})\n `\n\n\tconst users = [\n\t\t{ name: \"Alice\", settings: { theme: \"dark\", notifications: true } },\n\t\t{ name: \"Bob\", settings: { theme: \"light\", notifications: false } },\n\t]\n\n\tfor (const user of users) {\n\t\tinsertUser.run(user)\n\t}\n\n\tconst posts = [\n\t\t{ userId: 1, content: \"Post 1\", metadata: { tags: [\"a\", \"b\"], views: 10 } },\n\t\t{ userId: 1, content: \"Post 2\", metadata: { tags: [\"b\", \"c\"], views: 20 } },\n\t\t{ userId: 2, content: \"Post 3\", metadata: { tags: [\"a\", \"c\"], views: 30 } },\n\t]\n\n\tfor (const post of posts) {\n\t\tinsertPost.run(post)\n\t}\n\n\tconst query = db.sql<Record<string, never>>`\n SELECT\n u.name,\n json_extract(u.settings, '$') as user_settings,\n p.content,\n json_extract(p.metadata, '$') as post_metadata\n FROM users u\n JOIN posts p ON u.id = p.user_id\n ORDER BY p.id\n `\n\n\tconst generator = query.rows<{\n\t\tname: string\n\t\tuser_settings: Record<string, unknown>\n\t\tcontent: string\n\t\tpost_metadata: Record<string, unknown>\n\t}>({})\n\n\tlet count = 0\n\tfor (const row of generator) {\n\t\tif (count < 2) {\n\t\t\tassert.equal(row.name, \"Alice\")\n\t\t\tassert.deepEqual(row.user_settings, users[0].settings)\n\t\t} else {\n\t\t\tassert.equal(row.name, \"Bob\")\n\t\t\tassert.deepEqual(row.user_settings, users[1].settings)\n\t\t}\n\t\tassert.equal(row.content, posts[count].content)\n\t\tassert.deepEqual(row.post_metadata, posts[count].metadata)\n\t\tcount++\n\t}\n\n\tassert.equal(count, 3)\n})\n\ntest(\"generator handles dynamic query composition\", () => {\n\tdb.exec(`\n CREATE TABLE products (\n id INTEGER PRIMARY KEY,\n name TEXT,\n price REAL,\n category TEXT,\n details JSON\n )\n `)\n\n\tconst insert = db.sql<{\n\t\tname: string\n\t\tprice: number\n\t\tcategory: string\n\t\tdetails: Record<string, unknown>\n\t}>`\n INSERT INTO products (name, price, category, details)\n VALUES (${\"$name\"}, ${\"$price\"}, ${\"$category\"}, ${\"$details->json\"})\n `\n\n\tconst products = [\n\t\t{\n\t\t\tname: \"A\",\n\t\t\tprice: 10.99,\n\t\t\tcategory: \"electronics\",\n\t\t\tdetails: { stock: 5, rating: 4.5 },\n\t\t},\n\t\t{\n\t\t\tname: \"B\",\n\t\t\tprice: 20.99,\n\t\t\tcategory: \"books\",\n\t\t\tdetails: { stock: 10, rating: 4.0 },\n\t\t},\n\t\t{\n\t\t\tname: \"C\",\n\t\t\tprice: 15.99,\n\t\t\tcategory: \"electronics\",\n\t\t\tdetails: { stock: 0, rating: 4.2 },\n\t\t},\n\t]\n\tfor (const p of products) {\n\t\tinsert.run(p)\n\t}\n\n\tlet baseQuery = db.sql<Record<string, never>>`\n SELECT *, json_extract(details, '$') as details\n FROM products\n `\n\n\t// Compose with WHERE\n\tbaseQuery = baseQuery.sql`WHERE category = 'electronics'`\n\n\t// Compose with ORDER BY\n\tbaseQuery = baseQuery.sql`ORDER BY price DESC`\n\n\tconst generator = baseQuery.rows<{\n\t\tid: number\n\t\tname: string\n\t\tprice: number\n\t\tcategory: string\n\t\tdetails: Record<string, unknown>\n\t}>({})\n\n\tlet count = 0\n\tlet lastPrice = Number.POSITIVE_INFINITY\n\n\tfor (const row of generator) {\n\t\tassert.equal(row.category, \"electronics\")\n\t\tassert.ok(row.price <= lastPrice) // Check ordering\n\t\tlastPrice = row.price\n\t\tcount++\n\t}\n\n\tassert.equal(count, 2)\n})\n\ntest(\"generator handles error recovery and cleanup\", () => {\n\tdb.exec(`\n CREATE TABLE error_test (\n id INTEGER PRIMARY KEY,\n value INTEGER\n )\n `)\n\n\tfor (let i = 0; i < 5; i++) {\n\t\tdb.exec(`INSERT INTO error_test (value) VALUES (${i})`)\n\t}\n\n\tconst query = db.sql<Record<string, never>>`\n SELECT * FROM error_test ORDER BY id\n `\n\n\tconst generator = query.rows<{ id: number; value: number }>({})\n\n\tlet count = 0\n\ttry {\n\t\tfor (const row of generator) {\n\t\t\tassert.equal(row.value, count)\n\t\t\tcount++\n\t\t\tif (count === 3) {\n\t\t\t\tthrow new Error(\"Simulated error\")\n\t\t\t}\n\t\t}\n\t} catch (error) {\n\t\tassert.equal((error as Error).message, \"Simulated error\")\n\t}\n\n\t// Start a new query\n\tconst newGenerator = query.rows<{ id: number; value: number }>({})\n\tcount = 0\n\tfor (const row of newGenerator) {\n\t\tassert.equal(row.value, count)\n\t\tcount++\n\t}\n\tassert.equal(count, 5)\n})\n\ntest(\"handles JSON columns in RETURNING clause\", () => {\n\ttype TestData = {\n\t\tid?: number\n\t\tname: string\n\t\tmetadata: { tags: string[] }\n\t\tsettings: { theme: string }\n\t}\n\n\tconst stmt = db.sql<TestData>`\n INSERT INTO test_table ${{\n\t\t\tvalues: [\"$name\", \"$metadata->json\", \"$settings->json\"],\n\t\t\treturning: [\"*\", { jsonColumns: [\"metadata\", \"settings\"] }],\n\t\t}}\n `\n\n\tassert.equal(\n\t\tstmt\n\t\t\t.sourceSQL({\n\t\t\t\tname: \"test\",\n\t\t\t\tmetadata: { tags: [\"a\", \"b\"] },\n\t\t\t\tsettings: { theme: \"dark\" },\n\t\t\t})\n\t\t\t.trim(),\n\t\t`INSERT INTO test_table (name, metadata, settings)\\nVALUES ($name, jsonb($metadata), jsonb($settings))\\nRETURNING name,\\n json_extract(metadata, '$') AS metadata,\\n json_extract(settings, '$') AS settings`\n\t)\n})\n\ntest(\"handles mixed standard and JSON columns in RETURNING clause\", () => {\n\ttype TestData = {\n\t\tid: number\n\t\tname: string\n\t\tmetadata: { tags: string[] }\n\t}\n\n\tconst stmt = db.sql<TestData>`\n UPDATE test_table ${{\n\t\t\tset: {\n\t\t\t\tname: \"$name\",\n\t\t\t\tmetadata: \"$metadata->json\",\n\t\t\t},\n\t\t\twhere: \"id = $id\",\n\t\t\treturning: [\"*\", { jsonColumns: [\"metadata\"] }],\n\t\t}}\n `\n\n\tassert.equal(\n\t\tstmt\n\t\t\t.sourceSQL({\n\t\t\t\tid: 1,\n\t\t\t\tname: \"test\",\n\t\t\t\tmetadata: { tags: [\"a\", \"b\"] },\n\t\t\t})\n\t\t\t.trim(),\n\t\t\"UPDATE test_table\\nSET name = $name,\\n metadata = jsonb($metadata)\\nWHERE id = $id\\nRETURNING id,\\n name,\\n json_extract(metadata, '$') AS metadata\"\n\t)\n})\n\ntest(\"validates JSON columns configuration\", () => {\n\ttype TestData = {\n\t\tid: number\n\t\tmetadata: { tags: string[] }\n\t}\n\n\t// Invalid jsonColumns type\n\tassert.throws(\n\t\t() => {\n\t\t\tdb.sql<TestData>`\n INSERT INTO test_table ${{\n\t\t\t\t\tvalues: [\"$metadata->json\"],\n\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\treturning: [\"*\", { jsonColumns: \"not-an-array\" }] as any,\n\t\t\t\t}}\n `.get({ id: 0, metadata: { tags: [] } })\n\t\t},\n\t\t{ name: \"NodeSqliteError\" }\n\t)\n\n\t// Invalid column names\n\tassert.throws(\n\t\t() => {\n\t\t\tdb.sql<TestData>`\n INSERT INTO test_table ${{\n\t\t\t\t\tvalues: [\"$metadata->json\"],\n\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\treturning: [\"*\", { jsonColumns: [42] }] as any,\n\t\t\t\t}}\n `.get({ id: 0, metadata: { tags: [] } })\n\t\t},\n\t\t{ name: \"NodeSqliteError\" }\n\t)\n})\n",
"metadata": {
"size": 17695,
"modified": 1738725761195.7385,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"sql.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n// noinspection t\n\n// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n// noinspection t\n\nimport {\n\tbuildColsStatement,\n\tisJsonColumns,\n\tisSqlContext,\n\tvalidateContextCombination,\n\tvalidateSqlContext,\n} from \"#context\"\nimport { NodeSqliteError, SqlitePrimaryResultCode } from \"#errors\"\nimport { buildValuesStatement } from \"#values\"\nimport type {\n\tStatementResultingChanges,\n\tStatementSync,\n\tSupportedValueType,\n} from \"node:sqlite\"\nimport {\n\tisRawValue,\n\ttype SqlOptions,\n\ttype DataRow,\n\ttype RawValue,\n\ttype SqlContext,\n\ttype SqlTemplateValue,\n} from \"#types\"\nimport { buildWhereStatement } from \"#where.js\"\nimport sqlFormatter from \"@sqltools/formatter\"\nimport { buildOrderByStatement } from \"#order-by\"\nimport { buildSchema } from \"#schema\"\nimport type { Config } from \"@sqltools/formatter/lib/core/types\"\nimport stringify from \"#stringify\"\nimport { buildSetStatement } from \"#set.js\"\n\nfunction toSupportedValue(value: unknown): SupportedValueType {\n\tif (\n\t\tvalue === null ||\n\t\ttypeof value === \"string\" ||\n\t\ttypeof value === \"number\" ||\n\t\ttypeof value === \"bigint\" ||\n\t\tvalue instanceof Uint8Array\n\t) {\n\t\treturn value as SupportedValueType\n\t}\n\tif (typeof value === \"object\") {\n\t\treturn stringify(value) // Use stringify for objects\n\t}\n\treturn String(value)\n}\n\nexport const raw = (\n\tstrings: TemplateStringsArray,\n\t...values: (string | number | boolean | bigint | null)[]\n) => {\n\t// Add validation\n\tfor (const value of values) {\n\t\tif (typeof value === \"object\" && value !== null) {\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\"Invalid parameter\",\n\t\t\t\t\"Raw SQL values must be primitives (string | number | boolean | bigint | null)\",\n\t\t\t\tundefined\n\t\t\t)\n\t\t}\n\t}\n\n\treturn {\n\t\ttype: \"__x_literal__\" as const,\n\t\tvalue: String.raw(strings, ...values),\n\t} as RawValue\n}\n\nexport class Sql<P extends DataRow, RET = P> {\n\treadonly strings: readonly string[]\n\treadonly paramOperators = new Set<SqlTemplateValue<P, RET>>()\n\treadonly formatterConfig?: Readonly<Config> | false\n\n\t#contextOperators = new Set<string>()\n\n\t#params: P = {} as P\n\n\tget params(): P {\n\t\treturn this.#params\n\t}\n\n\tget contextOperators(): Set<string> {\n\t\treturn this.#contextOperators\n\t}\n\n\tconstructor({\n\t\tstrings,\n\t\tparamOperators,\n\t\tformatterConfig,\n\t}: SqlOptions<P, RET>) {\n\t\tthis.strings = strings\n\n\t\tfor (const op of paramOperators) {\n\t\t\tthis.paramOperators.add(op)\n\t\t}\n\n\t\tif (formatterConfig === false) {\n\t\t\tthis.formatterConfig = false\n\t\t} else {\n\t\t\tthis.formatterConfig = {\n\t\t\t\tindent: \" \",\n\t\t\t\treservedWordCase: \"upper\",\n\t\t\t\tlinesBetweenQueries: 1,\n\t\t\t\t...formatterConfig,\n\t\t\t\tlanguage: \"sql\",\n\t\t\t}\n\t\t}\n\t}\n\n\t#fmt(sql: string): string {\n\t\tif (this.formatterConfig) {\n\t\t\treturn sqlFormatter.format(sql, this.formatterConfig)\n\t\t}\n\t\treturn sql\n\t}\n\n\t#contextToSql(context: SqlContext<P, RET>): string {\n\t\tconst parts: string[] = []\n\n\t\tif (context.columns) {\n\t\t\tparts.push(buildColsStatement(context.columns))\n\t\t}\n\n\t\tif (context.schema) {\n\t\t\tparts.push(buildSchema(context.schema))\n\t\t\tparts[parts.length - 1] += \";\" // Add semicolon after table creation\n\t\t}\n\n\t\tif (context.values) {\n\t\t\tconst result = buildValuesStatement(context.values, this.#params)\n\t\t\tfor (const op of result.parameterOperators) {\n\t\t\t\tthis.#contextOperators.add(op)\n\t\t\t}\n\t\t\tparts.push(result.sql)\n\t\t}\n\n\t\tif (context.set) {\n\t\t\tconst result = buildSetStatement(context.set, this.#params)\n\t\t\tfor (const op of result.parameterOperators) {\n\t\t\t\tthis.#contextOperators.add(op)\n\t\t\t}\n\t\t\tconst setParts = result.sql.split(\"\\n\")\n\t\t\tif (setParts.length > 1) {\n\t\t\t\tparts.push(setParts[0])\n\t\t\t\tparts.push(...setParts.slice(1).map(p => ` ${p}`))\n\t\t\t} else {\n\t\t\t\tparts.push(result.sql)\n\t\t\t}\n\t\t}\n\t\tif (context.where) {\n\t\t\tconst result = buildWhereStatement(context.where, this.#params)\n\t\t\tparts.push(result.sql)\n\t\t\tfor (const op of result.parameterOperators) {\n\t\t\t\tthis.#contextOperators.add(op)\n\t\t\t}\n\t\t}\n\n\t\tif (context.orderBy) {\n\t\t\tparts.push(buildOrderByStatement(context.orderBy))\n\t\t}\n\n\t\tif (context.limit !== undefined) {\n\t\t\tparts.push(`LIMIT ${context.limit}`)\n\t\t\tif (context.offset !== undefined) {\n\t\t\t\tparts.push(`OFFSET ${context.offset}`)\n\t\t\t}\n\t\t} else if (context.offset !== undefined) {\n\t\t\tparts.push(\"LIMIT -1\")\n\t\t\tparts.push(`OFFSET ${context.offset}`)\n\t\t}\n\n\t\tif (context.returning) {\n\t\t\tif (context.returning === \"*\") {\n\t\t\t\tparts.push(\"RETURNING *\")\n\t\t\t} else if (Array.isArray(context.returning)) {\n\t\t\t\tif (\n\t\t\t\t\tcontext.returning.length === 2 &&\n\t\t\t\t\tcontext.returning[0] === \"*\" &&\n\t\t\t\t\tisJsonColumns(context.returning[1])\n\t\t\t\t) {\n\t\t\t\t\t// Handle JSON columns case\n\t\t\t\t\tconst config = context.returning[1]\n\t\t\t\t\tconst jsonColumns = new Set(config.jsonColumns || [])\n\t\t\t\t\tconst allColumns = new Set(Object.keys(this.#params))\n\n\t\t\t\t\tconst returningColumns = Array.from(allColumns).map(col =>\n\t\t\t\t\t\tjsonColumns.has(col) ? `json_extract(${col}, '$') as ${col}` : col\n\t\t\t\t\t)\n\t\t\t\t\tparts.push(`RETURNING ${returningColumns.join(\", \")}`)\n\t\t\t\t} else {\n\t\t\t\t\t// Handle regular column array case\n\t\t\t\t\tparts.push(`RETURNING ${context.returning.join(\", \")}`)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn parts.join(\"\\n\")\n\t}\n\n\tget sql(): string {\n\t\tlet result = this.strings[0]\n\n\t\tlet i = 0\n\t\tfor (const op of this.paramOperators) {\n\t\t\tif (isXStatementSync(op)) {\n\t\t\t\t// Handle interpolated SQL statement\n\t\t\t\tresult += op.sql.sql\n\t\t\t} else if (isSqlContext<P, RET>(op)) {\n\t\t\t\tresult += this.#contextToSql(op)\n\t\t\t} else if (isRawValue(op)) {\n\t\t\t\tresult += op.value\n\t\t\t} else if (typeof op === \"string\") {\n\t\t\t\tif (op.endsWith(\"->json\")) {\n\t\t\t\t\tconst paramName = op.split(\"->\")[0].substring(1)\n\t\t\t\t\tresult += `jsonb($${paramName})`\n\t\t\t\t} else if (op.endsWith(\"<-json\")) {\n\t\t\t\t\tconst paramName = op.split(\"<-\")[0].substring(1)\n\t\t\t\t\tresult += `json_extract(${paramName}, '$')`\n\t\t\t\t} else {\n\t\t\t\t\tresult += op\n\t\t\t\t}\n\t\t\t}\n\t\t\tresult += this.strings[i + 1]\n\t\t\ti++\n\t\t}\n\n\t\treturn result\n\t}\n\n\tget hasJsonColumns(): boolean {\n\t\tconst { sql } = this\n\t\treturn (\n\t\t\tsql.includes(\"json_extract\") ||\n\t\t\tsql.includes(\"json(\") ||\n\t\t\tsql.includes(\"jsonb(\") ||\n\t\t\tsql.includes(\"json_array\") ||\n\t\t\tsql.includes(\"json_object\") ||\n\t\t\tsql.includes(\"json_type\") ||\n\t\t\tsql.includes(\"json_valid\") ||\n\t\t\tsql.includes(\"json_patch\") ||\n\t\t\tsql.includes(\"json_group_array\") ||\n\t\t\tsql.includes(\"json_group_object\") ||\n\t\t\tsql.includes(\"json_tree\") ||\n\t\t\tsql.includes(\"json_each\") ||\n\t\t\tsql.includes(\"->\") ||\n\t\t\tsql.includes(\"->>\")\n\t\t)\n\t}\n\n\t#gatherParameterOperators() {\n\t\tconst operators = new Set<SqlTemplateValue<P, RET>>()\n\t\tfor (const op of this.paramOperators) {\n\t\t\tif (isXStatementSync(op)) {\n\t\t\t\t// need to call this to populate the context operators\n\t\t\t\top.sql.sql\n\t\t\t\tfor (const contextOp of op.sql.paramOperators) {\n\t\t\t\t\toperators.add(contextOp as SqlTemplateValue<P, RET>)\n\t\t\t\t}\n\n\t\t\t\tfor (const contextOp of op.sql.contextOperators) {\n\t\t\t\t\toperators.add(contextOp as SqlTemplateValue<P, RET>)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn operators\n\t}\n\n\t#toNamedParams(): Record<string, SupportedValueType> {\n\t\tconst namedParams: Record<string, SupportedValueType> = {}\n\t\tconst operators = new Set([\n\t\t\t...this.paramOperators,\n\t\t\t...this.#contextOperators,\n\t\t])\n\n\t\tfor (const op of this.#gatherParameterOperators()) {\n\t\t\toperators.add(op)\n\t\t}\n\n\t\t// Handle batch params\n\t\tif (Array.isArray(this.#params) || this.#params instanceof Set) {\n\t\t\tconst items = Array.isArray(this.#params)\n\t\t\t\t? this.#params\n\t\t\t\t: Array.from(this.#params)\n\t\t\tfor (let index = 0; index < items.length; index++) {\n\t\t\t\tconst row = items[index]\n\t\t\t\tfor (const [key, value] of Object.entries(row)) {\n\t\t\t\t\tconst operator = operators.has(`$${key}->json`)\n\t\t\t\t\t\t? `$${key}_${index}->json`\n\t\t\t\t\t\t: `$${key}_${index}`\n\n\t\t\t\t\tif (operator.endsWith(\"->json\")) {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\ttypeof value !== \"object\" &&\n\t\t\t\t\t\t\t!Array.isArray(value) &&\n\t\t\t\t\t\t\tvalue !== null\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\t\t\t\"Invalid parameter\",\n\t\t\t\t\t\t\t\t`Parameter '${key}' must be an object or array for JSON conversion`,\n\t\t\t\t\t\t\t\tundefined\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tnamedParams[`$${key}_${index}`] = stringify(value)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnamedParams[`$${key}_${index}`] = toSupportedValue(value)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn namedParams\n\t\t}\n\n\t\t// Handle single record params\n\t\tfor (const op of operators) {\n\t\t\tif (typeof op !== \"string\" || op.endsWith(\"<-json\")) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconst paramName = op.split(\"->\")[0].substring(1)\n\t\t\tconst value = (this.#params as P)[paramName]\n\n\t\t\tif (value === undefined) {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Missing parameter\",\n\t\t\t\t\t`Parameter '${paramName}' is undefined`,\n\t\t\t\t\tundefined\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tif (op.endsWith(\"->json\")) {\n\t\t\t\tif (\n\t\t\t\t\ttypeof value !== \"object\" &&\n\t\t\t\t\t!Array.isArray(value) &&\n\t\t\t\t\tvalue !== null\n\t\t\t\t) {\n\t\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\t\"Invalid parameter\",\n\t\t\t\t\t\t`Parameter '${paramName}' must be an object or array for JSON conversion`,\n\t\t\t\t\t\tundefined\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tnamedParams[`$${paramName}`] = stringify(value)\n\t\t\t} else {\n\t\t\t\tnamedParams[`$${paramName}`] = toSupportedValue(value)\n\t\t\t}\n\t\t}\n\t\treturn namedParams\n\t}\n\n\tprepare(params: P): {\n\t\tsql: string\n\t\tnamedParams: Record<string, SupportedValueType>\n\t\thasJsonColumns: boolean\n\t} {\n\t\tthis.#params = params\n\n\t\t// Convert Set to array just for context filtering\n\t\tconst contexts = Array.from([\n\t\t\t...this.paramOperators,\n\t\t\t...this.#gatherParameterOperators(),\n\t\t]).filter(\n\t\t\top =>\n\t\t\t\ttypeof op === \"object\" &&\n\t\t\t\t!Array.isArray(op) &&\n\t\t\t\t!isRawValue(op) &&\n\t\t\t\t!isXStatementSync(op)\n\t\t)\n\n\t\tconst validationErrors = contexts.flatMap(context =>\n\t\t\tvalidateSqlContext<P, RET>(context)\n\t\t)\n\n\t\tif (validationErrors.length > 0) {\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_CONTEXT\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_MISUSE,\n\t\t\t\t\"Invalid SQL context\",\n\t\t\t\tvalidationErrors.map(e => e.message).join(\"\\n\"),\n\t\t\t\tundefined\n\t\t\t)\n\t\t}\n\n\t\tconst combinationErrors = validateContextCombination<P, RET>(\n\t\t\tcontexts as SqlContext<P, RET>[]\n\t\t)\n\n\t\tif (combinationErrors.length > 0) {\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_CONTEXT\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_MISUSE,\n\t\t\t\t\"Invalid SQL context combination\",\n\t\t\t\tcombinationErrors.map(e => e.message).join(\"\\n\"),\n\t\t\t\tundefined\n\t\t\t)\n\t\t}\n\n\t\treturn {\n\t\t\tsql: this.#fmt(this.sql),\n\t\t\tnamedParams: this.#toNamedParams(),\n\t\t\thasJsonColumns: this.hasJsonColumns,\n\t\t}\n\t}\n}\ntype SingleRow<P extends DataRow> = {\n\t[K in keyof P]: P[K]\n}\n\n// Type for values params that can be single row or multiple rows\ntype ValuesParam<P extends DataRow> =\n\t| SingleRow<P>\n\t| SingleRow<P>[]\n\t| Set<SingleRow<P>>\n\n/**\n * Interface for prepared SQL statements with type safety and chaining support.\n * @template P Type of parameters object\n * @template RET Type of returned rows\n */\nexport interface XStatementSync<P extends DataRow, RET = unknown> {\n\t/** Execute query and return all result rows */\n\tall<R = RET>(params?: ValuesParam<P>): R[]\n\n\t/** Execute query and return an iterator over result rows */\n\titer<R = RET>(params?: ValuesParam<P>): Iterator<R> & Iterable<R>\n\n\t/** Execute query and return a generator that yields result rows */\n\trows<R = RET>(params?: ValuesParam<P>): Generator<R>\n\n\t/** Execute query and return first result row or undefined */\n\tget<R = RET>(params?: ValuesParam<P>): R | undefined\n\n\t/** Execute query and return statement result info */\n\trun(params?: ValuesParam<P>): StatementResultingChanges\n\n\t/** Get SQL with parameters expanded */\n\texpandedSQL: string | undefined\n\n\t/** Get original SQL source */\n\tsourceSQL: (params?: ValuesParam<P>) => string\n\n\treadonly sql: Sql<P, RET>\n\n\t__x_statement_sync__: true\n}\n\nfunction looksLikeJSON(value: unknown): value is string {\n\tif (typeof value !== \"string\") {\n\t\treturn false\n\t}\n\tconst data = value.trim()\n\treturn (\n\t\t// Only objects and arrays\n\t\t(data.startsWith(\"{\") && data.endsWith(\"}\")) ||\n\t\t(data.startsWith(\"[\") && data.endsWith(\"]\"))\n\t)\n}\n\n/**\n * Helper function to parse JSON columns in result rows\n */\nexport function parseJsonColumns(row: DataRow): DataRow {\n\tconst result = { ...row }\n\n\t// Handle every field in the row that's a string and try to parse it\n\tfor (const [key, value] of Object.entries(result)) {\n\t\tif (looksLikeJSON(value)) {\n\t\t\ttry {\n\t\t\t\tresult[key] = JSON.parse(value)\n\t\t\t} catch {\n\t\t\t\t// Keep original value if parsing fails\n\t\t\t}\n\t\t}\n\t}\n\treturn result\n}\ntype CreateXStatementSyncProps<P extends DataRow, R = P> = {\n\tbuild: (params: P) => {\n\t\tstmt: StatementSync\n\t\tnamedParams: Record<string, SupportedValueType>\n\t\thasJsonColumns: boolean\n\t}\n\tprepare: (sql: string) => StatementSync\n\tsql: Sql<P, R>\n}\n\nconst createErrorMessage = <P extends DataRow>(\n\terror: unknown,\n\tparams?: P | P[]\n) => {\n\tlet paramStr = \"\"\n\n\tif (Array.isArray(params) && params.length > 1) {\n\t\tconst item1 = params[0]\n\t\tconst item2 = params[1]\n\n\t\tparamStr = `{${Object.keys(item1).join(\", \")}, ...} + ${Object.keys(\n\t\t\titem2\n\t\t).join(\", \")}`\n\t} else if (typeof params === \"object\") {\n\t\tparamStr = stringify(params)\n\t} else {\n\t\tparamStr = String(params)\n\t}\n\treturn error instanceof Error\n\t\t? `${error.message}: params: ${paramStr}`\n\t\t: `${String(error)}: params: ${paramStr}`\n}\n\nexport const isXStatementSync = <P extends DataRow, R>(\n\tvalue: unknown\n): value is XStatementSync<P, R> => {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\tObject.hasOwn(value, \"get\") &&\n\t\tObject.hasOwn(value, \"all\") &&\n\t\tObject.hasOwn(value, \"run\") &&\n\t\tObject.hasOwn(value, \"iter\") &&\n\t\tObject.hasOwn(value, \"rows\") &&\n\t\tObject.hasOwn(value, \"sourceSQL\") &&\n\t\tObject.hasOwn(value, \"expandedSQL\") &&\n\t\tObject.hasOwn(value, \"__x_statement_sync__\") &&\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t(value as any).__x_statement_sync__ === true\n\t)\n}\n\n/**\n * Creates a type-safe prepared statement\n */\n// Update the factory function\nexport function createXStatementSync<P extends DataRow, RET = unknown>(\n\tprops: CreateXStatementSyncProps<P, RET>\n): XStatementSync<P, RET> {\n\tlet currentStatement: StatementSync | undefined\n\treturn {\n\t\tget sql() {\n\t\t\treturn props.sql\n\t\t},\n\n\t\tall<R = RET>(params: ValuesParam<P> = {} as P) {\n\t\t\ttry {\n\t\t\t\tconst { stmt, namedParams, hasJsonColumns } = props.build(params as P)\n\t\t\t\tcurrentStatement = stmt\n\t\t\t\tconst results = stmt.all(namedParams)\n\t\t\t\tif (!results || !results.length) {\n\t\t\t\t\t// No results case\n\t\t\t\t\treturn (Array.isArray(results) ? [] : undefined) as R\n\t\t\t\t}\n\n\t\t\t\tif (!hasJsonColumns) {\n\t\t\t\t\treturn results as R\n\t\t\t\t}\n\n\t\t\t\tif (Array.isArray(results)) {\n\t\t\t\t\t// Array results case\n\t\t\t\t\treturn results.map(row => parseJsonColumns(row as DataRow)) as R\n\t\t\t\t}\n\n\t\t\t\tif (typeof results === \"object\") {\n\t\t\t\t\t// Single object result case\n\t\t\t\t\treturn parseJsonColumns(results as DataRow) as R\n\t\t\t\t}\n\n\t\t\t\t// Primitive value case\n\t\t\t\treturn results as R\n\t\t\t} catch (error) {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_QUERY\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Query execution failed\",\n\t\t\t\t\tcreateErrorMessage(error, params)\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\n\t\tget<R = RET>(params: ValuesParam<P> = {} as P) {\n\t\t\ttry {\n\t\t\t\tconst { stmt, namedParams, hasJsonColumns } = props.build(params as P)\n\t\t\t\tcurrentStatement = stmt\n\t\t\t\tconst row = stmt.get(namedParams)\n\n\t\t\t\tif (!row) {\n\t\t\t\t\treturn undefined\n\t\t\t\t}\n\n\t\t\t\tif (!hasJsonColumns) {\n\t\t\t\t\treturn row as R\n\t\t\t\t}\n\n\t\t\t\treturn parseJsonColumns(row as DataRow) as R\n\t\t\t} catch (error) {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_QUERY\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Query execution failed\",\n\t\t\t\t\tcreateErrorMessage(error, params)\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\n\t\trun(params: ValuesParam<P> = {} as P) {\n\t\t\ttry {\n\t\t\t\tconst { stmt, namedParams } = props.build(params as P)\n\t\t\t\tcurrentStatement = stmt\n\t\t\t\treturn stmt.run(namedParams)\n\t\t\t} catch (error) {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_MUTATE\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Mutation failed\",\n\t\t\t\t\tcreateErrorMessage(error, params)\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\n\t\titer<R = RET>(params: ValuesParam<P> = {} as P): Iterable<R> & Iterator<R> {\n\t\t\ttry {\n\t\t\t\tconst { stmt, namedParams, hasJsonColumns } = props.build(params as P)\n\t\t\t\tcurrentStatement = stmt\n\t\t\t\tconst baseIterator = stmt.iterate(namedParams)\n\n\t\t\t\treturn {\n\t\t\t\t\t// Iterator protocol\n\t\t\t\t\tnext(): IteratorResult<R> {\n\t\t\t\t\t\tconst result = baseIterator.next()\n\t\t\t\t\t\tif (result.done) {\n\t\t\t\t\t\t\treturn { done: true, value: undefined }\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\tdone: false,\n\t\t\t\t\t\t\tvalue: hasJsonColumns\n\t\t\t\t\t\t\t\t? (parseJsonColumns(result.value as DataRow) as R)\n\t\t\t\t\t\t\t\t: (result.value as R),\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\n\t\t\t\t\t// Iterable protocol\n\t\t\t\t\t[Symbol.iterator]() {\n\t\t\t\t\t\treturn this\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_QUERY\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Query execution failed\",\n\t\t\t\t\tcreateErrorMessage(error, params)\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\n\t\t*rows<R = RET>(params: ValuesParam<P> = {} as P): Generator<R> {\n\t\t\ttry {\n\t\t\t\tconst { stmt, namedParams, hasJsonColumns } = props.build(params as P)\n\t\t\t\tcurrentStatement = stmt\n\t\t\t\tconst iterator = stmt.iterate(namedParams)\n\n\t\t\t\tlet result = iterator.next()\n\t\t\t\twhile (!result.done) {\n\t\t\t\t\tconst value = hasJsonColumns\n\t\t\t\t\t\t? (parseJsonColumns(result.value as DataRow) as R)\n\t\t\t\t\t\t: (result.value as R)\n\t\t\t\t\tyield value\n\t\t\t\t\tresult = iterator.next()\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_QUERY\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Query execution failed\",\n\t\t\t\t\tcreateErrorMessage(error, params)\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\n\t\tsourceSQL(params: ValuesParam<P> = {} as P) {\n\t\t\ttry {\n\t\t\t\tconst { stmt } = props.build(params as P)\n\n\t\t\t\treturn stmt.sourceSQL\n\t\t\t} catch (error) {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_QUERY\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Failed to get expanded SQL\",\n\t\t\t\t\tcreateErrorMessage(error, params)\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\n\t\tget expandedSQL() {\n\t\t\treturn currentStatement?.expandedSQL\n\t\t},\n\n\t\t__x_statement_sync__: true,\n\t}\n}\n",
"metadata": {
"size": 18220,
"modified": 1738740214239.6797,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"stringify.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\ntype Options = {\n\tdepthLimit: number\n\tedgesLimit: number\n}\n\nconst LIMIT_REPLACE_NODE = \"[...]\"\nconst CIRCULAR_REPLACE_NODE = \"[Circular]\"\n\nconst arr: [any, string | number, any, PropertyDescriptor?][] = []\nconst replacerStack: [any, string | number, any][] = []\n\nfunction defaultOptions(): Options {\n\treturn {\n\t\tdepthLimit: Number.MAX_SAFE_INTEGER,\n\t\tedgesLimit: Number.MAX_SAFE_INTEGER,\n\t}\n}\n\n// Regular stringify\nfunction stringify(\n\tobj: any,\n\treplacer?: (key: string, value: any) => any,\n\tspacer?: string | number,\n\toptions?: Options\n): string {\n\toptions = options ?? defaultOptions()\n\n\tdecirc(obj, \"\", 0, [], undefined, 0, options)\n\tlet res: string\n\ttry {\n\t\tif (replacerStack.length === 0) {\n\t\t\tres = JSON.stringify(obj, replacer, spacer)\n\t\t} else {\n\t\t\tres = JSON.stringify(obj, replaceGetterValues(replacer), spacer)\n\t\t}\n\t} catch (_) {\n\t\treturn JSON.stringify(\n\t\t\t\"[unable to serialize, circular reference is too complex to analyze]\"\n\t\t)\n\t} finally {\n\t\twhile (arr.length !== 0) {\n\t\t\tconst part = arr.pop()!\n\t\t\tif (part.length === 4) {\n\t\t\t\tObject.defineProperty(part[0], part[1], part[3]!)\n\t\t\t} else {\n\t\t\t\tpart[0][part[1]] = part[2]\n\t\t\t}\n\t\t}\n\t}\n\treturn res\n}\n\nfunction setReplace(\n\treplace: string,\n\tval: any,\n\tk: string | number,\n\tparent: any\n): void {\n\tconst propertyDescriptor = Object.getOwnPropertyDescriptor(parent, k)\n\tif (propertyDescriptor?.get !== undefined) {\n\t\tif (propertyDescriptor.configurable) {\n\t\t\tObject.defineProperty(parent, k, { value: replace })\n\t\t\tarr.push([parent, k, val, propertyDescriptor])\n\t\t} else {\n\t\t\treplacerStack.push([val, k, replace])\n\t\t}\n\t} else {\n\t\tparent[k] = replace\n\t\tarr.push([parent, k, val])\n\t}\n}\n\nfunction decirc(\n\tval: any,\n\tk: string | number,\n\tedgeIndex: number,\n\tstack: any[],\n\tparent: any,\n\tdepth: number,\n\toptions: Options\n): void {\n\tdepth += 1\n\tif (typeof val === \"object\" && val !== null) {\n\t\tfor (let i = 0; i < stack.length; i++) {\n\t\t\tif (stack[i] === val) {\n\t\t\t\tsetReplace(CIRCULAR_REPLACE_NODE, val, k, parent)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif (\n\t\t\ttypeof options.depthLimit !== \"undefined\" &&\n\t\t\tdepth > options.depthLimit\n\t\t) {\n\t\t\tsetReplace(LIMIT_REPLACE_NODE, val, k, parent)\n\t\t\treturn\n\t\t}\n\n\t\tif (\n\t\t\ttypeof options.edgesLimit !== \"undefined\" &&\n\t\t\tedgeIndex + 1 > options.edgesLimit\n\t\t) {\n\t\t\tsetReplace(LIMIT_REPLACE_NODE, val, k, parent)\n\t\t\treturn\n\t\t}\n\n\t\tstack.push(val)\n\t\t// Optimize for Arrays. Big arrays could kill the performance otherwise!\n\t\tif (Array.isArray(val)) {\n\t\t\tfor (let i = 0; i < val.length; i++) {\n\t\t\t\tdecirc(val[i], i, i, stack, val, depth, options)\n\t\t\t}\n\t\t} else {\n\t\t\tconst keys = Object.keys(val)\n\t\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\t\tconst key = keys[i]\n\t\t\t\tdecirc(val[key], key, i, stack, val, depth, options)\n\t\t\t}\n\t\t}\n\t\tstack.pop()\n\t}\n}\n\n// Stable-stringify\nfunction compareFunction(a: string, b: string): number {\n\tif (a < b) {\n\t\treturn -1\n\t}\n\tif (a > b) {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\nfunction deterministicStringify(\n\tobj: any,\n\treplacer?: (key: string, value: any) => any,\n\tspacer?: string | number,\n\toptions?: Options\n): string {\n\toptions = options ?? defaultOptions()\n\n\tconst tmp = deterministicDecirc(obj, \"\", 0, [], undefined, 0, options) || obj\n\tlet res: string\n\ttry {\n\t\tif (replacerStack.length === 0) {\n\t\t\tres = JSON.stringify(tmp, replacer, spacer)\n\t\t} else {\n\t\t\tres = JSON.stringify(tmp, replaceGetterValues(replacer), spacer)\n\t\t}\n\t} catch (_) {\n\t\treturn JSON.stringify(\n\t\t\t\"[unable to serialize, circular reference is too complex to analyze]\"\n\t\t)\n\t} finally {\n\t\twhile (arr.length !== 0) {\n\t\t\tconst part = arr.pop()!\n\t\t\tif (part.length === 4) {\n\t\t\t\tObject.defineProperty(part[0], part[1], part[3]!)\n\t\t\t} else {\n\t\t\t\tpart[0][part[1]] = part[2]\n\t\t\t}\n\t\t}\n\t}\n\treturn res\n}\n\nfunction deterministicDecirc(\n\tval: any,\n\tk: string | number,\n\tedgeIndex: number,\n\tstack: any[],\n\tparent: any,\n\tdepth: number,\n\toptions: Options\n): any {\n\tdepth += 1\n\tif (typeof val === \"object\" && val !== null) {\n\t\tfor (let i = 0; i < stack.length; i++) {\n\t\t\tif (stack[i] === val) {\n\t\t\t\tsetReplace(CIRCULAR_REPLACE_NODE, val, k, parent)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\tif (typeof val.toJSON === \"function\") {\n\t\t\t\treturn\n\t\t\t}\n\t\t} catch (_) {\n\t\t\treturn\n\t\t}\n\n\t\tif (\n\t\t\ttypeof options.depthLimit !== \"undefined\" &&\n\t\t\tdepth > options.depthLimit\n\t\t) {\n\t\t\tsetReplace(LIMIT_REPLACE_NODE, val, k, parent)\n\t\t\treturn\n\t\t}\n\n\t\tif (\n\t\t\ttypeof options.edgesLimit !== \"undefined\" &&\n\t\t\tedgeIndex + 1 > options.edgesLimit\n\t\t) {\n\t\t\tsetReplace(LIMIT_REPLACE_NODE, val, k, parent)\n\t\t\treturn\n\t\t}\n\n\t\tstack.push(val)\n\t\tif (Array.isArray(val)) {\n\t\t\tfor (let i = 0; i < val.length; i++) {\n\t\t\t\tdeterministicDecirc(val[i], i, i, stack, val, depth, options)\n\t\t\t}\n\t\t} else {\n\t\t\tconst tmp: Record<string, any> = {}\n\t\t\tconst keys = Object.keys(val).sort(compareFunction)\n\t\t\tfor (let i = 0; i < keys.length; i++) {\n\t\t\t\tconst key = keys[i]\n\t\t\t\tdeterministicDecirc(val[key], key, i, stack, val, depth, options)\n\t\t\t\ttmp[key] = val[key]\n\t\t\t}\n\t\t\tif (typeof parent !== \"undefined\") {\n\t\t\t\tarr.push([parent, k, val])\n\t\t\t\tparent[k] = tmp\n\t\t\t} else {\n\t\t\t\treturn tmp\n\t\t\t}\n\t\t}\n\t\tstack.pop()\n\t}\n}\n\nfunction replaceGetterValues(\n\treplacer?: (key: string, value: any) => any\n): (key: string, value: any) => any {\n\treplacer = replacer ?? ((_, v) => v)\n\n\treturn function (this: unknown, key: string, val: any): any {\n\t\tif (replacerStack.length > 0) {\n\t\t\tfor (let i = 0; i < replacerStack.length; i++) {\n\t\t\t\tconst part = replacerStack[i]\n\t\t\t\tif (part[1] === key && part[0] === val) {\n\t\t\t\t\tval = part[2]\n\t\t\t\t\treplacerStack.splice(i, 1)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn replacer!.call(this, key, val)\n\t}\n}\n\nstringify.stable = deterministicStringify\nstringify.stableStringify = deterministicStringify\n\nexport { stringify as default, deterministicStringify, stringify }\n",
"metadata": {
"size": 5849,
"modified": 1738725761195.977,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"types.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\nimport type { StatementSync } from \"node:sqlite\"\nimport type { Sql, XStatementSync } from \"#sql\"\nimport type { CacheStats, StatementCacheOptions } from \"#cache\"\nimport type { PragmaConfig } from \"#pragmas\"\nimport type { Logger } from \"#logger\"\nimport type { Primitive } from \"type-fest\"\nimport type { ForeignKeyDef } from \"#fk\"\n\n/**\n * Configuration options for database cleanup operations when closing the connection.\n */\nexport interface CleanupPragmas {\n\t/** Runs PRAGMA optimize to optimize the database */\n\toptimize?: boolean\n\n\t/** Runs PRAGMA shrink_memory to release memory back to the system */\n\tshrinkMemory?: boolean\n\n\t/** WAL checkpoint mode to run before closing */\n\twalCheckpoint?: \"PASSIVE\" | \"FULL\" | \"RESTART\" | \"TRUNCATE\"\n}\n\n/**\n * Configuration options for database initialization.\n */\nexport interface DBOptions {\n\t/** Database file path or \":memory:\" for in-memory database */\n\tlocation?: string | \":memory:\"\n\n\t/** Statement cache configuration - boolean to use defaults or detailed options */\n\tstatementCache?: boolean | StatementCacheOptions\n\n\t/** SQLite PRAGMA settings */\n\tpragma?: PragmaConfig\n\n\t/** Runtime environment affecting default PRAGMA settings */\n\tenvironment?: \"development\" | \"testing\" | \"production\"\n\n\t/** Custom logger implementation */\n\tlogger?: Logger\n\n\t/** SQL formatting configuration */\n\tformat?: FormatterConfig\n}\n\n/**\n * Function type for SQL template literal tag\n */\nexport type SqlFn<P extends DataRow> = (\n\tstrings: TemplateStringsArray,\n\t...params: SqlTemplateValues<P>\n) => Sql<P>\n\nexport interface IDatabase {\n\tprepareStatement(sql: string): StatementSync\n\tsql<P extends DataRow, R = unknown>(\n\t\tstrings: TemplateStringsArray,\n\t\t...params: SqlTemplateValues<P>\n\t): XStatementSync<P, R>\n\texec(sql: string): void\n\tbackup(filename: string): void\n\trestore(filename: string): void\n\tgetCacheStats(): CacheStats | undefined\n\tclearStatementCache(): void\n\tclose(pragmas?: CleanupPragmas): void\n}\n\nexport const COMPARISON_OPERATORS = [\n\t\"=\",\n\t\"!=\",\n\t\">\",\n\t\"<\",\n\t\">=\",\n\t\"<=\",\n\t\"LIKE\",\n\t\"NOT LIKE\",\n\t\"IN\",\n\t\"NOT IN\",\n\t\"IS\",\n\t\"IS NOT\",\n] as const\n\nexport const LOGICAL_OPERATORS = [\"AND\", \"OR\"] as const\n\nexport type ComparisonOperator = (typeof COMPARISON_OPERATORS)[number]\nexport type LogicalOperator = (typeof LOGICAL_OPERATORS)[number]\n\n/**\n * A row of data from a database query, or a row of data to be inserted, or a row of data used for query conditions.\n */\n// biome-ignore lint/suspicious/noExplicitAny: <explanation>\nexport type DataRow = { [key: string]: any }\n\nexport type RawValue = { type: \"__x_literal__\"; value: string }\n\n// Update isLiteral type guard\nexport function isRawValue(value: unknown): value is RawValue {\n\treturn (\n\t\ttypeof value === \"object\" &&\n\t\tvalue !== null &&\n\t\tObject.hasOwn(value, \"type\") &&\n\t\t(value as RawValue).type === \"__x_literal__\"\n\t)\n}\n\n/**\n * Represents a parameter operator that references a property of type P\n */\nexport type ParameterOperator<P extends DataRow> = `$${keyof P & string}`\n\n// Step 2: Get keys of non-primitive values\nexport type NonPrimitiveKeys<T> = {\n\t[K in keyof T]: T[K] extends Primitive ? never : K\n}[keyof T]\n\n/**\n * Represents a parameter operator that converts a property to JSON\n * Only allows non-primitive values to be converted to JSON\n */\nexport type ToJson<P extends DataRow> =\n\t`$${NonPrimitiveKeys<P> & string}${\"->json\"}`\n\n/**\n * Represents a parameter operator that parses a property from JSON\n * Only allows non-primitive values to be parsed from JSON\n */\nexport type FromJson<P extends DataRow> =\n\t`$${NonPrimitiveKeys<P> & string}${\"<-json\"}` // only supports json_extract\n\n/**\n * Union type of all possible parameter operators\n */\nexport type ParamValue<P extends DataRow> =\n\t| ParameterOperator<P>\n\t| ToJson<P>\n\t| FromJson<P>\n\nexport type SqlTemplateValue<P extends DataRow, R = P> =\n\t| ParamValue<P>\n\t| SqlContext<P, R>\n\t| RawValue\n\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t| XStatementSync<any, any>\n/**\n * Parameter values and contexts that can be used in SQL template literals\n */\nexport type SqlTemplateValues<P extends DataRow, R = P> = Array<\n\tSqlTemplateValue<P, R>\n>\n\n/**\n * Configuration for SQL formatting\n */\nexport type FormatterConfig =\n\t| false\n\t| {\n\t\t\t/** Indentation string (default: two spaces) */\n\t\t\tindent?: string\n\n\t\t\t/** Case for SQL keywords */\n\t\t\treservedWordCase?: \"upper\" | \"lower\"\n\n\t\t\t/** Lines between queries */\n\t\t\tlinesBetweenQueries?: number | \"preserve\"\n\t }\n\n/**\n * Options for initializing SQL builder\n */\nexport type SqlOptions<P extends DataRow, R = P> = {\n\tstrings: readonly string[]\n\tparamOperators: SqlTemplateValues<P, R>\n\tformatterConfig?: FormatterConfig\n\tgeneratedSql?: string\n}\n\nexport type ValueType<P extends DataRow> = ParameterOperator<P> | ToJson<P>\n\nexport type SetOptions<P extends DataRow> =\n\t| { [K in keyof P]?: ValueType<P> | RawValue }\n\t| [\"*\", { jsonColumns: (keyof P)[] }]\n\t| \"*\"\n\nexport type InsertOptions<P extends DataRow> =\n\t| ValueType<P>[]\n\t| \"*\"\n\t| [\"*\", { jsonColumns?: (keyof P)[]; batch?: boolean }]\n\nexport type ColumnOptions<P extends DataRow> =\n\t| (\n\t\t\t| keyof P\n\t\t\t| `${NonPrimitiveKeys<P> & string}${\"<-json\"}`\n\t\t\t| `${NonPrimitiveKeys<P> & string}${\"->json\"}`\n\t )[]\n\t| \"*\"\n\n// Core SQL context type\nexport type SqlContext<P extends DataRow, R = P> = Partial<{\n\tcolumns: ColumnOptions<P>\n\tvalues: InsertOptions<P>\n\tset: SetOptions<P>\n\twhere: WhereClause<P>\n\torderBy: Partial<Record<keyof P, \"ASC\" | \"DESC\">>\n\tlimit: number\n\toffset: number\n\treturning: (keyof R)[] | \"*\" | [\"*\", { jsonColumns?: (keyof R)[] }]\n\tschema: Schema<P>\n}>\n\nexport type SingleWhereCondition<P extends DataRow> =\n\t| `${keyof P & string} ${ComparisonOperator} $${keyof P & string}`\n\t| `${keyof P & string} IS NULL`\n\t| `${keyof P & string} IS NOT NULL`\n\t| [keyof P & string, ComparisonOperator, RawValue] // New tuple format for RawValue\n// Recursive type to enforce alternating condition/operator pattern\nexport type ExtendedWhereCondition<P extends DataRow> =\n\t| [SingleWhereCondition<P>, LogicalOperator, SingleWhereCondition<P>]\n\t| [\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t ]\n\t| [\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t ]\n\t| [\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t ]\n\t| [\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t ]\n\t| [\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t ]\n\t| [\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t ]\n\t| [\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t ]\n\t| [\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t ]\n\t| [\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t\t\tLogicalOperator,\n\t\t\tSingleWhereCondition<P>,\n\t ]\n\n/**\n * Represents a WHERE clause condition for SQL queries with strongly-typed column references and parameter bindings.\n * Supports single conditions and compound conditions with logical operators (AND/OR).\n * @example\n * // Single condition\n * const where: WhereClause<User> = \"age > $minAge\"\n *\n * // Compound condition\n * const where: WhereClause<User> = [\"age > $minAge\", \"AND\", \"isActive = $active\"]\n */\nexport type WhereClause<P extends DataRow> =\n\t| SingleWhereCondition<P>\n\t| ExtendedWhereCondition<P>\n\n/**\n * SQLite column constraints for table definitions\n */\nexport type BaseConstraint =\n\t| \"PRIMARY KEY\"\n\t| \"AUTOINCREMENT\"\n\t| \"UNIQUE\"\n\t| `CHECK(${string})`\n\t| `CHECK (${string})` // Support both with and without space\n\t| `FOREIGN KEY REFERENCES ${string} (${string})`\n\t| `DEFAULT ${string}`\n\t| \"NOT NULL\"\n/**\n * SQLite storage classes (data types)\n * @see https://www.sqlite.org/datatype3.html\n */\nexport type DataType = \"TEXT\" | \"INTEGER\" | \"REAL\" | \"BLOB\"\n\n/**\n * Patterns for combining data types with constraints based on nullability\n * @template T Field type\n * @template D SQLite data type\n */\nexport type ConstraintPatterns<T, D extends DataType> = undefined extends T\n\t?\n\t\t\t| `${D} ${BaseConstraint}`\n\t\t\t| `${D} ${BaseConstraint} ${Exclude<BaseConstraint, \"NOT NULL\">}`\n\t\t\t| `${D} ${BaseConstraint} ${Exclude<BaseConstraint, \"NOT NULL\">} ${Exclude<BaseConstraint, \"NOT NULL\">}`\n\t:\n\t\t\t| `${D} ${BaseConstraint}`\n\t\t\t| `${D} ${BaseConstraint} ${BaseConstraint}`\n\t\t\t| `${D} ${BaseConstraint} ${BaseConstraint} ${BaseConstraint}`\n\n/**\n * Maps TypeScript types to valid SQLite column definitions with constraints\n * @template T The TypeScript type to map\n */\nexport type ValidColumnTypeMap<T> = T extends string\n\t? ConstraintPatterns<T, \"TEXT\"> | \"TEXT\"\n\t: T extends number\n\t\t?\n\t\t\t\t| ConstraintPatterns<T, \"INTEGER\">\n\t\t\t\t| ConstraintPatterns<T, \"REAL\">\n\t\t\t\t| \"INTEGER\"\n\t\t\t\t| \"REAL\"\n\t\t: T extends boolean\n\t\t\t? ConstraintPatterns<T, \"INTEGER\"> | \"INTEGER\"\n\t\t\t: T extends bigint\n\t\t\t\t? ConstraintPatterns<T, \"INTEGER\"> | \"INTEGER\"\n\t\t\t\t: T extends object | unknown[]\n\t\t\t\t\t?\n\t\t\t\t\t\t\t| ConstraintPatterns<T, \"TEXT\">\n\t\t\t\t\t\t\t| ConstraintPatterns<T, \"BLOB\">\n\t\t\t\t\t\t\t| \"BLOB\"\n\t\t\t\t\t\t\t| \"TEXT\"\n\t\t\t\t\t: never\n\n/**\n * Type-safe column definitions for a table\n * @template T Table row type\n */\nexport type Schema<T extends DataRow> = {\n\t[K in keyof T]?: ValidColumnTypeMap<T[K]>\n} & { $$foreignKeys?: ForeignKeyDef<T>[] }\n",
"metadata": {
"size": 11536,
"modified": 1738737534330.812,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"validate.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n/**\n * Represents a validation error with a message and optional path indicating where the error occurred.\n * @property _validation_error - Internal flag to identify validation errors\n * @property message - Human-readable error message\n * @property path - Optional path indicating where the error occurred (e.g. \"user.name\")\n */\nexport type ValidationError = {\n\t_validation_error: true\n\tmessage: string\n\tpath?: string\n}\n\nexport const isValidationErr = (value: unknown): value is ValidationError => {\n\treturn (\n\t\ttypeof value === \"object\" && value !== null && \"_validation_error\" in value\n\t)\n}\n\nexport const isValidationErrs = (value: unknown): value is ValidationError[] =>\n\tArray.isArray(value) && value.length > 0 && value.every(isValidationErr)\n\nexport const validationErr = ({\n\tmsg: message,\n\tpath,\n}: {\n\tmsg: string\n\tpath?: string\n}): ValidationError => ({\n\t_validation_error: true,\n\tmessage,\n\tpath,\n})\n",
"metadata": {
"size": 1066,
"modified": 1738725761196.178,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"values.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { test, describe, beforeEach, afterEach } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { buildValuesStatement } from \"./values\"\nimport { NodeSqliteError } from \"./errors\"\nimport { DB } from \"#database\"\n\ndescribe(\"buildValuesStatement\", () => {\n\tdescribe(\"with '*' values\", () => {\n\t\ttest(\"builds SQL with all params keys\", () => {\n\t\t\tconst params = {\n\t\t\t\tname: \"John\",\n\t\t\t\tage: 30,\n\t\t\t\temail: \"john@example.com\",\n\t\t\t}\n\n\t\t\tconst result = buildValuesStatement(\"*\", params)\n\t\t\tassert.equal(result.sql, \"(name, age, email) VALUES ($name,$age,$email)\")\n\t\t})\n\n\t\ttest(\"handles empty params object\", () => {\n\t\t\tconst params = {}\n\t\t\tconst result = buildValuesStatement(\"*\", params)\n\t\t\tassert.equal(result.sql, \"() VALUES ()\")\n\t\t})\n\t})\n\n\tdescribe(\"with JSON columns configuration\", () => {\n\t\ttest(\"builds SQL with specified JSON columns\", () => {\n\t\t\tconst params = {\n\t\t\t\tid: 1,\n\t\t\t\tname: \"John\",\n\t\t\t\tmetadata: { key: \"value\" },\n\t\t\t}\n\n\t\t\tconst result = buildValuesStatement(\n\t\t\t\t[\"*\", { jsonColumns: [\"metadata\"] }],\n\t\t\t\tparams\n\t\t\t)\n\n\t\t\tassert.equal(\n\t\t\t\tresult.sql,\n\t\t\t\t\"(id, name, metadata) VALUES ($id,$name,jsonb($metadata))\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"handles multiple JSON columns\", () => {\n\t\t\tconst params = {\n\t\t\t\tid: 1,\n\t\t\t\tprofile: { age: 30 },\n\t\t\t\tsettings: { theme: \"dark\" },\n\t\t\t}\n\n\t\t\tconst result = buildValuesStatement(\n\t\t\t\t[\"*\", { jsonColumns: [\"profile\", \"settings\"] }],\n\t\t\t\tparams\n\t\t\t)\n\n\t\t\tassert.equal(\n\t\t\t\tresult.sql,\n\t\t\t\t\"(id, profile, settings) VALUES ($id,jsonb($profile),jsonb($settings))\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"ignores non-existent JSON columns\", () => {\n\t\t\tconst params = {\n\t\t\t\tid: 1,\n\t\t\t\tname: \"John\",\n\t\t\t}\n\n\t\t\tconst result = buildValuesStatement(\n\t\t\t\t[\"*\", { jsonColumns: [\"metadata\" as keyof typeof params] }],\n\t\t\t\tparams\n\t\t\t)\n\n\t\t\tassert.equal(result.sql, \"(id, name) VALUES ($id,$name)\")\n\t\t})\n\t})\n\n\tdescribe(\"with explicit column array\", () => {\n\t\ttest(\"builds SQL with specified columns\", () => {\n\t\t\tconst params = {\n\t\t\t\tname: \"John\",\n\t\t\t\tage: 30,\n\t\t\t\temail: \"john@example.com\",\n\t\t\t}\n\n\t\t\tconst result = buildValuesStatement([\"$name\", \"$age\"], params)\n\n\t\t\tassert.equal(result.sql, \"(name, age) VALUES ($name,$age)\")\n\t\t})\n\n\t\ttest(\"handles JSON columns with toJson suffix\", () => {\n\t\t\tconst params = {\n\t\t\t\tid: 1,\n\t\t\t\tmetadata: { key: \"value\" },\n\t\t\t\tsettings: { theme: \"dark\" },\n\t\t\t}\n\n\t\t\tconst result = buildValuesStatement(\n\t\t\t\t[\"$id\", \"$metadata->json\", \"$settings->json\"],\n\t\t\t\tparams\n\t\t\t)\n\n\t\t\tassert.equal(\n\t\t\t\tresult.sql,\n\t\t\t\t\"(id, metadata, settings) VALUES ($id,jsonb($metadata),jsonb($settings))\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"handles mix of regular and JSON columns\", () => {\n\t\t\tconst params = {\n\t\t\t\tid: 1,\n\t\t\t\tname: \"John\",\n\t\t\t\tmetadata: { key: \"value\" },\n\t\t\t}\n\n\t\t\tconst result = buildValuesStatement(\n\t\t\t\t[\"$id\", \"$name\", \"$metadata->json\"],\n\t\t\t\tparams\n\t\t\t)\n\n\t\t\tassert.equal(\n\t\t\t\tresult.sql,\n\t\t\t\t\"(id, name, metadata) VALUES ($id,$name,jsonb($metadata))\"\n\t\t\t)\n\t\t})\n\t})\n\n\tdescribe(\"error handling\", () => {\n\t\ttest(\"throws on invalid parameter format\", () => {\n\t\t\tconst params = { name: \"John\" }\n\n\t\t\tassert.throws(\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t() => buildValuesStatement([\"name\" as any], params),\n\t\t\t\t(err: unknown) => {\n\t\t\t\t\tassert(err instanceof NodeSqliteError)\n\t\t\t\t\tassert(err.message.includes(\"must be in format\"))\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t)\n\t\t})\n\n\t\ttest(\"throws on non-string parameter\", () => {\n\t\t\tconst params = { name: \"John\" }\n\n\t\t\tassert.throws(\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t() => buildValuesStatement([42 as any], params),\n\t\t\t\t(err: unknown) => {\n\t\t\t\t\tassert(err instanceof NodeSqliteError)\n\t\t\t\t\tassert(err.message.includes(\"must be a string\"))\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t)\n\t\t})\n\n\t\ttest(\"throws on invalid JSON column configuration\", () => {\n\t\t\tconst params = { name: \"John\" }\n\n\t\t\tassert.throws(\n\t\t\t\t() =>\n\t\t\t\t\tbuildValuesStatement(\n\t\t\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t\t\t[\"*\", { wrongKey: [\"metadata\"] } as any],\n\t\t\t\t\t\tparams\n\t\t\t\t\t),\n\t\t\t\tNodeSqliteError\n\t\t\t)\n\t\t})\n\n\t\ttest(\"throws on invalid toJson syntax\", () => {\n\t\t\tconst params = { metadata: { key: \"value\" } }\n\n\t\t\tassert.throws(\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t\t() => buildValuesStatement([\"$metadata->jso\" as any], params),\n\t\t\t\t(err: unknown) => {\n\t\t\t\t\tassert(err instanceof NodeSqliteError)\n\t\t\t\t\tassert(err.message.includes(\"must be in format\"))\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t)\n\t\t})\n\t})\n\n\tdescribe(\"edge cases\", () => {\n\t\ttest(\"handles params with special characters in names\", () => {\n\t\t\tconst params = {\n\t\t\t\t\"user-name\": \"John\",\n\t\t\t\temail_address: \"john@example.com\",\n\t\t\t}\n\n\t\t\tconst result = buildValuesStatement(\"*\", params)\n\t\t\tassert.equal(\n\t\t\t\tresult.sql,\n\t\t\t\t\"(user-name, email_address) VALUES ($user-name,$email_address)\"\n\t\t\t)\n\t\t})\n\n\t\ttest(\"preserves column order from explicit array\", () => {\n\t\t\tconst params = {\n\t\t\t\tc: 3,\n\t\t\t\ta: 1,\n\t\t\t\tb: 2,\n\t\t\t}\n\n\t\t\tconst result = buildValuesStatement([\"$c\", \"$a\", \"$b\"], params)\n\n\t\t\tassert.equal(result.sql, \"(c, a, b) VALUES ($c,$a,$b)\")\n\t\t})\n\n\t\ttest(\"handles single column case\", () => {\n\t\t\tconst params = { id: 1 }\n\n\t\t\tconst result = buildValuesStatement([\"$id\"], params)\n\t\t\tassert.equal(result.sql, \"(id) VALUES ($id)\")\n\t\t})\n\t})\n})\n\ndescribe(\"Values Context SQL Generation\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({\n\t\t\tlocation: \":memory:\",\n\t\t\tenvironment: \"testing\",\n\t\t})\n\t\tdb.exec(`\n CREATE TABLE test_data (\n simple_text TEXT,\n data_one TEXT,\n data_two TEXT\n );\n `)\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\ttest(\"generates correct SQL for basic values\", () => {\n\t\tconst stmt = db.sql<{\n\t\t\tsimple_text: string\n\t\t\tdata_one: string\n\t\t}>`INSERT INTO test_data ${{ values: [\"$simple_text\", \"$data_one\"] }}`\n\n\t\tassert.equal(\n\t\t\tstmt.sourceSQL({ simple_text: \"test\", data_one: \"data\" }).trim(),\n\t\t\t\"INSERT INTO test_data (simple_text, data_one)\\nVALUES ($simple_text, $data_one)\"\n\t\t)\n\t})\n\n\ttest(\"generates correct SQL for ->json fields\", () => {\n\t\ttype TestData = {\n\t\t\tsimple_text: string\n\t\t\tdata_one: { value: string }\n\t\t}\n\n\t\tconst stmt = db.sql<TestData>`INSERT INTO test_data ${{ values: [\"$simple_text\", \"$data_one->json\"] }}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({ simple_text: \"test\", data_one: { value: \"test value\" } })\n\t\t\t\t.trim(),\n\t\t\t\"INSERT INTO test_data (simple_text, data_one)\\nVALUES ($simple_text, jsonb($data_one))\"\n\t\t)\n\t})\n\n\ttest(\"generates correct SQL for multiple ->json fields\", () => {\n\t\ttype TestData = {\n\t\t\tsimple_text: string\n\t\t\tdata_one: { value: string }\n\t\t\tdata_two: { count: number }\n\t\t}\n\n\t\tconst stmt = db.sql<TestData>`INSERT INTO test_data ${{\n\t\t\tvalues: [\"$simple_text\", \"$data_one->json\", \"$data_two->json\"],\n\t\t}}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tsimple_text: \"test\",\n\t\t\t\t\tdata_one: { value: \"test value\" },\n\t\t\t\t\tdata_two: { count: 42 },\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"INSERT INTO test_data (simple_text, data_one, data_two)\\nVALUES ($simple_text, jsonb($data_one), jsonb($data_two))\"\n\t\t)\n\t})\n\n\ttest(\"generates correct SQL for '*' with jsonColumns\", () => {\n\t\ttype TestData = {\n\t\t\tsimple_text: string\n\t\t\tdata_one: { value: string }\n\t\t\tdata_two: { count: number }\n\t\t}\n\n\t\tconst stmt = db.sql<TestData>`INSERT INTO test_data ${{\n\t\t\tvalues: [\"*\", { jsonColumns: [\"data_one\", \"data_two\"] }],\n\t\t}}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tsimple_text: \"test\",\n\t\t\t\t\tdata_one: { value: \"test value\" },\n\t\t\t\t\tdata_two: { count: 42 },\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"INSERT INTO test_data (simple_text, data_one, data_two)\\nVALUES ($simple_text, jsonb($data_one), jsonb($data_two))\"\n\t\t)\n\t})\n})\n\ndescribe(\"Values Context SQL Generation\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({\n\t\t\tlocation: \":memory:\",\n\t\t\tenvironment: \"testing\",\n\t\t})\n\t\tdb.exec(`\n CREATE TABLE test_data (\n\t\tid INTEGER PRIMARY KEY,\n simple_text TEXT,\n data_one TEXT,\n data_two TEXT,\n metadata TEXT,\n settings TEXT,\n config TEXT,\n tags TEXT\n );\n `)\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\ttest(\"generates formatted SQL for basic values\", () => {\n\t\tconst stmt = db.sql<{\n\t\t\tsimple_text: string\n\t\t\tdata_one: string\n\t\t}>`INSERT INTO test_data ${{ values: [\"$simple_text\", \"$data_one\"] }}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tsimple_text: \"test\",\n\t\t\t\t\tdata_one: \"data\",\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"INSERT INTO test_data (simple_text, data_one)\\nVALUES ($simple_text, $data_one)\"\n\t\t)\n\t})\n\ttest(\"generates SQL for complex object with nested JSON\", () => {\n\t\ttype ComplexData = {\n\t\t\tsimple_text: string\n\t\t\tmetadata: { created: string }\n\t\t\tsettings: { theme: string }\n\t\t\tconfig: { flags: boolean }\n\t\t\ttags: string[]\n\t\t}\n\n\t\tconst stmt = db.sql<ComplexData>`INSERT INTO test_data ${{\n\t\t\tvalues: [\n\t\t\t\t\"$simple_text\",\n\t\t\t\t\"$metadata->json\",\n\t\t\t\t\"$settings->json\",\n\t\t\t\t\"$config->json\",\n\t\t\t\t\"$tags->json\",\n\t\t\t],\n\t\t}}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tsimple_text: \"test\",\n\t\t\t\t\tmetadata: { created: \"2025-01-01\" },\n\t\t\t\t\tsettings: { theme: \"dark\" },\n\t\t\t\t\tconfig: { flags: true },\n\t\t\t\t\ttags: [\"tag1\"],\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"INSERT INTO test_data (simple_text, metadata, settings, config, tags)\\nVALUES (\\n $simple_text,\\n jsonb($metadata),\\n jsonb($settings),\\n jsonb($config),\\n jsonb($tags)\\n )\"\n\t\t)\n\t})\n\n\ttest(\"generates SQL with all fields as JSON except one\", () => {\n\t\ttype AllJsonData = {\n\t\t\tid: string\n\t\t\tdata_one: { [key: string]: unknown }\n\t\t\tdata_two: { [key: string]: unknown }\n\t\t\tmetadata: { [key: string]: unknown }\n\t\t\tsettings: { [key: string]: unknown }\n\t\t\tconfig: { [key: string]: unknown }\n\t\t}\n\n\t\tconst stmt = db.sql<AllJsonData>`INSERT INTO test_data ${{\n\t\t\tvalues: [\n\t\t\t\t\"*\",\n\t\t\t\t{\n\t\t\t\t\tjsonColumns: [\n\t\t\t\t\t\t\"data_one\",\n\t\t\t\t\t\t\"data_two\",\n\t\t\t\t\t\t\"metadata\",\n\t\t\t\t\t\t\"settings\",\n\t\t\t\t\t\t\"config\",\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t],\n\t\t}}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tid: \"123\",\n\t\t\t\t\tdata_one: { key: \"value1\" },\n\t\t\t\t\tdata_two: { key: \"value2\" },\n\t\t\t\t\tmetadata: { key: \"value3\" },\n\t\t\t\t\tsettings: { key: \"value4\" },\n\t\t\t\t\tconfig: { key: \"value5\" },\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"INSERT INTO test_data (\\n id,\\n data_one,\\n data_two,\\n metadata,\\n settings,\\n config\\n )\\nVALUES (\\n $id,\\n jsonb($data_one),\\n jsonb($data_two),\\n jsonb($metadata),\\n jsonb($settings),\\n jsonb($config)\\n )\"\n\t\t)\n\t})\n})\n\ndescribe(\"batch Values Generation\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({\n\t\t\tlocation: \":memory:\",\n\t\t\tenvironment: \"testing\",\n\t\t})\n\t\tdb.exec(`\n CREATE TABLE bands (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL,\n formed_year INTEGER,\n members INTEGER,\n metadata TEXT\n );\n `)\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\ttest(\"generates correct SQL for basic array of values\", () => {\n\t\ttype Band = {\n\t\t\tname: string\n\t\t\tformed_year: number\n\t\t\tmembers: number\n\t\t}\n\n\t\tconst stmt = db.sql<Band>`\n INSERT INTO bands ${{ values: [\"*\", { batch: true }] }}\n `\n\n\t\tconst bands = [\n\t\t\t{ name: \"INDIAN OCEAN\", formed_year: 1990, members: 5 },\n\t\t\t{ name: \"BTS\", formed_year: 2013, members: 7 },\n\t\t\t{ name: \"METALLICA\", formed_year: 1981, members: 4 },\n\t\t]\n\n\t\tassert.equal(\n\t\t\tstmt.sourceSQL(bands).trim(),\n\t\t\t\"INSERT INTO bands (name, formed_year, members)\\nVALUES ($name_0, $formed_year_0, $members_0),\\n ($name_1, $formed_year_1, $members_1),\\n ($name_2, $formed_year_2, $members_2)\"\n\t\t)\n\t})\n\n\ttest(\"handles empty array\", () => {\n\t\ttype Band = {\n\t\t\tname: string\n\t\t\tformed_year: number\n\t\t}\n\n\t\tconst stmt = db.sql<Band>`\n INSERT INTO bands ${{ values: [\"*\", { batch: true }] }}\n `\n\n\t\tassert.throws(\n\t\t\t() => stmt.sourceSQL([]),\n\t\t\t(err: unknown) => {\n\t\t\t\tassert(err instanceof NodeSqliteError)\n\t\t\t\tassert(err.message.includes(\"Cannot insert empty array\"))\n\t\t\t\treturn true\n\t\t\t}\n\t\t)\n\t})\n\n\ttest(\"handles Set input\", () => {\n\t\ttype Band = {\n\t\t\tname: string\n\t\t\tmembers: number\n\t\t}\n\n\t\tconst stmt = db.sql<Band>`\n INSERT INTO bands ${{ values: [\"*\", { batch: true }] }}\n `\n\n\t\tconst bandsSet = new Set([\n\t\t\t{ name: \"PINK FLOYD\", members: 5 },\n\t\t\t{ name: \"LED ZEPPELIN\", members: 4 },\n\t\t])\n\n\t\tassert.equal(\n\t\t\tstmt.sourceSQL(bandsSet).trim(),\n\t\t\t\"INSERT INTO bands (name, members)\\nVALUES ($name_0, $members_0),\\n ($name_1, $members_1)\" // Fixed parameter names\n\t\t)\n\t})\n\n\ttest(\"combines batch with JSON columns\", () => {\n\t\ttype Band = {\n\t\t\tname: string\n\t\t\tmembers: number\n\t\t\tmetadata: {\n\t\t\t\tgenre: string[]\n\t\t\t\talbums: number\n\t\t\t\tactive: boolean\n\t\t\t}\n\t\t}\n\n\t\tconst stmt = db.sql<Band>`\n INSERT INTO bands ${{ values: [\"*\", { batch: true, jsonColumns: [\"metadata\"] }] }}\n `\n\n\t\tconst bands = [\n\t\t\t{\n\t\t\t\tname: \"QUEEN\",\n\t\t\t\tmembers: 4,\n\t\t\t\tmetadata: { genre: [\"rock\"], albums: 15, active: true },\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"THE BEATLES\",\n\t\t\t\tmembers: 4,\n\t\t\t\tmetadata: { genre: [\"rock\", \"pop\"], albums: 12, active: false },\n\t\t\t},\n\t\t]\n\n\t\tassert.equal(\n\t\t\tstmt.sourceSQL(bands).trim(),\n\t\t\t\"INSERT INTO bands (name, members, metadata)\\nVALUES ($name_0, $members_0, jsonb($metadata_0)),\\n ($name_1, $members_1, jsonb($metadata_1))\"\n\t\t)\n\t})\n\n\ttest(\"throws on non-array/non-set input\", () => {\n\t\ttype Band = { name: string }\n\t\tconst stmt = db.sql<Band>`\n INSERT INTO bands ${{ values: [\"*\", { batch: true }] }}\n `\n\n\t\t// Ensure we're testing with the correct error type and message\n\t\tassert.throws(() => stmt.sourceSQL({ name: \"INVALID\" }), {\n\t\t\tname: \"NodeSqliteError\",\n\t\t\tmessage: /Expected array or Set/,\n\t\t})\n\t})\n\n\ttest(\"handles array with single item\", () => {\n\t\ttype Band = {\n\t\t\tname: string\n\t\t\tmembers: number\n\t\t}\n\n\t\tconst stmt = db.sql<Band>`\n INSERT INTO bands ${{ values: [\"*\", { batch: true }] }}\n `\n\n\t\tconst bands = [{ name: \"SOLO ARTIST\", members: 1 }]\n\n\t\tassert.equal(\n\t\t\tstmt.sourceSQL(bands).trim(),\n\t\t\t\"INSERT INTO bands (name, members)\\nVALUES ($name_0, $members_0)\"\n\t\t)\n\t})\n\n\ttest(\"ensures consistent column order across all rows\", () => {\n\t\ttype Band = {\n\t\t\tname: string\n\t\t\tformed_year?: number\n\t\t\tmembers: number\n\t\t}\n\n\t\tconst stmt = db.sql<Band>`\n INSERT INTO bands ${{ values: [\"*\", { batch: true }] }}\n `\n\n\t\tconst bands = [\n\t\t\t{ name: \"BAND1\", members: 4, formed_year: 1990 },\n\t\t\t{ name: \"BAND2\", members: 3 },\n\t\t\t{ name: \"BAND3\", members: 5, formed_year: 1985 },\n\t\t]\n\n\t\tconst sql = stmt.sourceSQL(bands)\n\t\tconst lines = sql.trim().split(\"\\n\")\n\n\t\t// Verify column order is consistent\n\t\tconst columnsLine = lines[0]\n\t\tassert(columnsLine.includes(\"name\") && columnsLine.includes(\"members\"))\n\n\t\t// Verify all VALUES lines have same number of parameters\n\t\tconst valueSets = lines.slice(2)\n\t\tconst paramCounts = valueSets.map(line => (line.match(/\\$/g) || []).length)\n\t\tassert(paramCounts.every(count => count === paramCounts[0]))\n\t})\n})\n\ndescribe(\"Values Database Operations\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({\n\t\t\tlocation: \":memory:\",\n\t\t\tenvironment: \"testing\",\n\t\t})\n\n\t\t// Create test tables\n\t\tdb.exec(`\n CREATE TABLE stocks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n ticker TEXT NOT NULL UNIQUE,\n description TEXT,\n exchange TEXT CHECK(exchange IN ('AMEX','ARCA','BATS','NYSE','NASDAQ','NYSEARCA','FTXU','CBSE','GNSS','ERSX','OTC','CRYPTO','')),\n hasIcon INTEGER DEFAULT 0\n );\n `)\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\ttest(\":values - inserts single record with values array\", () => {\n\t\ttype Stock = {\n\t\t\tticker: string\n\t\t\tdescription: string\n\t\t\texchange: string\n\t\t\thasIcon: number\n\t\t}\n\n\t\tconst stock = {\n\t\t\tticker: \"AAPL\",\n\t\t\tdescription: \"Apple Inc.\",\n\t\t\texchange: \"NASDAQ\",\n\t\t\thasIcon: 1,\n\t\t}\n\n\t\tconst stmt = db.sql<Stock>`\n INSERT INTO stocks ${{ values: [\"$ticker\", \"$description\", \"$exchange\", \"$hasIcon\"] }}\n `\n\n\t\tstmt.run(stock)\n\n\t\tconst result =\n\t\t\tdb.sql`SELECT * FROM stocks WHERE ticker = ${\"$ticker\"}`.get<{\n\t\t\t\tid: number\n\t\t\t}>({ ticker: \"AAPL\" })\n\t\tassert.equal(result?.id, 1)\n\t})\n\n\ttest(\":values - inserts single record with values:* syntax\", () => {\n\t\ttype Stock = {\n\t\t\tticker: string\n\t\t\tdescription: string\n\t\t\texchange: string\n\t\t\thasIcon: number\n\t\t}\n\n\t\tconst stock = {\n\t\t\tticker: \"AAPL\",\n\t\t\tdescription: \"Apple Inc.\",\n\t\t\texchange: \"NASDAQ\",\n\t\t\thasIcon: 1,\n\t\t}\n\n\t\tconst stmt = db.sql<Stock>`\n INSERT INTO stocks ${{ values: \"*\" }}\n `\n\t\tstmt.run(stock)\n\n\t\tconst result =\n\t\t\tdb.sql`SELECT * FROM stocks WHERE ticker = ${\"$ticker\"}`.get<{\n\t\t\t\tticker: string\n\t\t\t}>({ ticker: \"AAPL\" })\n\t\tassert.deepEqual(result?.ticker, \"AAPL\")\n\t})\n\n\ttest(\":values - inserts batch of records using values:* and batch:true\", () => {\n\t\ttype Stock = {\n\t\t\tticker: string\n\t\t\tdescription: string\n\t\t\texchange: string\n\t\t\thasIcon: number\n\t\t}\n\n\t\tconst stocks = [\n\t\t\t{\n\t\t\t\tticker: \"AAPL\",\n\t\t\t\tdescription: \"Apple Inc.\",\n\t\t\t\texchange: \"NASDAQ\",\n\t\t\t\thasIcon: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tticker: \"MSFT\",\n\t\t\t\tdescription: \"Microsoft Corporation\",\n\t\t\t\texchange: \"NASDAQ\",\n\t\t\t\thasIcon: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tticker: \"GOOGL\",\n\t\t\t\tdescription: \"Alphabet Inc.\",\n\t\t\t\texchange: \"NASDAQ\",\n\t\t\t\thasIcon: 1,\n\t\t\t},\n\t\t]\n\n\t\tconst stmt = db.sql<Stock[]>`\n INSERT INTO stocks ${{ values: [\"*\", { batch: true }] }}\n `\n\t\tstmt.run(stocks)\n\n\t\tconst results = db.sql`SELECT * FROM stocks ORDER BY ticker`.all<{\n\t\t\tticker: string\n\t\t}>()\n\t\tassert.equal(results.length, 3)\n\t\tassert.deepEqual(\n\t\t\tresults.map(r => r.ticker),\n\t\t\t[\"AAPL\", \"GOOGL\", \"MSFT\"]\n\t\t)\n\t})\n\n\ttest(\":values - batch insert properly fails transaction on error\", () => {\n\t\ttype Stock = {\n\t\t\tticker: string\n\t\t\tdescription: string\n\t\t\texchange: string\n\t\t\thasIcon: number\n\t\t}\n\n\t\tconst stocks = [\n\t\t\t{\n\t\t\t\tticker: \"AAPL\",\n\t\t\t\tdescription: \"Apple Inc.\",\n\t\t\t\texchange: \"NASDAQ\",\n\t\t\t\thasIcon: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tticker: \"AAPL\", // Duplicate ticker should cause error\n\t\t\t\tdescription: \"Apple Inc Again\",\n\t\t\t\texchange: \"NASDAQ\",\n\t\t\t\thasIcon: 1,\n\t\t\t},\n\t\t]\n\n\t\tconst stmt = db.sql<Stock[]>`\n INSERT INTO stocks ${{ values: [\"*\", { batch: true }] }}\n `\n\n\t\t// Should throw due to unique constraint violation\n\t\tassert.throws(() => stmt.run(stocks))\n\n\t\t// Query to verify no records were inserted\n\t\tconst result = db.sql`\n SELECT COUNT(*) as count FROM stocks\n `.get<{ count: number }>()\n\n\t\t// Ensure we have a number even if result is undefined\n\t\tconst count = result?.count ?? 0\n\t\tassert.equal(count, 0)\n\t})\n\n\ttest(\":values - batch insert handles nulls and optional fields\", () => {\n\t\ttype Stock = {\n\t\t\tticker: string\n\t\t\tdescription?: string\n\t\t\texchange: string\n\t\t\thasIcon: number\n\t\t}\n\n\t\tconst stocks = [\n\t\t\t{\n\t\t\t\tticker: \"AAPL\",\n\t\t\t\tdescription: \"Apple Inc.\",\n\t\t\t\texchange: \"NASDAQ\",\n\t\t\t\thasIcon: 1,\n\t\t\t},\n\t\t\t{\n\t\t\t\tticker: \"MSFT\",\n\t\t\t\texchange: \"NASDAQ\", // No description\n\t\t\t\thasIcon: 1,\n\t\t\t},\n\t\t]\n\n\t\tconst stmt = db.sql<Stock[]>`\n INSERT INTO stocks ${{ values: [\"*\", { batch: true }] }}\n `\n\t\tstmt.run(stocks)\n\n\t\tconst results = db.sql`SELECT * FROM stocks ORDER BY ticker`.all<{\n\t\t\tdescription: string\n\t\t}>()\n\t\tassert.equal(results.length, 2)\n\t\tassert.equal(results[1].description, null)\n\t})\n\n\ttest(\":values - batch insert verifies constraints\", () => {\n\t\ttype Stock = {\n\t\t\tticker: string\n\t\t\tdescription: string\n\t\t\texchange: string\n\t\t\thasIcon: number\n\t\t}\n\n\t\tconst stocks = [\n\t\t\t{\n\t\t\t\tticker: \"AAPL\",\n\t\t\t\tdescription: \"Apple Inc.\",\n\t\t\t\texchange: \"INVALID\", // Invalid exchange value\n\t\t\t\thasIcon: 1,\n\t\t\t},\n\t\t]\n\n\t\tconst stmt = db.sql<Stock[]>`\n INSERT INTO stocks ${{ values: [\"*\", { batch: true }] }}\n `\n\n\t\t// Should throw due to check constraint violation\n\t\tassert.throws(() => stmt.run(stocks))\n\n\t\t// Query to verify no records were inserted\n\t\tconst result = db.sql`\n SELECT COUNT(*) as count FROM stocks\n `.get<{ count: number }>()\n\n\t\t// Ensure we have a number even if result is undefined\n\t\tconst count = result?.count ?? 0\n\t\tassert.equal(count, 0)\n\t})\n\n\ttest(\":values - inserts using values array with explicit ID\", () => {\n\t\ttype Stock = {\n\t\t\tid: number\n\t\t\tticker: string\n\t\t\tdescription: string\n\t\t\texchange: string\n\t\t\thasIcon: number\n\t\t}\n\n\t\tconst stock = {\n\t\t\tid: 42,\n\t\t\tticker: \"AAPL\",\n\t\t\tdescription: \"Apple Inc.\",\n\t\t\texchange: \"NASDAQ\",\n\t\t\thasIcon: 1,\n\t\t}\n\n\t\tconst stmt = db.sql<Stock>`\n INSERT INTO stocks ${{\n\t\t\t\tvalues: [\"$id\", \"$ticker\", \"$description\", \"$exchange\", \"$hasIcon\"],\n\t\t\t}}\n `\n\t\tstmt.run(stock)\n\n\t\tconst result = db.sql`SELECT * FROM stocks WHERE id = ${\"$id\"}`.get<{\n\t\t\tid: number\n\t\t}>({\n\t\t\tid: 42,\n\t\t})\n\t\tassert.deepEqual(result?.id, 42)\n\t})\n})\n",
"metadata": {
"size": 20355,
"modified": 1738725761196.2942,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"values.ts": {
"content": "// noinspection t\n\nimport { NodeSqliteError, SqlitePrimaryResultCode } from \"#errors\"\nimport type { DataRow, InsertOptions } from \"#types\"\n\ntype BuildSqlResult = {\n\tcolumns: string[]\n\tplaceholders: string[]\n\tparameterOperators: string[]\n\tisMulti?: boolean\n\titemCount?: number\n}\n\nfunction buildSqlComponents<P extends DataRow>(\n\toptions: InsertOptions<P>,\n\tparams: P | P[]\n): BuildSqlResult {\n\t// Handle batch operations\n\tif (Array.isArray(params) || params instanceof Set) {\n\t\tconst items = Array.from(params)\n\t\tif (items.length === 0) {\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\"Empty data set\",\n\t\t\t\t\"Cannot insert empty array or set\",\n\t\t\t\tundefined\n\t\t\t)\n\t\t}\n\n\t\tconst firstItem = items[0]\n\t\tconst columns = Object.keys(firstItem)\n\n\t\t// Handle * with batch config\n\t\tif (Array.isArray(options) && options[0] === \"*\" && options.length === 2) {\n\t\t\tconst [, config] = options\n\t\t\tconst jsonColumns = new Set(\n\t\t\t\ttypeof config === \"object\" && \"jsonColumns\" in config\n\t\t\t\t\t? config.jsonColumns\n\t\t\t\t\t: []\n\t\t\t)\n\n\t\t\t// Generate unique parameter names and placeholders for each row\n\t\t\tconst placeholderRows = items.map((_, rowIndex) => {\n\t\t\t\tconst rowPlaceholders = columns.map(col =>\n\t\t\t\t\tjsonColumns.has(col)\n\t\t\t\t\t\t? `jsonb($${col}_${rowIndex})`\n\t\t\t\t\t\t: `$${col}_${rowIndex}`\n\t\t\t\t)\n\t\t\t\treturn `(${rowPlaceholders.join(\", \")})`\n\t\t\t})\n\n\t\t\tconst paramOps = items.flatMap((_, rowIndex) =>\n\t\t\t\tcolumns.map(col => `$${col}_${rowIndex}`)\n\t\t\t)\n\n\t\t\treturn {\n\t\t\t\tcolumns,\n\t\t\t\tplaceholders: placeholderRows,\n\t\t\t\tparameterOperators: paramOps,\n\t\t\t\tisMulti: true,\n\t\t\t\titemCount: items.length,\n\t\t\t}\n\t\t}\n\n\t\t// Handle simple * for batch\n\t\tif (options === \"*\") {\n\t\t\tconst placeholderRows = items.map((_, rowIndex) => {\n\t\t\t\tconst rowPlaceholders = columns.map(col => `$${col}_${rowIndex}`)\n\t\t\t\treturn `(${rowPlaceholders.join(\", \")})`\n\t\t\t})\n\n\t\t\tconst paramOps = items.flatMap((_, rowIndex) =>\n\t\t\t\tcolumns.map(col => `$${col}_${rowIndex}`)\n\t\t\t)\n\n\t\t\treturn {\n\t\t\t\tcolumns,\n\t\t\t\tplaceholders: placeholderRows,\n\t\t\t\tparameterOperators: paramOps,\n\t\t\t\tisMulti: true,\n\t\t\t\titemCount: items.length,\n\t\t\t}\n\t\t}\n\t}\n\n\t// Handle single record operations (rest of the code remains the same)\n\tif (options === \"*\") {\n\t\tconst columns = Object.keys(params as P)\n\t\tconst paramOps = columns.map(k => `$${k}`)\n\t\treturn {\n\t\t\tcolumns,\n\t\t\tplaceholders: paramOps,\n\t\t\tparameterOperators: paramOps,\n\t\t}\n\t}\n\n\tif (Array.isArray(options) && options[0] === \"*\" && options.length === 2) {\n\t\tconst [, config] = options\n\n\t\tif (!config || typeof config !== \"object\") {\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\"Invalid configuration\",\n\t\t\t\t\"Second element must be a configuration object\",\n\t\t\t\tundefined\n\t\t\t)\n\t\t}\n\n\t\tif (\n\t\t\t\"batch\" in config &&\n\t\t\tconfig.batch &&\n\t\t\t!Array.isArray(params) &&\n\t\t\t!(params instanceof Set)\n\t\t) {\n\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\"Invalid parameters\",\n\t\t\t\t\"Expected array or Set when using batch\",\n\t\t\t\tundefined\n\t\t\t)\n\t\t}\n\n\t\tif (\"jsonColumns\" in config) {\n\t\t\tconst jsonColumns = new Set(config.jsonColumns)\n\t\t\tconst columns = Object.keys(params as P)\n\t\t\tconst paramOps = columns.map(k => `$${k}`)\n\n\t\t\treturn {\n\t\t\t\tcolumns,\n\t\t\t\tplaceholders: columns.map(col =>\n\t\t\t\t\tjsonColumns.has(col) ? `jsonb($${col})` : `$${col}`\n\t\t\t\t),\n\t\t\t\tparameterOperators: paramOps,\n\t\t\t}\n\t\t}\n\n\t\tthrow new NodeSqliteError(\n\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\"Invalid configuration\",\n\t\t\t\"Configuration must include jsonColumns for single record\",\n\t\t\tundefined\n\t\t)\n\t}\n\n\t// Handle explicit column array (remains the same)\n\tif (Array.isArray(options)) {\n\t\tconst columns: string[] = []\n\t\tconst placeholders: string[] = []\n\t\tconst paramOps: string[] = []\n\n\t\tfor (const op of options) {\n\t\t\tif (typeof op !== \"string\") {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Invalid parameter format\",\n\t\t\t\t\t`Parameter must be a string but got ${typeof op}`,\n\t\t\t\t\tundefined\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst match = op.match(/^\\$([^->]+)(->json)?$/)\n\t\t\tif (!match) {\n\t\t\t\tthrow new NodeSqliteError(\n\t\t\t\t\t\"ERR_SQLITE_PARAM\",\n\t\t\t\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\t\t\t\"Invalid parameter format\",\n\t\t\t\t\t`Parameter \"${op}\" must be in format $column or $column->json`,\n\t\t\t\t\tundefined\n\t\t\t\t)\n\t\t\t}\n\n\t\t\tconst column = match[1]\n\t\t\tcolumns.push(column)\n\t\t\tparamOps.push(op)\n\n\t\t\tif (op.endsWith(\"->json\")) {\n\t\t\t\tplaceholders.push(`jsonb($${column})`)\n\t\t\t} else {\n\t\t\t\tplaceholders.push(`$${column}`)\n\t\t\t}\n\t\t}\n\n\t\treturn { columns, placeholders, parameterOperators: paramOps }\n\t}\n\n\tthrow new NodeSqliteError(\n\t\t\"ERR_SQLITE_PARAM\",\n\t\tSqlitePrimaryResultCode.SQLITE_ERROR,\n\t\t\"Invalid format\",\n\t\t\"Must be '*', an array of parameters, or a configuration tuple\",\n\t\tundefined\n\t)\n}\n\nexport function buildValuesStatement<P extends DataRow>(\n\tvalues: InsertOptions<P>,\n\tparams: P | P[]\n): { sql: string; parameterOperators: string[] } {\n\tconst result = buildSqlComponents(values, params)\n\n\tif (\n\t\tresult.isMulti &&\n\t\tresult.itemCount &&\n\t\tArray.isArray(result.placeholders)\n\t) {\n\t\treturn {\n\t\t\tsql: `(${result.columns.join(\", \")}) VALUES\\n ${result.placeholders.join(\",\\n \")}`,\n\t\t\tparameterOperators: result.parameterOperators,\n\t\t}\n\t}\n\n\treturn {\n\t\tsql: `(${result.columns.join(\", \")}) VALUES (${result.placeholders})`,\n\t\tparameterOperators: result.parameterOperators,\n\t}\n}\n",
"metadata": {
"size": 5434,
"modified": 1738725761196.4004,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"where.test.ts": {
"content": "// Copyright 2025 Takin Profit. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\nimport { test, describe, beforeEach, afterEach } from \"node:test\"\nimport assert from \"node:assert/strict\"\nimport { buildWhereStatement, validateWhereClause } from \"#where\"\nimport { DB } from \"#database\"\nimport { raw } from \"#sql.js\"\nimport type { WhereClause } from \"#types.js\"\n\ninterface TestUser {\n\tid: number\n\tname: string\n\tactive: boolean\n\tmetadata: object\n}\n\ndescribe(\"validateWhereClause\", () => {\n\ttest(\"validates single conditions\", () => {\n\t\tconst validCases: WhereClause<TestUser>[] = [\n\t\t\t\"id = $id\",\n\t\t\t\"name LIKE $name\",\n\t\t\t\"active != $active\",\n\t\t\t\"metadata IS NULL\",\n\t\t\t\"metadata IS NOT NULL\",\n\t\t]\n\n\t\tfor (const condition of validCases) {\n\t\t\tconst errors = validateWhereClause(condition)\n\t\t\tassert.equal(errors.length, 0, `Expected no errors for: ${condition}`)\n\t\t}\n\t})\n\n\ttest(\"validates compound conditions\", () => {\n\t\tconst validCases: WhereClause<TestUser>[] = [\n\t\t\t[\"id > $id\", \"AND\", \"active = $active\"],\n\t\t\t[\"name LIKE $name\", \"OR\", \"metadata IS NULL\", \"AND\", \"active != $active\"],\n\t\t\t[\n\t\t\t\t\"id < $id\",\n\t\t\t\t\"AND\",\n\t\t\t\t\"name = $name\",\n\t\t\t\t\"OR\",\n\t\t\t\t\"active = $active\",\n\t\t\t\t\"AND\",\n\t\t\t\t\"metadata IS NOT NULL\",\n\t\t\t],\n\t\t]\n\n\t\tfor (const condition of validCases) {\n\t\t\tconst errors = validateWhereClause(condition)\n\t\t\tassert.equal(\n\t\t\t\terrors.length,\n\t\t\t\t0,\n\t\t\t\t`Expected no errors for: ${JSON.stringify(condition)}`\n\t\t\t)\n\t\t}\n\t})\n\n\ttest(\"rejects invalid single conditions\", () => {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\tconst invalidCases: any = [\n\t\t\t\"id ==== $id\", // Invalid operator\n\t\t\t\"name LIKES $name\", // Invalid operator\n\t\t\t\"active <> $active\", // Invalid operator\n\t\t\t\"id = id\", // Missing $ prefix\n\t\t\t\"metadata >> $id\", // Invalid operator\n\t\t]\n\n\t\tfor (const condition of invalidCases) {\n\t\t\tconst errors = validateWhereClause(condition)\n\t\t\tassert.ok(errors.length > 0, `Expected errors for: ${condition}`)\n\t\t}\n\t})\n\n\ttest(\"rejects invalid compound conditions\", () => {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\tconst invalidCases: any = [\n\t\t\t[],\n\t\t\t[\"single_condition\"],\n\t\t\t[\"id = $id\", \"INVALID_OP\", \"name = $name\"],\n\t\t\t[\"id = $id\", \"AND\", \"name = $name\", \"OR\"], // Missing final condition\n\t\t\t[\"AND\", \"id = $id\", \"OR\", \"name = $name\"], // Starts with operator\n\t\t\t[\"id = $id\", \"AND\", \"invalid condition\", \"OR\", \"name = $name\"],\n\t\t]\n\n\t\tfor (const condition of invalidCases) {\n\t\t\tconst errors = validateWhereClause(condition)\n\t\t\tassert.ok(\n\t\t\t\terrors.length > 0,\n\t\t\t\t`Expected errors for: ${JSON.stringify(condition)}`\n\t\t\t)\n\t\t}\n\t})\n\n\ttest(\"validates operator alternation\", () => {\n\t\tconst errors = validateWhereClause([\n\t\t\t\"id = $id\",\n\t\t\t\"AND\",\n\t\t\t\"name = $name\",\n\t\t\t\"AND\",\n\t\t\t\"name = $name\",\n\t\t\t\"OR\",\n\t\t\t\"active = $active\",\n\t\t\t\"AND\",\n\t\t\t\"metadata IS NULL\",\n\t\t])\n\t\tassert.equal(errors.length, 0)\n\n\t\tconst invalidErrors = validateWhereClause([\n\t\t\t\"id = $id\",\n\t\t\t\"AND\",\n\t\t\t\"OR\", // Two operators in sequence\n\t\t\t\"name = $name\",\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t] as any)\n\t\tassert.ok(invalidErrors.length > 0)\n\t})\n\n\ttest(\"validates length constraints\", () => {\n\t\t// Test minimum length\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\tconst tooShort = validateWhereClause([\"id = $id\"] as any)\n\t\tassert.ok(tooShort.length > 0)\n\n\t\t// Test even length rejection\n\t\tconst evenLength = validateWhereClause([\n\t\t\t\"id = $id\",\n\t\t\t\"AND\",\n\t\t\t\"name = $name\",\n\t\t\t\"AND\",\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t] as any)\n\t\tassert.ok(evenLength.length > 0)\n\t})\n})\n\ndescribe(\"buildWhereStatement\", () => {\n\ttest(\"builds single condition with each comparison operator\", () => {\n\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\tconst conditions: any = [\n\t\t\t\"id = $id\",\n\t\t\t\"name != $name\",\n\t\t\t\"age > $min_age\",\n\t\t\t\"age < $max_age\",\n\t\t\t\"age >= $min_age\",\n\t\t\t\"age <= $max_age\",\n\t\t\t\"name LIKE $pattern\",\n\t\t\t\"email NOT LIKE $pattern\",\n\t\t\t\"id IN $ids\",\n\t\t\t\"id NOT IN $ids\",\n\t\t\t\"metadata IS NULL\",\n\t\t\t\"metadata IS NOT NULL\",\n\t\t]\n\n\t\tfor (const condition of conditions) {\n\t\t\tconst result = buildWhereStatement(condition)\n\t\t\tassert.equal(result.sql, `WHERE ${condition}`)\n\t\t}\n\t})\n\n\ttest(\"builds compound conditions with AND\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t\"id > $min_id\",\n\t\t\t\"AND\",\n\t\t\t\"id < $max_id\",\n\t\t\t\"AND\",\n\t\t\t\"active = $active\",\n\t\t])\n\t\tassert.equal(\n\t\t\tresult.sql,\n\t\t\t\"WHERE id > $min_id AND id < $max_id AND active = $active\"\n\t\t)\n\t})\n\n\ttest(\"builds compound conditions with OR\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t\"name LIKE $pattern1\",\n\t\t\t\"OR\",\n\t\t\t\"name LIKE $pattern2\",\n\t\t\t\"OR\",\n\t\t\t\"email LIKE $pattern3\",\n\t\t])\n\t\tassert.equal(\n\t\t\tresult.sql,\n\t\t\t\"WHERE name LIKE $pattern1 OR name LIKE $pattern2 OR email LIKE $pattern3\"\n\t\t)\n\t})\n\n\ttest(\"builds mixed AND/OR conditions\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t\"active = $active\",\n\t\t\t\"AND\",\n\t\t\t\"age > $min_age\",\n\t\t\t\"OR\",\n\t\t\t\"name LIKE $pattern\",\n\t\t])\n\t\tassert.equal(\n\t\t\tresult.sql,\n\t\t\t\"WHERE active = $active AND age > $min_age OR name LIKE $pattern\"\n\t\t)\n\t})\n\n\ttest(\"builds complex nested conditions\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t\"id > $min_id\",\n\t\t\t\"AND\",\n\t\t\t\"id < $max_id\",\n\t\t\t\"AND\",\n\t\t\t\"active = $active\",\n\t\t\t\"OR\",\n\t\t\t\"metadata IS NULL\",\n\t\t\t\"AND\",\n\t\t\t\"created_at < $date\",\n\t\t])\n\t\tassert.equal(\n\t\t\tresult.sql,\n\t\t\t\"WHERE id > $min_id AND id < $max_id AND active = $active OR metadata IS NULL AND created_at < $date\"\n\t\t)\n\t})\n\n\ttest(\"handles null checks with other conditions\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t\"metadata IS NULL\",\n\t\t\t\"OR\",\n\t\t\t\"metadata IS NOT NULL\",\n\t\t\t\"AND\",\n\t\t\t\"active = $active\",\n\t\t])\n\t\tassert.equal(\n\t\t\tresult.sql,\n\t\t\t\"WHERE metadata IS NULL OR metadata IS NOT NULL AND active = $active\"\n\t\t)\n\t})\n\n\ttest(\"handles multiple IN conditions\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t\"id IN $ids\",\n\t\t\t\"AND\",\n\t\t\t\"age IN $ages\",\n\t\t\t\"OR\",\n\t\t\t\"name NOT IN $names\",\n\t\t])\n\t\tassert.equal(\n\t\t\tresult.sql,\n\t\t\t\"WHERE id IN $ids AND age IN $ages OR name NOT IN $names\"\n\t\t)\n\t})\n\n\ttest(\"handles LIKE conditions with mixed operators\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t\"name LIKE $pattern\",\n\t\t\t\"AND\",\n\t\t\t\"email NOT LIKE $pattern\",\n\t\t\t\"OR\",\n\t\t\t\"age > $min_age\",\n\t\t\t\"AND\",\n\t\t\t\"active = $active\",\n\t\t])\n\t\tassert.equal(\n\t\t\tresult.sql,\n\t\t\t\"WHERE name LIKE $pattern AND email NOT LIKE $pattern OR age > $min_age AND active = $active\"\n\t\t)\n\t})\n\n\ttest(\"handles maximum supported condition length\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t\"id = $id1\",\n\t\t\t\"AND\",\n\t\t\t\"id = $id2\",\n\t\t\t\"AND\",\n\t\t\t\"id = $id3\",\n\t\t\t\"AND\",\n\t\t\t\"id = $id4\",\n\t\t\t\"AND\",\n\t\t\t\"id = $id5\",\n\t\t\t\"AND\",\n\t\t\t\"id = $id6\",\n\t\t\t\"AND\",\n\t\t\t\"id = $id7\",\n\t\t\t\"AND\",\n\t\t\t\"id = $id8\",\n\t\t\t\"AND\",\n\t\t\t\"id = $id9\",\n\t\t\t\"AND\",\n\t\t\t\"id = $id10\",\n\t\t])\n\t\tassert.equal(\n\t\t\tresult.sql,\n\t\t\t\"WHERE id = $id1 AND id = $id2 AND id = $id3 AND id = $id4 AND id = $id5 AND id = $id6 AND id = $id7 AND id = $id8 AND id = $id9 AND id = $id10\"\n\t\t)\n\t})\n})\n\ndescribe(\"Where Context SQL Generation\", () => {\n\tlet db: DB\n\n\tbeforeEach(() => {\n\t\tdb = new DB({\n\t\t\tlocation: \":memory:\",\n\t\t\tenvironment: \"testing\",\n\t\t})\n\t\tdb.exec(`\n CREATE TABLE test_data (\n id INTEGER PRIMARY KEY,\n name TEXT,\n age INTEGER,\n active BOOLEAN,\n metadata TEXT,\n created_at TEXT,\n email TEXT,\n settings TEXT\n );\n `)\n\t})\n\n\tafterEach(() => {\n\t\tdb.close()\n\t})\n\n\ttest(\"generates basic where condition\", () => {\n\t\tconst stmt = db.sql<{\n\t\t\tid: number\n\t\t}>`SELECT * FROM test_data ${{ where: \"id = $id\" }}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tid: 1,\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"SELECT *\\nFROM test_data\\nWHERE id = $id\"\n\t\t)\n\t})\n\n\ttest(\"generates where with AND conditions\", () => {\n\t\ttype QueryParams = { min_age: number; active: boolean }\n\t\tconst stmt = db.sql<QueryParams>`SELECT * FROM test_data ${{\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\twhere: [\"age > $min_age\", \"AND\", \"active = $active\"] as any,\n\t\t}}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tmin_age: 18,\n\t\t\t\t\tactive: true,\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"SELECT *\\nFROM test_data\\nWHERE age > $min_age\\n AND active = $active\"\n\t\t)\n\t})\n\n\ttest(\"generates where with complex conditions\", () => {\n\t\ttype QueryParams = {\n\t\t\tmin_age: number\n\t\t\tpattern: string\n\t\t\tactive: boolean\n\t\t}\n\t\tconst stmt = db.sql<QueryParams>`SELECT * FROM test_data ${{\n\t\t\twhere: [\n\t\t\t\t\"age > $min_age\",\n\t\t\t\t\"AND\",\n\t\t\t\t\"name LIKE $pattern\",\n\t\t\t\t\"OR\",\n\t\t\t\t\"active = $active\",\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t] as any,\n\t\t}}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tmin_age: 18,\n\t\t\t\t\tpattern: \"test%\",\n\t\t\t\t\tactive: true,\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"SELECT *\\nFROM test_data\\nWHERE age > $min_age\\n AND name LIKE $pattern\\n OR active = $active\"\n\t\t)\n\t})\n\n\ttest(\"generates where with IS NULL\", () => {\n\t\tconst stmt = db.sql<\n\t\t\tRecord<string, never>\n\t\t>`SELECT * FROM test_data ${{ where: \"metadata IS NULL\" }}`\n\n\t\tassert.equal(\n\t\t\tstmt.sourceSQL().trim(),\n\t\t\t\"SELECT *\\nFROM test_data\\nWHERE metadata IS NULL\"\n\t\t)\n\t})\n\n\ttest(\"generates where with complex JSON conditions\", () => {\n\t\ttype QueryParams = {\n\t\t\tmin_age: number\n\t\t\tsettings: { theme: string }\n\t\t}\n\t\tconst stmt = db.sql<QueryParams>`SELECT * FROM test_data ${{\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\twhere: [\"age > $min_age\", \"AND\", \"settings = $settings->json\"] as any,\n\t\t}}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tmin_age: 18,\n\t\t\t\t\tsettings: {\n\t\t\t\t\t\ttheme: \"dark\",\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"SELECT *\\nFROM test_data\\nWHERE age > $min_age\\n AND settings = jsonb($settings)\"\n\t\t)\n\t})\n\n\ttest(\"validates raw value conditions\", () => {\n\t\tconst validCases: WhereClause<TestUser>[] = [\n\t\t\t[\"id\", \"=\", raw`(SELECT max_id FROM config)`],\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t[\"created_at\", \">\", raw`CURRENT_TIMESTAMP`] as any,\n\t\t\t[\"updated_at\", \"<\", raw`datetime('now', '-7 days')`],\n\t\t]\n\n\t\tfor (const condition of validCases) {\n\t\t\tconst errors = validateWhereClause(condition)\n\t\t\tassert.equal(\n\t\t\t\terrors.length,\n\t\t\t\t0,\n\t\t\t\t`Expected no errors for: ${JSON.stringify(condition)}`\n\t\t\t)\n\t\t}\n\t})\n\n\ttest(\"validates raw values in compound conditions\", () => {\n\t\tconst validCases: WhereClause<TestUser>[] = [\n\t\t\t[\n\t\t\t\t[\"id\", \">\", raw`(SELECT min_id FROM ranges)`],\n\t\t\t\t\"AND\",\n\t\t\t\t\"active = $active\",\n\t\t\t],\n\t\t\t[\"name LIKE $pattern\", \"OR\", [\"created_at\", \">\", raw`CURRENT_TIMESTAMP`]],\n\t\t\t[\n\t\t\t\t[\"id\", \"=\", raw`(SELECT ref_id FROM related)`],\n\t\t\t\t\"AND\",\n\t\t\t\t\"metadata IS NOT NULL\",\n\t\t\t\t\"OR\",\n\t\t\t\t[\"updated_at\", \"<\", raw`datetime('now', '-1 hour')`],\n\t\t\t],\n\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t] as any\n\n\t\tfor (const condition of validCases) {\n\t\t\tconst errors = validateWhereClause(condition)\n\t\t\tassert.equal(\n\t\t\t\terrors.length,\n\t\t\t\t0,\n\t\t\t\t`Expected no errors for: ${JSON.stringify(condition)}`\n\t\t\t)\n\t\t}\n\t})\n\n\ttest(\"rejects invalid raw value conditions\", () => {\n\t\tconst invalidCases = [\n\t\t\t[\"id\", \"INVALID\", raw`something`], // Invalid operator\n\t\t\t[\"id\", \"=\", \"not a raw value\"], // Missing raw value\n\t\t\t[\"id\", \"=\"], // Missing raw value\n\t\t\t[123, \"=\", raw`value`], // Invalid column name\n\t\t]\n\n\t\tfor (const condition of invalidCases) {\n\t\t\tconst errors = validateWhereClause(condition as WhereClause<TestUser>)\n\t\t\tassert.ok(\n\t\t\t\terrors.length > 0,\n\t\t\t\t`Expected errors for: ${JSON.stringify(condition)}`\n\t\t\t)\n\t\t}\n\t})\n\n\t// Add these tests inside the \"buildWhereStatement\" describe block:\n\n\ttest(\"builds single condition with raw value\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t\"id\",\n\t\t\t\">\",\n\t\t\traw`(SELECT max_id FROM ranges)`,\n\t\t])\n\t\tassert.equal(result.sql, \"WHERE id > (SELECT max_id FROM ranges)\")\n\t\tassert.deepEqual(result.parameterOperators, [])\n\t})\n\n\ttest(\"builds conditions mixing raw values and parameters\", () => {\n\t\tconst result = buildWhereStatement([\n\t\t\t[\"created_at\", \">\", raw`CURRENT_TIMESTAMP`],\n\t\t\t\"AND\",\n\t\t\t\"name LIKE $pattern\",\n\t\t\t\"OR\",\n\t\t\t[\"updated_at\", \"<\", raw`datetime('now', '-1 day')`],\n\t\t])\n\t\tassert.equal(\n\t\t\tresult.sql,\n\t\t\t\"WHERE created_at > CURRENT_TIMESTAMP AND name LIKE $pattern OR updated_at < datetime('now', '-1 day')\"\n\t\t)\n\t\tassert.deepEqual(result.parameterOperators, [\"$pattern\"])\n\t})\n\n\t// Add this test inside the \"Where Context SQL Generation\" describe block:\n\n\ttest(\"generates where with raw value conditions\", () => {\n\t\ttype QueryParams = {\n\t\t\tpattern: string\n\t\t}\n\t\tconst stmt = db.sql<QueryParams>`SELECT * FROM test_data ${{\n\t\t\twhere: [\n\t\t\t\t[\"created_at\", \">\", raw`datetime('now', '-7 days')`],\n\t\t\t\t\"AND\",\n\t\t\t\t\"name LIKE $pattern\",\n\t\t\t\t// biome-ignore lint/suspicious/noExplicitAny: <explanation>\n\t\t\t] as any,\n\t\t}}`\n\n\t\tassert.equal(\n\t\t\tstmt\n\t\t\t\t.sourceSQL({\n\t\t\t\t\tpattern: \"test%\",\n\t\t\t\t})\n\t\t\t\t.trim(),\n\t\t\t\"SELECT *\\nFROM test_data\\nWHERE created_at > datetime('now', '-7 days')\\n AND name LIKE $pattern\"\n\t\t)\n\t})\n})\n",
"metadata": {
"size": 12905,
"modified": 1738725761196.501,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
},
"where.ts": {
"content": "import {\n\tCOMPARISON_OPERATORS,\n\tLOGICAL_OPERATORS,\n\ttype RawValue,\n\ttype ComparisonOperator,\n\ttype DataRow,\n\ttype LogicalOperator,\n\tisRawValue,\n\ttype WhereClause,\n} from \"#types\"\nimport { validationErr, type ValidationError } from \"#validate.js\"\n\nexport function validateWhereClause<P extends DataRow>(\n\twhere: WhereClause<P>\n): ValidationError[] {\n\tconst errors: ValidationError[] = []\n\n\tif (typeof where === \"string\") {\n\t\treturn validateSingleCondition(where)\n\t}\n\n\t// Handle tuple format for RawValue\n\tif (\n\t\tArray.isArray(where) &&\n\t\twhere.length === 3 &&\n\t\t!LOGICAL_OPERATORS.includes(where[1] as (typeof LOGICAL_OPERATORS)[number])\n\t) {\n\t\tconst [column, operator, value] = where\n\t\tif (\n\t\t\ttypeof column !== \"string\" ||\n\t\t\t!COMPARISON_OPERATORS.includes(operator as ComparisonOperator) ||\n\t\t\t!isRawValue(value)\n\t\t) {\n\t\t\treturn [\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: \"Invalid RawValue condition format\",\n\t\t\t\t\tpath: \"\",\n\t\t\t\t}),\n\t\t\t]\n\t\t}\n\t\treturn []\n\t}\n\n\tif (!Array.isArray(where)) {\n\t\treturn [\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"Where clause must be a string, array, or RawValue condition\",\n\t\t\t}),\n\t\t]\n\t}\n\n\t// Check for minimum length and odd number of elements for logical combinations\n\tif (where.length < 3 || where.length % 2 === 0) {\n\t\treturn [\n\t\t\tvalidationErr({\n\t\t\t\tmsg: \"Where array must have odd number of elements with minimum length 3\",\n\t\t\t}),\n\t\t]\n\t}\n\n\t// Validate conditions and operators alternate correctly\n\tfor (let i = 0; i < where.length; i++) {\n\t\tif (i % 2 === 0) {\n\t\t\t// Should be condition\n\t\t\tif (Array.isArray(where[i])) {\n\t\t\t\t// Handle RawValue condition\n\t\t\t\tconst condition = where[i] as [string, ComparisonOperator, RawValue]\n\t\t\t\tif (condition.length !== 3 || !isRawValue(condition[2])) {\n\t\t\t\t\terrors.push(\n\t\t\t\t\t\tvalidationErr({\n\t\t\t\t\t\t\tmsg: `Invalid RawValue condition at position ${i}`,\n\t\t\t\t\t\t\tpath: `[${i}]`,\n\t\t\t\t\t\t})\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst conditionErrors = validateSingleCondition(where[i] as string)\n\t\t\t\terrors.push(...conditionErrors)\n\t\t\t}\n\t\t} else if (!LOGICAL_OPERATORS.includes(where[i] as LogicalOperator)) {\n\t\t\terrors.push(\n\t\t\t\tvalidationErr({\n\t\t\t\t\tmsg: `Invalid logical operator at position ${i}`,\n\t\t\t\t\tpath: `[${i}]`,\n\t\t\t\t})\n\t\t\t)\n\t\t}\n\t}\n\n\treturn errors\n}\n\nfunction validateSingleCondition<P extends DataRow>(\n\tcondition: string\n): ValidationError[] {\n\tconst pattern = new RegExp(\n\t\t`^[\\\\w]+\\\\s+(${COMPARISON_OPERATORS.join(\"|\")})\\\\s+\\\\$[\\\\w->json]+$|^[\\\\w]+\\\\s+IS(\\\\s+NOT)?\\\\s+NULL$`\n\t)\n\n\tif (!pattern.test(condition)) {\n\t\treturn [\n\t\t\tvalidationErr({\n\t\t\t\tmsg: `Invalid condition format: ${condition}`,\n\t\t\t\tpath: condition,\n\t\t\t}),\n\t\t]\n\t}\n\n\treturn []\n}\n\nexport function buildWhereStatement<P extends DataRow>(\n\twhere: WhereClause<P>,\n\tparams?: P\n): { sql: string; parameterOperators: string[] } {\n\tconst paramOps: string[] = []\n\n\tif (typeof where === \"string\") {\n\t\t// Extract parameter names from where clause\n\t\tconst matches = where.match(/\\$\\w+/g) || []\n\t\tparamOps.push(...matches)\n\t\treturn {\n\t\t\tsql: `WHERE ${where}`,\n\t\t\tparameterOperators: paramOps,\n\t\t}\n\t}\n\n\t// Handle RawValue tuple format\n\tif (\n\t\tArray.isArray(where) &&\n\t\twhere.length === 3 &&\n\t\t!LOGICAL_OPERATORS.includes(where[1] as (typeof LOGICAL_OPERATORS)[number])\n\t) {\n\t\tconst [column, operator, value] = where\n\t\treturn {\n\t\t\tsql: `WHERE ${column} ${operator} ${(value as RawValue).value}`,\n\t\t\tparameterOperators: [],\n\t\t}\n\t}\n\n\t// Handle array case\n\tconst conditions = where\n\t\t.map((part, i) => {\n\t\t\tif (i % 2 === 0) {\n\t\t\t\tif (Array.isArray(part)) {\n\t\t\t\t\t// Handle RawValue condition\n\t\t\t\t\tconst [column, operator, value] = part\n\t\t\t\t\treturn `${column} ${operator} ${value.value}`\n\t\t\t\t}\n\t\t\t\tconst matches = (part as string).match(/\\$\\w+/g) || []\n\t\t\t\tparamOps.push(...matches)\n\t\t\t\t// Handle JSON operator\n\t\t\t\tif ((part as string).includes(\"->json\")) {\n\t\t\t\t\treturn (part as string).replace(/\\$([\\w]+)->json/, \"jsonb($$$1)\")\n\t\t\t\t}\n\t\t\t\treturn part\n\t\t\t}\n\t\t\treturn ` ${part} `\n\t\t})\n\t\t.join(\"\")\n\n\treturn {\n\t\tsql: `WHERE ${conditions}`,\n\t\tparameterOperators: paramOps,\n\t}\n}\n",
"metadata": {
"size": 3963,
"modified": 1738725761196.5874,
"extension": ".ts",
"mimeType": "application/typescript",
"language": "typescript",
"encoding": "utf8"
}
}
}
}