ReviewOS

also looking at this

stacks/bun-query-builder

refactor(types): stop publishing `any` across the query builder's surface

#1117
Merged chris wants to merge types/remove-any-from-public-surface into main
13 files +359 -110
packages/bun-query-builder/src/db.tsmodified+63-23
Changes to packages/bun-query-builder/src/db.ts
@@ -1,4 +1,5 @@
11import type { DatabaseConfig, PoolConfig, SupportedDialect } from './types'
2import type { SQLQueryBindings } from 'bun:sqlite'
23import { SQL } from 'bun'
34
45/**
@@ -6,6 +7,42 @@ import { SQL } from 'bun'
67 * Both Bun's native `SQL` query and our `createSQLiteSQL` wrapper satisfy this.
78 * See stacksjs/bun-query-builder#1044.
89 */
10/**
11 * What a value interpolated into a tagged template can turn out to be.
12 *
13 * Three things arrive here: a plain bind param, a nested query (carries `sql`
14 * plus a `values` ARRAY), and a raw-SQL marker (carries `__raw`/`raw` and no
15 * `values`). Naming the shape is what lets the branches below read properties
16 * without an `any` - and the order of those branches is load-bearing, see
17 * stacksjs/bun-query-builder#1084.
18 */
19export interface EmbeddedValue {
20 sql?: unknown
21 values?: unknown
22 __raw?: unknown
23 raw?: unknown
24 value?: unknown
25}
26
27/**
28 * The one place user values become driver bindings.
29 *
30 * Everything a caller interpolates arrives as `unknown` - that is honest, we
31 * do not know what it is. bun:sqlite accepts a narrower set. The conversion is
32 * unchecked because the driver itself is the validator: it throws a clear
33 * "can't bind" for anything it cannot take, and a check here would only
34 * duplicate that rule and drift from it. Kept as one named function so the
35 * boundary is visible, rather than an `any` at each of the six call sites.
36 */
37export function toBindings(params: readonly unknown[]): SQLQueryBindings[] {
38 return params as SQLQueryBindings[]
39}
40
41/** A template value seen as a possible fragment; null when it is a plain bind param. */
42export function asEmbeddedValue(value: unknown): EmbeddedValue | null {
43 return value !== null && typeof value === 'object' ? value as EmbeddedValue : null
44}
45
946export interface DriverQuery {
1047 execute: () => Promise<any>
1148 values?: () => any
renamed throughout · 14 hidden lines
Changes to packages/bun-query-builder/src/db.ts
@@ -30,11 +67,11 @@ export type AwaitableDriverQuery = DriverQuery & PromiseLike<any> renamed throughout
3067 */
3168export interface DriverConnection {
3269 /** Tagged-template form: `` sql`SELECT 1` ``. */
33 (strings: TemplateStringsArray, ...values: any[]): DriverQuery
70 (strings: TemplateStringsArray, ...values: unknown[]): DriverQuery
3471 /** Function form for raw identifiers / value helpers: `sql('col')`. */
3572 (value: any): any
36 unsafe: (sql: string, values?: any[]) => AwaitableDriverQuery
37 query?: (sql: string, params?: any[]) => any
73 unsafe: (sql: string, values?: unknown[]) => AwaitableDriverQuery
74 query?: (sql: string, params?: unknown[]) => any
3875 close?: () => Promise<void> | void
3976 _prepareStatement?: (sql: string) => any
4077 [key: string]: any
Changes to packages/bun-query-builder/src/db.ts
@@ -74,11 +111,11 @@ import { applySqliteBootstrapPragmas } from './sqlite-pragmas'
74111 * numbered placeholders is returned untouched, so `?` queries and Bun's own
75112 * tagged-template output are unaffected.
76113 */
77export function bindNumberedPlaceholders(sql: string, params: any[]): { sql: string, params: any[] } {
114export function bindNumberedPlaceholders<T>(sql: string, params: T[]): { sql: string, params: T[] } {
78115 if (params.length === 0 || !/\$\d/.test(sql))
79116 return { sql, params }
80117
81 const out: any[] = []
118 const out: T[] = []
82119 let rewritten = ''
83120 let highest = 0
84121
@@ -185,7 +222,7 @@ class SQLiteWrapper {
185222 * Execute a query with parameters and return results.
186223 * This mimics the SQL tagged template literal behavior.
187224 */
188 query(sql: string, params: any[] = []): any[] {
225 query(sql: string, params: SQLQueryBindings[] = []): any[] {
189226 const bound = bindNumberedPlaceholders(sql, params)
190227 const stmt = this.db.prepare(bound.sql)
191228 return stmt.all(...bound.params)
@@ -194,7 +231,7 @@ class SQLiteWrapper {
194231 /**
195232 * Execute a query that doesn't return results (INSERT, UPDATE, DELETE).
196233 */
197 run(sql: string, params: any[] = []): any {
234 run(sql: string, params: SQLQueryBindings[] = []): any {
198235 const bound = bindNumberedPlaceholders(sql, params)
199236 const stmt = this.db.prepare(bound.sql)
200237 return stmt.run(...bound.params)
renamed throughout · 12 hidden lines
Changes to packages/bun-query-builder/src/db.ts
@@ -364,10 +401,10 @@ function createSQLiteSQL(filename: string): SQL { renamed throughout
364401 /**
365402 * Process a tagged template literal and return a query object
366403 */
367 function processTaggedTemplate(strings: TemplateStringsArray, ...values: any[]): any {
404 function processTaggedTemplate(strings: TemplateStringsArray, ...values: unknown[]): any {
368405 // Build the SQL string with placeholders
369406 let sql = strings[0]
370 const params: any[] = []
407 const params: unknown[] = []
371408
372409 for (let i = 0; i < values.length; i++) {
373410 const value = values[i]
Changes to packages/bun-query-builder/src/db.ts
@@ -396,13 +433,16 @@ function createSQLiteSQL(filename: string): SQL {
396433 // The discriminator is `values` being an ARRAY: raw markers carry
397434 // `__raw` and no `values`, and Bun's native query objects expose `values`
398435 // as a method, so neither is captured here.
399 if (value && typeof value === 'object' && typeof value.sql === 'string' && Array.isArray(value.values)) {
400 sql += value.sql + (strings[i + 1] || '')
401 params.push(...value.values)
436 const embedded = asEmbeddedValue(value)
437
438 if (embedded && typeof embedded.sql === 'string' && Array.isArray(embedded.values)) {
439 sql += embedded.sql + (strings[i + 1] || '')
440 params.push(...embedded.values)
402441 }
403442 // Handle raw SQL markers (from sql('identifier') or sql.raw())
404 else if (value && typeof value === 'object' && (value.__raw || value.raw)) {
405 const rawValue = value.__raw ? value.value : (typeof value.raw === 'string' ? value.raw : value.raw())
443 else if (embedded && (embedded.__raw || embedded.raw)) {
444 const raw = embedded.raw
445 const rawValue = embedded.__raw ? embedded.value : (typeof raw === 'string' ? raw : (raw as () => string)())
406446 sql += rawValue + (strings[i + 1] || '')
407447 }
408448 // Handle arrays - expand to placeholders
@@ -432,11 +472,11 @@ function createSQLiteSQL(filename: string): SQL {
432472 // caller silently loses its data.
433473 const trimmed = sql.trim().toUpperCase()
434474 if (trimmed.startsWith('SELECT') || trimmed.startsWith('PRAGMA') || /\bRETURNING\b/.test(trimmed)) {
435 const result = wrapper.query(sql, params)
475 const result = wrapper.query(sql, toBindings(params))
436476 return Promise.resolve(result)
437477 }
438478 else {
439 const result = wrapper.run(sql, params)
479 const result = wrapper.run(sql, toBindings(params))
440480 return Promise.resolve(result)
441481 }
442482 }
@@ -455,7 +495,7 @@ function createSQLiteSQL(filename: string): SQL {
455495 * - Tagged template literals: sql`SELECT * FROM users`
456496 * - Function calls for raw identifiers: sql('column_name')
457497 */
458 function sqlFunction(stringsOrValue: TemplateStringsArray | string, ...values: any[]): any {
498 function sqlFunction(stringsOrValue: TemplateStringsArray | string, ...values: unknown[]): any {
459499 // If called with a template literal (array of strings)
460500 if (Array.isArray(stringsOrValue) && 'raw' in stringsOrValue) {
461501 return processTaggedTemplate(stringsOrValue as TemplateStringsArray, ...values)
@@ -475,7 +515,7 @@ function createSQLiteSQL(filename: string): SQL {
475515 sqlFunction.raw = (str: string) => createRawMarker(str)
476516
477517 // Add .unsafe() method for raw SQL with parameters (like Bun's SQL.unsafe)
478 sqlFunction.unsafe = (sql: string, params: any[] = []) => {
518 sqlFunction.unsafe = (sql: string, params: unknown[] = []) => {
479519 const execute = (): Promise<any> => {
480520 try {
481521 // A RETURNING clause turns an INSERT/UPDATE/DELETE into a row producer;
@@ -483,11 +523,11 @@ function createSQLiteSQL(filename: string): SQL {
483523 // `.run()`'s { changes, lastInsertRowid }. See the tagged-template path.
484524 const trimmed = sql.trim().toUpperCase()
485525 if (trimmed.startsWith('SELECT') || trimmed.startsWith('PRAGMA') || /\bRETURNING\b/.test(trimmed)) {
486 const result = wrapper.query(sql, params)
526 const result = wrapper.query(sql, toBindings(params))
487527 return Promise.resolve(result)
488528 }
489529 else {
490 const result = wrapper.run(sql, params)
530 const result = wrapper.run(sql, toBindings(params))
491531 return Promise.resolve(result)
492532 }
493533 }
@@ -522,9 +562,9 @@ function createSQLiteSQL(filename: string): SQL {
522562 }
523563
524564 // Add .query() method for direct SQL execution
525 sqlFunction.query = (sql: string, params?: any[]) => {
565 sqlFunction.query = (sql: string, params?: unknown[]) => {
526566 try {
527 const result = wrapper.query(sql, params || [])
567 const result = wrapper.query(sql, toBindings(params || []))
528568 return Promise.resolve(result)
529569 }
530570 catch (error) {
@@ -533,7 +573,7 @@ function createSQLiteSQL(filename: string): SQL {
533573 }
534574
535575 // Add .file() method for executing SQL from a file
536 sqlFunction.file = async (filePath: string, _params?: any[]) => {
576 sqlFunction.file = async (filePath: string, _params?: unknown[]) => {
537577 try {
538578 const { readFileSync } = await import('node:fs')
539579 const sqlContent = readFileSync(filePath, 'utf-8')