@bakery-framework/orm 1.0.0

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.
package/src/pool.ts ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Connection pool settings for the MySQL and Postgres adapters.
3
+ *
4
+ * Nothing exposed these before: every deployment ran on Bun's defaults, which
5
+ * is fine until an app with more workers than the server has connection slots
6
+ * meets `FATAL: sorry, too many clients already`. `--threads N` makes that a
7
+ * realistic shape rather than a theoretical one — each worker opens its own
8
+ * pool, so the number that matters is `max x threads`.
9
+ *
10
+ * **SQLite ignores all of it**, and that is not an omission: a SQLite adapter
11
+ * is one file handle, and there is no pool to size.
12
+ */
13
+ export interface PoolOptions {
14
+ /** Maximum concurrent connections. Bun's default is 10. */
15
+ max?: number
16
+ /** Seconds an idle connection is kept before being closed. */
17
+ idleTimeout?: number
18
+ /** Seconds to wait for a connection before giving up. */
19
+ connectionTimeout?: number
20
+ /** Seconds after which a connection is retired and replaced. */
21
+ maxLifetime?: number
22
+ }
23
+
24
+ /**
25
+ * The env var behind each option.
26
+ *
27
+ * Environment rather than `server.config.ts` because pool size is a property
28
+ * of the *deployment*, not of the app: the same build runs with one connection
29
+ * on a laptop and forty in production, and `--threads N` multiplies whatever
30
+ * is set here per worker.
31
+ */
32
+ const ENV_KEYS: Record<keyof PoolOptions, string> = {
33
+ max: 'DB_POOL_MAX',
34
+ idleTimeout: 'DB_POOL_IDLE_TIMEOUT',
35
+ connectionTimeout: 'DB_POOL_CONNECTION_TIMEOUT',
36
+ maxLifetime: 'DB_POOL_MAX_LIFETIME',
37
+ }
38
+
39
+ /**
40
+ * Read pool options from the environment, dropping anything unusable.
41
+ *
42
+ * Unset stays unset — an option Bakery does not pass is one Bun defaults,
43
+ * which is different from passing Bun a zero. A non-numeric or negative value
44
+ * is dropped for the same reason: `DB_POOL_MAX=lots` must not become
45
+ * `max: NaN`, which Bun would take and then behave unpredictably around.
46
+ *
47
+ * **Only known keys are ever forwarded**, and that matters more than it looks:
48
+ * Bun accepts an unrecognised option silently — verified, `new SQL(url, {
49
+ * totallyNotAnOption: 1 })` constructs and queries fine — so a typo'd key
50
+ * would configure nothing and report nothing. Passing a fixed set means the
51
+ * typo lands in an env var name, where it is at least visible in one place.
52
+ */
53
+ export function poolOptionsFromEnv(
54
+ env: Record<string, string | undefined> = process.env,
55
+ ): PoolOptions {
56
+ const out: PoolOptions = {}
57
+ for (const [key, envKey] of Object.entries(ENV_KEYS) as [
58
+ keyof PoolOptions,
59
+ string,
60
+ ][]) {
61
+ const raw = env[envKey]
62
+ if (raw === undefined || raw === '') continue
63
+ const n = Number(raw)
64
+ if (!Number.isFinite(n) || n <= 0) continue
65
+ out[key] = n
66
+ }
67
+ return out
68
+ }
69
+
70
+ /**
71
+ * Merge pool options into the object handed to `new SQL()`.
72
+ *
73
+ * The timeouts are **seconds here and milliseconds inside Bun** — it multiplies
74
+ * by 1000 on the way in, verified by reading `sql.options` back: `idleTimeout:
75
+ * 5` is stored as `5000`. Seconds is what Bun's own documented unit is, so this
76
+ * passes them straight through rather than converting and doubling the factor.
77
+ */
78
+ export function withPoolOptions<T extends object>(
79
+ base: T,
80
+ pool: PoolOptions = poolOptionsFromEnv(),
81
+ ): T & PoolOptions {
82
+ return { ...base, ...pool }
83
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Type-level extension point connecting the framework to the app's schema.
3
+ *
4
+ * The dependency must point one way: the app depends on the framework, never
5
+ * the reverse. Yet the ORM's types are derived from the app's `schema.ts` —
6
+ * previously via a direct `import ... from '~/schema'`, which meant the
7
+ * framework's own types imported an app-owned, gitignored file, and a fresh
8
+ * clone could not typecheck its database layer.
9
+ *
10
+ * Instead, the app *registers* its schema here through declaration merging
11
+ * (the same pattern TanStack Router and vue-router use). At the bottom of
12
+ * `schema.ts`:
13
+ *
14
+ * ```ts
15
+ * declare module '@bakery-framework/orm/schema-registry' {
16
+ * interface SchemaRegistry {
17
+ * schema: {
18
+ * DBSchema: MyDBSchema
19
+ * DBOptionals: MyDBOptionals
20
+ * Views: DBInfo.Views
21
+ * }
22
+ * }
23
+ * }
24
+ * ```
25
+ *
26
+ * With no registration, every table and column falls back to permissive
27
+ * `any`-shaped records — the ORM stays fully usable, just untyped. The
28
+ * runtime needs no registration at all: schema *values* are loaded by path
29
+ * (see `sync/load.ts`), from `schema` in `server.config.ts` when the app sets
30
+ * one and otherwise from `<cwd>/orm/index.ts` or `<cwd>/schema.ts`, whichever
31
+ * exists. Absence is tolerated; only a *configured* path that does not exist
32
+ * is an error.
33
+ */
34
+
35
+ import type { MapOf } from '@bakery-framework/core/types'
36
+
37
+ // Augmented by the app; empty until then.
38
+ //
39
+ // **It has to be an `interface`, and the empty body is the point.** Biome's
40
+ // **`interface`, not `type`, and the empty body is the point.** A type alias
41
+ // cannot be declaration-merged, so rewriting this to `type X = {}` turns every
42
+ // app's `declare module '@bakery-framework/orm/schema-registry'` into
43
+ // `TS2300: Duplicate identifier` and the whole schema registry stops working.
44
+ //
45
+ // Biome's `noBannedTypes` proposed exactly that rewrite and called it a safe
46
+ // fix; the 2026-08-09 sweep applied it and `bun run typecheck` was the only
47
+ // thing that caught it. That rule is off repo-wide now — it had no true
48
+ // positives here — but `noEmptyInterface` reaches the same construct and
49
+ // offers the same fix, so the suppression stays. The compiler is the real
50
+ // guard: change this line and `apps/starter` fails to typecheck.
51
+ // biome-ignore lint/suspicious/noEmptyInterface: must stay mergeable — above
52
+ export interface SchemaRegistry {}
53
+
54
+ type Registered = SchemaRegistry extends { schema: infer S } ? S : never
55
+
56
+ /** Table map: `{ tableName: { column: type } }`. Permissive when unregistered. */
57
+ export type AppDBSchema = [Registered] extends [never]
58
+ ? MapOf<MapOf<any>>
59
+ : Registered extends { DBSchema: infer T }
60
+ ? T
61
+ : MapOf<MapOf<any>>
62
+
63
+ /** Per-table union of column names that have defaults (optional on insert). */
64
+ export type AppDBOptionals = [Registered] extends [never]
65
+ ? MapOf<any>
66
+ : Registered extends { DBOptionals: infer T }
67
+ ? T
68
+ : MapOf<any>
69
+
70
+ /** Union of table names that are views (excluded from mutation targets). */
71
+ export type AppViews = [Registered] extends [never]
72
+ ? never
73
+ : Registered extends { Views: infer T }
74
+ ? T
75
+ : never
@@ -0,0 +1,467 @@
1
+ import { Case } from '@bakery-framework/core/utils'
2
+ import { is, throws } from '@bakery-framework/core/utils/common'
3
+ import { quoteIdentifier } from './adapters/base'
4
+ import { getActiveDb } from './connection'
5
+ import type * as SyncTypes from './sync/types'
6
+
7
+ export type TypeMap = {
8
+ integer: number
9
+ number: number
10
+ string: string & {}
11
+ boolean: boolean
12
+ buffer: Buffer
13
+ }
14
+
15
+ export type DataTypes = keyof TypeMap
16
+
17
+ /**
18
+ * A column, described by what it *is* rather than by how it is spelled.
19
+ *
20
+ * TableDef<number> // NOT NULL, required on insert
21
+ * TableDef<string | null, true> // nullable
22
+ * TableDef<Status, false, true> // an enum, optional on insert
23
+ *
24
+ * Three parameters, where there were five (`type, default, nullable,
25
+ * autoIncrement, primary`). The two that went were positional booleans nothing
26
+ * outside `Field.Primary()` ever set, and `default` was carried only so
27
+ * nullability and optionality could be *derived* from it — which is why
28
+ * "defaulted but not nullable" and "nullable" were indistinguishable to
29
+ * anything downstream.
30
+ *
31
+ * **`T` is the row type, not a dialect name.** `number`, `string | null`, or an
32
+ * enum's own union — so `ExtractTableTypes` is a lookup rather than a mapping
33
+ * through `TypeMap`, and a type `TypeMap` cannot express (an enum, a branded
34
+ * id) needs no special case.
35
+ *
36
+ * At runtime this key holds the dialect discriminator (`'integer'`, `'string'`)
37
+ * that the adapters switch on. The two never meet: every adapter takes
38
+ * `def: unknown` and casts, and the sync engine has its own `ColumnConstraint`,
39
+ * bridged by the cast in `sync/load.ts`. That separation already existed and is
40
+ * what lets this change the schema-facing type without touching a single
41
+ * adapter, generated file, or stored ledger payload.
42
+ */
43
+ export type TableDef<
44
+ T,
45
+ N extends boolean = false,
46
+ O extends boolean = false,
47
+ > = {
48
+ type: T
49
+ nullable: N
50
+ optional: O
51
+ default?: unknown
52
+ length?: number
53
+ autoIncrement?: boolean
54
+ primary?: boolean
55
+ _enum?: readonly string[]
56
+ }
57
+
58
+ export const dateNow = '%dateNow%'
59
+
60
+ /**
61
+ * The row type of a column descriptor.
62
+ *
63
+ * Two forms reach here. A `Field.*` builder returns `TableDef<T, …>`, whose
64
+ * `type` **is** the row type, so this is a lookup. A hand-written literal —
65
+ * `{ type: 'integer', default: 0, nullable: true }`, the one shape `Field`
66
+ * does not spell — carries a dialect name there instead, which still goes
67
+ * through `TypeMap`.
68
+ *
69
+ * Distinguished by whether `type` is one of `TypeMap`'s keys. That is
70
+ * unambiguous because those keys are string *literals* and no row type is one:
71
+ * a column of literal type `'integer'` would be `Field.Enum(['integer'])`,
72
+ * whose `type` is the union, not the key.
73
+ */
74
+ type RowTypeOf<Col> = Col extends { type: infer T }
75
+ ? T extends keyof TypeMap
76
+ ? TypeMap[T] | (Col extends { nullable: true } ? null : never)
77
+ : T
78
+ : any
79
+
80
+ export type ExtractTableTypes<C, K extends keyof C> = {
81
+ [P in keyof C[K] as P extends '_view' ? never : P]: RowTypeOf<C[K][P]>
82
+ }
83
+
84
+ /**
85
+ * Which columns may be omitted from an `INSERT`.
86
+ *
87
+ * `optional: true` when a `Field.*` builder said so, which is the whole check
88
+ * for anything `Field` produces — it states optionality rather than leaving it
89
+ * to be reconstructed. The three derived conditions below it are the fallback
90
+ * for a hand-written literal, which has no `optional` key.
91
+ *
92
+ * Stating it also makes a shape expressible that derivation could not:
93
+ * **optional but neither nullable nor defaulted** — a column the database fills
94
+ * in, such as a generated key.
95
+ */
96
+ export type ExtractOptionals<C, T extends keyof C> = {
97
+ [K in keyof C[T]]: K extends '_view'
98
+ ? never
99
+ : C[T][K] extends { optional: true }
100
+ ? K
101
+ : C[T][K] extends { optional: false }
102
+ ? never
103
+ : C[T][K] extends { nullable: true }
104
+ ? K
105
+ : C[T][K] extends { default: string | number | boolean | null }
106
+ ? K
107
+ : C[T][K] extends { autoIncrement: true }
108
+ ? K
109
+ : never
110
+ }[keyof C[T]]
111
+
112
+ export type ExtractViews<C> = {
113
+ [K in keyof C]: C[K] extends { _view: string } ? K : never
114
+ }[keyof C]
115
+
116
+ export class ColumnRef<C extends string = string> {
117
+ constructor(public col: C) {}
118
+ }
119
+
120
+ export function col<C extends string>(name: C): ColumnRef<C> {
121
+ return new ColumnRef(name)
122
+ }
123
+
124
+ /**
125
+ * The only SQL function names that may be emitted into a query. `fnName` is
126
+ * interpolated rather than bound, so it must never come from request data.
127
+ */
128
+ export const SQL_FUNCTIONS = new Set([
129
+ 'ABS',
130
+ 'AVG',
131
+ 'COALESCE',
132
+ 'CONCAT',
133
+ 'COUNT',
134
+ 'LENGTH',
135
+ 'LOWER',
136
+ 'MAX',
137
+ 'MIN',
138
+ 'REPLACE',
139
+ 'SUBSTR',
140
+ 'SUM',
141
+ 'TRIM',
142
+ 'UPPER',
143
+ ])
144
+
145
+ /**
146
+ * Functions that are only meaningful over a window.
147
+ *
148
+ * Deliberately a second list rather than more entries in `SQL_FUNCTIONS`:
149
+ * `ROW_NUMBER()` outside an `OVER` clause is a syntax error on all three
150
+ * dialects, so putting it in the general list would let the builder emit SQL
151
+ * that cannot run. The aggregates in `SQL_FUNCTIONS` work in *both* positions
152
+ * and stay where they are — `evalOperands` accepts either list inside a window.
153
+ *
154
+ * Verified present on all three servers: SQLite has had window functions since
155
+ * 3.25, MySQL since 8.0, Postgres throughout.
156
+ */
157
+ export const WINDOW_FUNCTIONS = new Set([
158
+ 'CUME_DIST',
159
+ 'DENSE_RANK',
160
+ 'FIRST_VALUE',
161
+ 'LAG',
162
+ 'LAST_VALUE',
163
+ 'LEAD',
164
+ 'NTH_VALUE',
165
+ 'NTILE',
166
+ 'PERCENT_RANK',
167
+ 'RANK',
168
+ 'ROW_NUMBER',
169
+ ])
170
+
171
+ /**
172
+ * A function call with an `OVER (…)` clause.
173
+ *
174
+ * `spec` is the inside of the parentheses — `PARTITION BY "a" ORDER BY "b" ASC`
175
+ * — and arrives **already validated and quoted**, exactly like `_joins[].on`.
176
+ * That is deliberate: `safeColumn` lives in the query builder, and duplicating
177
+ * the identifier rules here would make two writers of the same SQL, which is
178
+ * the thing convention 8 exists to prevent. This class stores; the builder
179
+ * validates.
180
+ *
181
+ * No frame clause (`ROWS BETWEEN …`). It is legal everywhere and would fit, but
182
+ * it has its own grammar and nobody has asked — an empty frame is the SQL
183
+ * default and is what every call here gets.
184
+ */
185
+ export class WindowRef {
186
+ constructor(
187
+ /** Either an aggregate call, or the name of a window-only function. */
188
+ public fn: SQLFunctionRef | string,
189
+ public spec: string,
190
+ /** Arguments for a window-only function, e.g. `LAG(col, 1)`. */
191
+ public args: unknown[] = [],
192
+ ) {}
193
+ }
194
+
195
+ const RX_SAFE_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/
196
+
197
+ /** True for a plain identifier safe to interpolate into SQL after quoting. */
198
+ export function isSafeIdentifier(name: unknown): name is string {
199
+ return typeof name === 'string' && RX_SAFE_IDENTIFIER.test(name)
200
+ }
201
+
202
+ function activeQuoteChar(): string {
203
+ return getActiveDb()?.quoteChar || '"'
204
+ }
205
+
206
+ /**
207
+ * Memo for `Case.snake`, which is the single most expensive step in quoting an
208
+ * identifier — measurably more than the quoting itself — and is re-run for the
209
+ * same handful of table and column names on every clause of every query.
210
+ *
211
+ * A capped `Map` rather than `LRUCache`: an LRU `get` deletes and re-inserts on
212
+ * every hit to maintain recency, which measured ~38x slower than `Map.get` and
213
+ * would cost more than the `Case.snake` call it is meant to replace. Convention
214
+ * 6 asks for bounded, and this is bounded by construction — identifiers are
215
+ * schema-derived in practice, but `DB.col()` takes a caller-supplied string, so
216
+ * the bound cannot rest on that. Dropping the whole map on overflow is correct
217
+ * because every entry is recomputable: it is a memo, not state.
218
+ */
219
+ const SNAKE_CACHE_MAX = 4096
220
+ const snakeCache = new Map<string, string>()
221
+
222
+ function snake(name: string): string {
223
+ const hit = snakeCache.get(name)
224
+ if (hit !== undefined) return hit
225
+ const value = Case.snake(name)
226
+ if (snakeCache.size >= SNAKE_CACHE_MAX) snakeCache.clear()
227
+ snakeCache.set(name, value)
228
+ return value
229
+ }
230
+
231
+ /**
232
+ * Quote an identifier for the active dialect. Everything that interpolates a
233
+ * table/column/alias name goes through here rather than inlining the quote
234
+ * character, so the quote-stripping in `quoteIdentifier` can never be skipped.
235
+ */
236
+ export function qRaw(name: string): string {
237
+ return quoteIdentifier(name, activeQuoteChar())
238
+ }
239
+
240
+ /** Quote a single identifier, snake-casing it first. */
241
+ export function qId(name: string): string {
242
+ return qRaw(snake(name))
243
+ }
244
+
245
+ /** Quote `table.column`, a bare column, or pass `*` through untouched. */
246
+ export function qRef(ref: string): string {
247
+ if (ref === '*') return ref
248
+ const dot = ref.indexOf('.')
249
+ if (dot === -1) return qId(ref)
250
+ return `${qId(ref.slice(0, dot))}.${qId(ref.slice(dot + 1))}`
251
+ }
252
+
253
+ /**
254
+ * Lives here rather than in `orm/query.ts` so `evalOperands` can identify it
255
+ * with a real `instanceof`. It used to be duck-typed on the presence of
256
+ * `fnName`/`col`, which let any JSON request body reach the interpolation below.
257
+ */
258
+ export class SQLFunctionRef<C extends string = string> {
259
+ constructor(
260
+ public fnName: string,
261
+ public col: C,
262
+ public extraArgs: any[] = [],
263
+ /**
264
+ * `COUNT(DISTINCT col)` rather than `COUNT(col)`.
265
+ *
266
+ * A property of the call, not of the query: the builder-level `.distinct()`
267
+ * is `SELECT DISTINCT` over the whole row, which is a different thing and
268
+ * composes independently of this one.
269
+ */
270
+ public distinct = false,
271
+ ) {}
272
+ }
273
+
274
+ export class OperatorRef<R = any> {
275
+ constructor(
276
+ public operator: string,
277
+ public right: R,
278
+ public isRightColumn?: boolean,
279
+ ) {}
280
+ }
281
+
282
+ // Convention 8 makes this the single writer of SQL identifiers, so the arms
283
+ // belong together: splitting by operand kind spreads that responsibility.
284
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: recursive operand evaluator — one arm per operand shape
285
+ export function evalOperands(
286
+ where: unknown,
287
+ params: unknown[],
288
+ isColumn?: boolean,
289
+ ): string {
290
+ if (where instanceof ColumnRef) {
291
+ return qRef(where.col)
292
+ }
293
+
294
+ if (where instanceof OperatorRef) {
295
+ const op = where.operator.toUpperCase()
296
+ if (op === 'IS NULL' || op === 'IS NOT NULL') {
297
+ return op
298
+ }
299
+ if (op === 'BETWEEN' && Array.isArray(where.right)) {
300
+ const min = evalOperands(where.right[0], params, false)
301
+ const max = evalOperands(where.right[1], params, false)
302
+ return `BETWEEN ${min} AND ${max}`
303
+ }
304
+ if ((op === 'IN' || op === 'NOT IN') && Array.isArray(where.right)) {
305
+ const items = where.right
306
+ .map(v => evalOperands(v, params, false))
307
+ .join(', ')
308
+ return `${op} (${items})`
309
+ }
310
+ const rightSql = evalOperands(
311
+ where.right,
312
+ params,
313
+ where.isRightColumn || where.right instanceof ColumnRef,
314
+ )
315
+ return `${op} ${rightSql}`
316
+ }
317
+
318
+ if (Array.isArray(where)) {
319
+ return `(${where.map(v => evalOperands(v, params, isColumn)).join(', ')})`
320
+ }
321
+
322
+ if (where === null) return 'NULL'
323
+
324
+ if (typeof where === 'object' && where !== null) {
325
+ if (where instanceof WindowRef) {
326
+ // The function half only. `spec` was built by the query builder out of
327
+ // `safeColumn` output and is inserted as-is — see the class comment.
328
+ let call: string
329
+ if (where.fn instanceof SQLFunctionRef) {
330
+ call = evalOperands(where.fn, params, true)
331
+ } else {
332
+ const fnName = String(where.fn).toUpperCase()
333
+ // Interpolated, not bound, so it passes the same kind of allow-list
334
+ // `SQL_FUNCTIONS` applies to an ordinary call.
335
+ if (!WINDOW_FUNCTIONS.has(fnName) && !SQL_FUNCTIONS.has(fnName)) {
336
+ throws(`Unsupported window function: ${where.fn}`)
337
+ }
338
+ const args = where.args
339
+ .map(a => evalOperands(a, params, false))
340
+ .join(', ')
341
+ call = `${fnName}(${args})`
342
+ }
343
+ return `${call} OVER (${where.spec})`
344
+ }
345
+
346
+ if (where instanceof SQLFunctionRef) {
347
+ const fnName = String(where.fnName).toUpperCase()
348
+ const colArg = where.col
349
+ const extraArgs = where.extraArgs || []
350
+
351
+ // fnName is interpolated, not bound — only known functions may pass.
352
+ if (!SQL_FUNCTIONS.has(fnName)) {
353
+ throws(`Unsupported SQL function: ${where.fnName}`)
354
+ }
355
+
356
+ let evalCol = ''
357
+ if (colArg === '*') {
358
+ evalCol = '*'
359
+ } else if (colArg instanceof ColumnRef) {
360
+ evalCol = evalOperands(colArg, params, true)
361
+ } else if (typeof colArg === 'string') {
362
+ evalCol = qRef(colArg)
363
+ } else {
364
+ evalCol = evalOperands(colArg, params, true)
365
+ }
366
+
367
+ if (extraArgs.length > 0) {
368
+ const evalExtras = extraArgs
369
+ .map((a: any) => evalOperands(a, params, false))
370
+ .join(', ')
371
+ return `${fnName}(${evalCol}, ${evalExtras})`
372
+ }
373
+ return `${fnName}(${where.distinct ? 'DISTINCT ' : ''}${evalCol})`
374
+ }
375
+
376
+ if (typeof (where as any).parse === 'function') {
377
+ const { sql, params: subParams } = (where as any).parse()
378
+ if (subParams && subParams.length > 0) {
379
+ params.push(...subParams)
380
+ }
381
+ return `(${sql})`
382
+ }
383
+
384
+ const entries = Object.entries(where)
385
+ if (entries.length === 0) throws('Empty operands object')
386
+ const [key, val] = entries[0]!
387
+
388
+ if (SQL_FUNCTIONS.has(key.toUpperCase())) {
389
+ const args = Array.isArray(val) ? val : [val]
390
+ return `${key.toUpperCase()}(${args.map(arg => evalOperands(arg, params, true)).join(', ')})`
391
+ }
392
+
393
+ // `{table: 'column'}` becomes a bare identifier reference, so both halves are
394
+ // interpolated. Case.snake does not strip quote characters, so an unchecked
395
+ // value here can break out of the identifier.
396
+ if (!isSafeIdentifier(key) || !isSafeIdentifier(val)) {
397
+ throws(
398
+ `Unsafe identifier in operands object: ${key}.${String(val)}. ` +
399
+ 'Pass a scalar to bind it as a parameter.',
400
+ )
401
+ }
402
+
403
+ return `${qId(key)}.${qId(val as string)}`
404
+ }
405
+
406
+ if (typeof where === 'boolean') return where ? 'TRUE' : 'FALSE'
407
+
408
+ if (typeof where === 'string') {
409
+ if (isColumn === true) return qRef(where)
410
+
411
+ if (
412
+ isColumn === undefined &&
413
+ /^[a-zA-Z_][a-zA-Z0-9_]*\.[a-zA-Z_][a-zA-Z0-9_]*$/.test(where)
414
+ ) {
415
+ return qRef(where)
416
+ }
417
+
418
+ params.push(where)
419
+ return '?'
420
+ }
421
+
422
+ params.push(where)
423
+ return '?'
424
+ }
425
+
426
+ export function old<TSchema extends SyncTypes.DBConstraints>(
427
+ oldTableName: string,
428
+ schema: TSchema,
429
+ transform?: (oldRow: Record<string, unknown>) => unknown,
430
+ ): TSchema
431
+
432
+ /**
433
+ * Constrained on `{ type: unknown }`, not on `ColumnConstraint`.
434
+ *
435
+ * This is the one function that takes a column descriptor in a *typed*
436
+ * position, so it is where the two vocabularies meet: `ColumnConstraint.type`
437
+ * is the dialect name the sync engine reads, while a `Field.*` builder declares
438
+ * `type` as the **row type**. Requiring the sync-side shape here rejected every
439
+ * `Field` value — `old('title', Field.Varchar(255))` stopped compiling.
440
+ *
441
+ * `old()` only copies the object and adds two keys, so it needs no more than
442
+ * "something with a `type`", and returning `T` unchanged means the row type and
443
+ * optionality survive the wrapper.
444
+ */
445
+ export function old<T extends { type: unknown }>(
446
+ oldColumnName: string,
447
+ columnDef: T,
448
+ transform?: (oldValue: unknown, oldRow: Record<string, unknown>) => unknown,
449
+ ): T
450
+
451
+ export function old(
452
+ oldName: string,
453
+ target: unknown,
454
+ transform?: unknown,
455
+ ): unknown {
456
+ if (target && is.object(target) && 'type' in target) {
457
+ return Object.assign({}, target, {
458
+ _oldColumn: oldName,
459
+ _transform: transform,
460
+ })
461
+ }
462
+
463
+ return Object.assign({}, target, {
464
+ _oldTable: oldName,
465
+ _transform: transform,
466
+ })
467
+ }