@pineliner/odb-client 1.0.6 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,660 @@
1
+ /**
2
+ * SQL Template Tag Converter - postgres.js Compatible
3
+ *
4
+ * Provides postgres.js-style SQL template literals and helper methods
5
+ * for safe, composable SQL query building.
6
+ *
7
+ * ## Basic Usage
8
+ *
9
+ * ```typescript
10
+ * import { getConnection, sql } from '@pineliner/odb-client'
11
+ *
12
+ * const db = await getConnection('tenant-id')
13
+ *
14
+ * // Simple query with parameters
15
+ * const users = await db`SELECT * FROM users WHERE id = ${userId}`
16
+ *
17
+ * // Use sql() helper for dynamic parts
18
+ * const columns = ['name', 'email', 'age']
19
+ * const result = await db`SELECT ${sql(columns)} FROM users`
20
+ * ```
21
+ *
22
+ * ## Supported Patterns (postgres.js Compatible)
23
+ *
24
+ * ### 1. Query Parameters (Basic Template Literals)
25
+ * ```typescript
26
+ * // Parameters are automatically escaped
27
+ * await db`SELECT * FROM users WHERE name = ${name} AND age > ${age}`
28
+ * // → SELECT * FROM users WHERE name = ? AND age = ? [name, age]
29
+ * ```
30
+ *
31
+ * ### 2. Dynamic Column Selection
32
+ * ```typescript
33
+ * const columns = ['name', 'age']
34
+ *
35
+ * await db`
36
+ * SELECT ${sql(columns)}
37
+ * FROM users
38
+ * `
39
+ * // → SELECT name, age FROM users
40
+ * ```
41
+ *
42
+ * ### 3. Dynamic Inserts (Single Object)
43
+ * ```typescript
44
+ * const user = {
45
+ * name: 'Murray',
46
+ * age: 68
47
+ * }
48
+ *
49
+ * await db`INSERT INTO users ${sql(user, 'name', 'age')}`
50
+ * // → INSERT INTO users (name, age) VALUES (?, ?)
51
+ * // ["Murray", 68]
52
+ *
53
+ * // Columns can also be given with an array
54
+ * const columns = ['name', 'age']
55
+ * await db`INSERT INTO users ${sql(user, columns)}`
56
+ * ```
57
+ *
58
+ * ### 4. Bulk Inserts (Array of Objects)
59
+ * ```typescript
60
+ * const users = [
61
+ * { name: 'Murray', age: 68, garbage: 'ignore' },
62
+ * { name: 'Walter', age: 80 }
63
+ * ]
64
+ *
65
+ * await db`INSERT INTO users ${sql(users, 'name', 'age')}`
66
+ * // → INSERT INTO users (name, age) VALUES (?, ?), (?, ?)
67
+ * // ["Murray", 68, "Walter", 80]
68
+ *
69
+ * // Omit column names to use all object keys
70
+ * await db`INSERT INTO users ${sql(users)}`
71
+ * // → INSERT INTO users (name, age, garbage) VALUES (?, ?, ?), (?, ?, ?)
72
+ * ```
73
+ *
74
+ * ### 5. Dynamic Column Updates
75
+ * ```typescript
76
+ * const user = {
77
+ * id: 1,
78
+ * name: 'Murray',
79
+ * age: 68
80
+ * }
81
+ *
82
+ * await db`
83
+ * UPDATE users
84
+ * SET ${set(user, 'name', 'age')}
85
+ * WHERE user_id = ${user.id}
86
+ * `
87
+ * // → UPDATE users SET name = ?, age = ? WHERE user_id = ?
88
+ * // ["Murray", 68, 1]
89
+ *
90
+ * // Or using set() helper
91
+ * await db`
92
+ * UPDATE users
93
+ * SET ${set({ name: user.name, age: user.age })}
94
+ * WHERE user_id = ${user.id}
95
+ * `
96
+ * ```
97
+ *
98
+ * ### 6. Multiple Updates in One Query
99
+ * ```typescript
100
+ * const users = [
101
+ * [1, 'John', 34],
102
+ * [2, 'Jane', 27],
103
+ * ]
104
+ *
105
+ * await db`
106
+ * UPDATE users
107
+ * SET name = update_data.name, age = (update_data.age)::int
108
+ * FROM (VALUES ${sql(users)}) AS update_data (id, name, age)
109
+ * WHERE users.id = (update_data.id)::int
110
+ * RETURNING users.id, users.name, users.age
111
+ * `
112
+ * // → UPDATE users SET ... FROM (VALUES (?, ?, ?), (?, ?, ?)) AS update_data ...
113
+ * // [1, "John", 34, 2, "Jane", 27]
114
+ * ```
115
+ *
116
+ * ### 7. Dynamic WHERE IN Clause
117
+ * ```typescript
118
+ * const users = await db`
119
+ * SELECT *
120
+ * FROM users
121
+ * WHERE age IN ${sql([68, 75, 23])}
122
+ * `
123
+ * // → SELECT * FROM users WHERE age IN (?, ?, ?)
124
+ * // [68, 75, 23]
125
+ *
126
+ * // Or use array directly in template
127
+ * await db`SELECT * FROM users WHERE age IN ${[68, 75, 23]}`
128
+ * ```
129
+ *
130
+ * ### 8. Dynamic Values in SELECT
131
+ * ```typescript
132
+ * const [{ a, b, c }] = await db`
133
+ * SELECT *
134
+ * FROM (VALUES ${sql(['a', 'b', 'c'])}) AS x(a, b, c)
135
+ * `
136
+ * // Note: For VALUES with string literals, use the array directly or wrap in sql()
137
+ * ```
138
+ *
139
+ * ### 9. Conditional Fragments
140
+ * ```typescript
141
+ * const isAdmin = true
142
+ * const minAge = 25
143
+ *
144
+ * await db`
145
+ * SELECT * FROM users
146
+ * WHERE is_active = 1
147
+ * ${isAdmin ? fragment`AND role = 'admin'` : empty()}
148
+ * ${minAge ? fragment`AND age >= ${minAge}` : empty()}
149
+ * `
150
+ * // → SELECT * FROM users WHERE is_active = 1 AND role = 'admin' AND age >= ?
151
+ * // [25]
152
+ * ```
153
+ *
154
+ * ### 10. Transactions
155
+ * ```typescript
156
+ * await db.begin(async tx => {
157
+ * await tx`INSERT INTO users ${sql(user, 'name', 'email')}`
158
+ * await tx`UPDATE accounts SET balance = balance - ${amount} WHERE user_id = ${userId}`
159
+ * })
160
+ * // Automatically commits on success, rolls back on error
161
+ * ```
162
+ *
163
+ * ### 11. Raw SQL (use with caution)
164
+ * ```typescript
165
+ * const tableName = 'users' // From trusted source only!
166
+ * await db`SELECT * FROM ${raw(tableName)} WHERE id = ${userId}`
167
+ * // → SELECT * FROM users WHERE id = ?
168
+ * // [userId]
169
+ * ```
170
+ *
171
+ * ### 12. Reusable Fragments
172
+ * ```typescript
173
+ * const activeUsers = fragment`is_active = 1 AND is_deleted = 0`
174
+ * const olderThan = (age: number) => fragment`age > ${age}`
175
+ *
176
+ * await db`
177
+ * SELECT * FROM users
178
+ * WHERE ${activeUsers}
179
+ * AND ${olderThan(25)}
180
+ * `
181
+ * ```
182
+ *
183
+ * ## API Reference
184
+ *
185
+ * - `sql(columns: string[])` - Dynamic column list for SELECT
186
+ * - `sql(object, ...keys)` - INSERT format from single object
187
+ * - `sql(objects[], ...keys)` - Bulk INSERT from array of objects
188
+ * - `sql(array[][])` - Bulk insert values (2D array)
189
+ * - `fragment` `` ` - Template tag for reusable query fragments
190
+ * - `raw(string)` - Unescaped SQL fragment (dangerous!)
191
+ * - `empty()` - Empty SQL fragment for conditionals
192
+ * - `set(object)` - Generate SET clause for UPDATE
193
+ * - `where(object)` - Generate WHERE clause with AND conditions
194
+ * - `join(fragments[], separator)` - Combine multiple fragments
195
+ */
196
+
197
+ export interface SqlQuery {
198
+ sql: string
199
+ args: any[]
200
+ }
201
+
202
+ export interface SqlFragment {
203
+ _isSqlFragment: true
204
+ sql: string
205
+ args: any[]
206
+ }
207
+
208
+ /**
209
+ * Convert template literal to parameterized query
210
+ *
211
+ * @param strings - Template literal strings
212
+ * @param values - Template literal values
213
+ * @returns Object with sql string and args array
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * const query = convertTemplateToQuery(
218
+ * ['SELECT * FROM users WHERE id = ', ' AND name = ', ''],
219
+ * [userId, userName]
220
+ * )
221
+ * // → { sql: "SELECT * FROM users WHERE id = ? AND name = ?", args: [userId, userName] }
222
+ * ```
223
+ */
224
+ export function convertTemplateToQuery(
225
+ strings: TemplateStringsArray,
226
+ values: any[]
227
+ ): SqlQuery {
228
+ let sql = ''
229
+ const args: any[] = []
230
+
231
+ for (let i = 0; i < strings.length; i++) {
232
+ sql += strings[i]
233
+
234
+ if (i < values.length) {
235
+ const value = values[i]
236
+
237
+ // Handle SQL fragments (from sql`...` or sql())
238
+ if (isSqlFragment(value)) {
239
+ sql += value.sql
240
+ args.push(...value.args)
241
+ }
242
+ // Handle arrays for IN clauses or bulk inserts
243
+ else if (Array.isArray(value)) {
244
+ // Check if it's a 2D array (bulk insert)
245
+ if (value.length > 0 && Array.isArray(value[0])) {
246
+ // Bulk insert: [[1, 'John'], [2, 'Jane']]
247
+ const rowPlaceholders = value.map(row =>
248
+ `(${row.map(() => '?').join(', ')})`
249
+ ).join(', ')
250
+ sql += rowPlaceholders
251
+ args.push(...value.flat())
252
+ } else {
253
+ // Simple array for IN clause: [1, 2, 3]
254
+ const placeholders = value.map(() => '?').join(', ')
255
+ sql += `(${placeholders})`
256
+ args.push(...value)
257
+ }
258
+ }
259
+ // Regular parameterized value
260
+ else {
261
+ sql += '?'
262
+ args.push(value)
263
+ }
264
+ }
265
+ }
266
+
267
+ return { sql, args }
268
+ }
269
+
270
+ /**
271
+ * Check if value is a SQL fragment
272
+ */
273
+ function isSqlFragment(value: any): value is SqlFragment {
274
+ return value && typeof value === 'object' && value._isSqlFragment === true
275
+ }
276
+
277
+ /**
278
+ * sql() helper - postgres.js compatible
279
+ *
280
+ * Multi-purpose SQL helper that adapts based on arguments.
281
+ *
282
+ * @param value - Array, object, array of objects, or 2D array
283
+ * @param keys - Optional keys (string[] or ...string)
284
+ * @returns SQL fragment
285
+ *
286
+ * @example Dynamic column selection
287
+ * ```typescript
288
+ * sql(['name', 'email', 'age'])
289
+ * // → "name, email, age"
290
+ * ```
291
+ *
292
+ * @example Single object INSERT
293
+ * ```typescript
294
+ * sql({ name: 'John', age: 30 }, 'name', 'age')
295
+ * // → "(name, age) VALUES (?, ?)" with args ["John", 30]
296
+ * ```
297
+ *
298
+ * @example Single object UPDATE
299
+ * ```typescript
300
+ * sql({ name: 'John', age: 30 }, 'name', 'age')
301
+ * // Context determines if INSERT or UPDATE format
302
+ * // For UPDATE: "name" = ?, "age" = ? with args ["John", 30]
303
+ * // For INSERT: (name, age) VALUES (?, ?) with args ["John", 30]
304
+ * ```
305
+ *
306
+ * @example Array of objects (bulk insert)
307
+ * ```typescript
308
+ * sql([{ name: 'John', age: 30 }, { name: 'Jane', age: 25 }], 'name', 'age')
309
+ * // → "(name, age) VALUES (?, ?), (?, ?)" with args ["John", 30, "Jane", 25]
310
+ * ```
311
+ *
312
+ * @example 2D array (bulk insert values)
313
+ * ```typescript
314
+ * sql([[1, 'John'], [2, 'Jane']])
315
+ * // → "(?, ?), (?, ?)" with args [1, "John", 2, "Jane"]
316
+ * ```
317
+ *
318
+ * @example VALUES clause
319
+ * ```typescript
320
+ * sql(['a', 'b', 'c'])
321
+ * // In SELECT context: "a, b, c"
322
+ * // In VALUES context: You'd use the array directly in template
323
+ * ```
324
+ */
325
+ export function sql(
326
+ value: any,
327
+ ...keys: (string | string[])[]
328
+ ): SqlFragment {
329
+ // Normalize keys - accept both sql(obj, 'a', 'b') and sql(obj, ['a', 'b'])
330
+ let columnKeys: string[] = []
331
+ if (keys.length > 0) {
332
+ if (Array.isArray(keys[0])) {
333
+ columnKeys = keys[0]
334
+ } else {
335
+ columnKeys = keys as string[]
336
+ }
337
+ }
338
+
339
+ // Pattern 0: Single string → raw identifier (column/table name)
340
+ if (typeof value === 'string' && columnKeys.length === 0) {
341
+ return {
342
+ _isSqlFragment: true,
343
+ sql: value,
344
+ args: []
345
+ }
346
+ }
347
+
348
+ // Pattern 1: Array of strings → column list for SELECT
349
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'string') {
350
+ return {
351
+ _isSqlFragment: true,
352
+ sql: value.join(', '),
353
+ args: []
354
+ }
355
+ }
356
+
357
+ // Pattern 2: Array of objects with keys → bulk insert
358
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' && !Array.isArray(value[0])) {
359
+ if (columnKeys.length === 0) {
360
+ // Use keys from first object
361
+ columnKeys = Object.keys(value[0])
362
+ }
363
+
364
+ // Extract values in column order for each object
365
+ const allValues: any[] = []
366
+ const rowPlaceholders: string[] = []
367
+
368
+ for (const obj of value) {
369
+ const rowValues = columnKeys.map(key => obj[key])
370
+ allValues.push(...rowValues)
371
+ rowPlaceholders.push(`(${columnKeys.map(() => '?').join(', ')})`)
372
+ }
373
+
374
+ return {
375
+ _isSqlFragment: true,
376
+ sql: `(${columnKeys.join(', ')}) VALUES ${rowPlaceholders.join(', ')}`,
377
+ args: allValues
378
+ }
379
+ }
380
+
381
+ // Pattern 3: Single object → INSERT or UPDATE format
382
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
383
+ // If no keys specified, use all keys from the object
384
+ if (columnKeys.length === 0) {
385
+ columnKeys = Object.keys(value)
386
+ }
387
+
388
+ const values = columnKeys.map(key => value[key])
389
+ const placeholders = columnKeys.map(() => '?').join(', ')
390
+
391
+ // Return INSERT format - context will determine usage
392
+ // For UPDATE, use set() helper instead
393
+ return {
394
+ _isSqlFragment: true,
395
+ sql: `(${columnKeys.join(', ')}) VALUES (${placeholders})`,
396
+ args: values
397
+ }
398
+ }
399
+
400
+ // Pattern 4: 2D Array → bulk insert values (raw)
401
+ if (Array.isArray(value) && value.length > 0 && Array.isArray(value[0])) {
402
+ const rowPlaceholders = value.map(row =>
403
+ `(${row.map(() => '?').join(', ')})`
404
+ ).join(', ')
405
+
406
+ return {
407
+ _isSqlFragment: true,
408
+ sql: rowPlaceholders,
409
+ args: value.flat()
410
+ }
411
+ }
412
+
413
+ // Pattern 5: Empty fragment (no arguments or empty array)
414
+ return {
415
+ _isSqlFragment: true,
416
+ sql: '',
417
+ args: []
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Create an empty SQL fragment
423
+ *
424
+ * Useful for conditional queries where you want to add nothing.
425
+ *
426
+ * @returns Empty SQL fragment
427
+ *
428
+ * @example
429
+ * ```typescript
430
+ * const isFiltered = false
431
+ * await db`
432
+ * SELECT * FROM users
433
+ * WHERE is_active = 1
434
+ * ${isFiltered ? sql`AND age > 25` : empty()}
435
+ * `
436
+ * ```
437
+ */
438
+ export function empty(): SqlFragment {
439
+ return {
440
+ _isSqlFragment: true,
441
+ sql: '',
442
+ args: []
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Create a raw SQL fragment (use with extreme caution!)
448
+ *
449
+ * This bypasses parameterization and inserts the string directly into SQL.
450
+ * **WARNING**: Can lead to SQL injection if used with untrusted input!
451
+ *
452
+ * Only use for:
453
+ * - Table/column names from trusted sources
454
+ * - SQL keywords or operators
455
+ * - Pre-validated identifiers
456
+ *
457
+ * @param value - Raw SQL string (must be from trusted source)
458
+ * @returns SQL fragment with no parameters
459
+ *
460
+ * @example Safe usage (trusted table name)
461
+ * ```typescript
462
+ * const tableName = 'users' // From config, not user input!
463
+ * await db`SELECT * FROM ${raw(tableName)} WHERE id = ${userId}`
464
+ * ```
465
+ *
466
+ * @example UNSAFE - Never do this!
467
+ * ```typescript
468
+ * // ❌ DANGEROUS - SQL injection vulnerability!
469
+ * const userInput = req.query.table // From user!
470
+ * await db`SELECT * FROM ${raw(userInput)}` // Don't do this!
471
+ * ```
472
+ */
473
+ export function raw(value: string): SqlFragment {
474
+ return {
475
+ _isSqlFragment: true,
476
+ sql: value,
477
+ args: []
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Create a reusable SQL fragment from template literal
483
+ *
484
+ * Useful for building composable query parts that can be reused.
485
+ *
486
+ * @param strings - Template literal strings
487
+ * @param values - Template literal values
488
+ * @returns SQL fragment that can be embedded in other queries
489
+ *
490
+ * @example Basic fragment
491
+ * ```typescript
492
+ * const activeUsers = fragment`is_active = 1 AND is_deleted = 0`
493
+ *
494
+ * await db`SELECT * FROM users WHERE ${activeUsers}`
495
+ * await db`SELECT COUNT(*) FROM users WHERE ${activeUsers}`
496
+ * ```
497
+ *
498
+ * @example Parameterized fragment
499
+ * ```typescript
500
+ * const olderThan = (age: number) => fragment`age > ${age}`
501
+ * const youngerThan = (age: number) => fragment`age < ${age}`
502
+ *
503
+ * await db`
504
+ * SELECT * FROM users
505
+ * WHERE ${olderThan(25)} AND ${youngerThan(65)}
506
+ * `
507
+ * ```
508
+ *
509
+ * @example Complex reusable conditions
510
+ * ```typescript
511
+ * const validUser = fragment`
512
+ * is_active = 1
513
+ * AND is_deleted = 0
514
+ * AND is_verified = 1
515
+ * AND banned_at IS NULL
516
+ * `
517
+ *
518
+ * const adminUser = fragment`${validUser} AND role = 'admin'`
519
+ *
520
+ * await db`SELECT * FROM users WHERE ${adminUser}`
521
+ * ```
522
+ */
523
+ export function fragment(
524
+ strings: TemplateStringsArray,
525
+ ...values: any[]
526
+ ): SqlFragment {
527
+ const query = convertTemplateToQuery(strings, values)
528
+ return {
529
+ _isSqlFragment: true,
530
+ ...query
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Combine multiple SQL fragments
536
+ *
537
+ * @param fragments - Array of SQL fragments
538
+ * @param separator - Optional separator (default: ', ')
539
+ * @returns Combined SQL fragment
540
+ *
541
+ * @example
542
+ * ```typescript
543
+ * const conditions = [
544
+ * sql`age > ${25}`,
545
+ * sql`country = ${'US'}`,
546
+ * sql`is_active = 1`
547
+ * ]
548
+ *
549
+ * await db`
550
+ * SELECT * FROM users
551
+ * WHERE ${join(conditions, ' AND ')}
552
+ * `
553
+ * ```
554
+ */
555
+ export function join(fragments: SqlFragment[], separator: string = ', '): SqlFragment {
556
+ if (fragments.length === 0) {
557
+ return empty()
558
+ }
559
+
560
+ const sql = fragments.map(f => f.sql).join(separator)
561
+ const args = fragments.flatMap(f => f.args)
562
+
563
+ return {
564
+ _isSqlFragment: true,
565
+ sql,
566
+ args
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Create SET clause for UPDATE queries
572
+ *
573
+ * @param data - Object with column: value pairs
574
+ * @param keys - Optional array of keys to include (filters the object)
575
+ * @returns SQL fragment for SET clause
576
+ *
577
+ * @example All keys
578
+ * ```typescript
579
+ * const updates = { name: 'John Doe', age: 31, updated_at: new Date() }
580
+ *
581
+ * await db`
582
+ * UPDATE users
583
+ * SET ${set(updates)}
584
+ * WHERE id = ${userId}
585
+ * `
586
+ * // → UPDATE users SET name = ?, age = ?, updated_at = ? WHERE id = ?
587
+ * ```
588
+ *
589
+ * @example Specific keys only
590
+ * ```typescript
591
+ * const user = { id: 1, name: 'Murray', age: 68 }
592
+ *
593
+ * await db`
594
+ * UPDATE users
595
+ * SET ${set(user, 'name', 'age')}
596
+ * WHERE user_id = ${user.id}
597
+ * `
598
+ * // → UPDATE users SET name = ?, age = ? WHERE user_id = ?
599
+ * ```
600
+ */
601
+ export function set(
602
+ data: Record<string, any>,
603
+ ...keys: string[]
604
+ ): SqlFragment {
605
+ let columns: string[]
606
+
607
+ if (keys.length > 0) {
608
+ // Use only specified keys
609
+ columns = keys
610
+ } else {
611
+ // Use all keys from object
612
+ columns = Object.keys(data)
613
+ }
614
+
615
+ if (columns.length === 0) {
616
+ return empty()
617
+ }
618
+
619
+ const setClauses = columns.map(key => `${key} = ?`)
620
+ const values = columns.map(key => data[key])
621
+
622
+ return {
623
+ _isSqlFragment: true,
624
+ sql: setClauses.join(', '),
625
+ args: values
626
+ }
627
+ }
628
+
629
+ /**
630
+ * Create WHERE clause from object (AND conditions)
631
+ *
632
+ * @param conditions - Object with column: value pairs
633
+ * @returns SQL fragment for WHERE clause
634
+ *
635
+ * @example
636
+ * ```typescript
637
+ * const filters = { is_active: 1, role: 'admin', age: 30 }
638
+ *
639
+ * await db`
640
+ * SELECT * FROM users
641
+ * WHERE ${where(filters)}
642
+ * `
643
+ * // → SELECT * FROM users WHERE is_active = ? AND role = ? AND age = ?
644
+ * ```
645
+ */
646
+ export function where(conditions: Record<string, any>): SqlFragment {
647
+ const entries = Object.entries(conditions)
648
+ if (entries.length === 0) {
649
+ return empty()
650
+ }
651
+
652
+ const whereClauses = entries.map(([key]) => `${key} = ?`)
653
+ const values = entries.map(([, value]) => value)
654
+
655
+ return {
656
+ _isSqlFragment: true,
657
+ sql: whereClauses.join(' AND '),
658
+ args: values
659
+ }
660
+ }
@@ -28,12 +28,21 @@ export interface QueryResult<T = any> {
28
28
  /**
29
29
  * Unified database connection interface
30
30
  * All backends implement this through adapters
31
+ *
32
+ * postgres.js-compatible API:
33
+ * - sql`...` returns rows array directly (not QueryResult)
34
+ * - sql() can be called as helper function for fragments
35
+ * - sql.empty(), sql.raw(), etc. are available as helper methods
31
36
  */
32
37
  export interface Connection {
33
- // Template tag queries (postgres.js-like)
34
- sql<T = any>(strings: TemplateStringsArray, ...values: any[]): Promise<QueryResult<T>>
38
+ // Template tag queries (postgres.js-compatible)
39
+ // Returns rows array directly, not { rows: [...] }
40
+ sql<T = any>(strings: TemplateStringsArray, ...values: any[]): Promise<T[]>
35
41
 
36
- // Standard query methods (same as execute)
42
+ // Helper function overload (for fragments like sql(['col1', 'col2']))
43
+ sql(value: any, ...keys: string[]): any
44
+
45
+ // Standard query methods (return QueryResult)
37
46
  query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>>
38
47
  execute(sql: string | { sql: string; args?: any[] }, params?: any[]): Promise<QueryResult>
39
48
  prepare(sql: string): PreparedStatement
@@ -45,6 +54,9 @@ export interface Connection {
45
54
  // Connection management
46
55
  close(): Promise<void>
47
56
 
57
+ // ORM support (optional)
58
+ createORM?(): any
59
+
48
60
  // Metadata (optional, ODB-Lite specific)
49
61
  databaseHash?: string
50
62
  databaseName?: string