sqlite-zod-orm 3.8.0 → 3.10.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/entity.ts ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * entity.ts — Entity augmentation logic extracted from Database class.
3
+ *
4
+ * Handles attaching .update(), .delete(), relationship navigation methods,
5
+ * and the auto-persist proxy to raw row objects.
6
+ */
7
+ import type { AugmentedEntity, Relationship } from './types';
8
+ import { getStorableFields, transformForStorage } from './schema';
9
+ import type { DatabaseContext } from './context';
10
+ import { getById, findMany, update, deleteEntity } from './crud';
11
+
12
+ /**
13
+ * Augment a raw entity with:
14
+ * - .update(data) → persist partial update
15
+ * - .delete() → delete from DB
16
+ * - Lazy relationship accessors (author(), books(), etc.)
17
+ * - Auto-persist proxy: `entity.name = 'New'` auto-updates DB
18
+ */
19
+ export function attachMethods<T extends Record<string, any>>(
20
+ ctx: DatabaseContext,
21
+ entityName: string,
22
+ entity: T,
23
+ ): AugmentedEntity<any> {
24
+ const augmented = entity as any;
25
+ augmented.update = (data: any) => update(ctx, entityName, entity.id, data);
26
+ augmented.delete = () => deleteEntity(ctx, entityName, entity.id);
27
+
28
+ // Attach lazy relationship navigation
29
+ for (const rel of ctx.relationships) {
30
+ if (rel.from === entityName && rel.type === 'belongs-to') {
31
+ // book.author() → lazy load parent via author_id FK
32
+ augmented[rel.relationshipField] = () => {
33
+ const fkValue = entity[rel.foreignKey];
34
+ return fkValue ? getById(ctx, rel.to, fkValue) : null;
35
+ };
36
+ } else if (rel.from === entityName && rel.type === 'one-to-many') {
37
+ // author.books() → lazy load children
38
+ const belongsToRel = ctx.relationships.find(
39
+ r => r.type === 'belongs-to' && r.from === rel.to && r.to === rel.from
40
+ );
41
+ if (belongsToRel) {
42
+ const fk = belongsToRel.foreignKey;
43
+ augmented[rel.relationshipField] = () => {
44
+ return findMany(ctx, rel.to, { [fk]: entity.id });
45
+ };
46
+ }
47
+ }
48
+ }
49
+
50
+ // Auto-persist proxy: setting a field auto-updates the DB row
51
+ const storableFieldNames = new Set(getStorableFields(ctx.schemas[entityName]!).map(f => f.name));
52
+ return new Proxy(augmented, {
53
+ set: (target, prop: string, value) => {
54
+ if (storableFieldNames.has(prop) && target[prop] !== value) {
55
+ update(ctx, entityName, target.id, { [prop]: value });
56
+ }
57
+ target[prop] = value;
58
+ return true;
59
+ },
60
+ get: (target, prop, receiver) => Reflect.get(target, prop, receiver),
61
+ });
62
+ }
package/src/helpers.ts ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * sql-helpers.ts — SQL utility functions extracted from Database class.
3
+ *
4
+ * Contains WHERE clause building and other SQL-level helpers.
5
+ */
6
+ import { transformForStorage } from './schema';
7
+
8
+ /**
9
+ * Build a parameterized WHERE clause from a conditions object.
10
+ *
11
+ * Supports:
12
+ * - Simple equality: `{ name: 'Alice' }`
13
+ * - Operators: `{ age: { $gt: 18 } }`
14
+ * - $in: `{ status: { $in: ['active', 'pending'] } }`
15
+ * - $or: `{ $or: [{ name: 'Alice' }, { name: 'Bob' }] }`
16
+ */
17
+ export function buildWhereClause(conditions: Record<string, any>, tablePrefix?: string): { clause: string; values: any[] } {
18
+ const parts: string[] = [];
19
+ const values: any[] = [];
20
+
21
+ for (const key in conditions) {
22
+ if (key.startsWith('$')) {
23
+ if (key === '$or' && Array.isArray(conditions[key])) {
24
+ const orBranches = conditions[key] as Record<string, any>[];
25
+ const orParts: string[] = [];
26
+ for (const branch of orBranches) {
27
+ const sub = buildWhereClause(branch, tablePrefix);
28
+ if (sub.clause) {
29
+ orParts.push(`(${sub.clause.replace(/^WHERE /, '')})`);
30
+ values.push(...sub.values);
31
+ }
32
+ }
33
+ if (orParts.length > 0) parts.push(`(${orParts.join(' OR ')})`);
34
+ }
35
+ continue;
36
+ }
37
+ const value = conditions[key];
38
+ const fieldName = tablePrefix ? `"${tablePrefix}"."${key}"` : `"${key}"`;
39
+
40
+ if (typeof value === 'object' && value !== null && !Array.isArray(value) && !(value instanceof Date)) {
41
+ const operator = Object.keys(value)[0];
42
+ if (!operator?.startsWith('$')) {
43
+ throw new Error(`Querying on nested object '${key}' not supported. Use operators like $gt.`);
44
+ }
45
+ const operand = value[operator];
46
+
47
+ if (operator === '$in') {
48
+ if (!Array.isArray(operand)) throw new Error(`$in for '${key}' requires an array`);
49
+ if (operand.length === 0) { parts.push('1 = 0'); continue; }
50
+ parts.push(`${fieldName} IN (${operand.map(() => '?').join(', ')})`);
51
+ values.push(...operand.map((v: any) => transformForStorage({ v }).v));
52
+ continue;
53
+ }
54
+
55
+ if (operator === '$notIn') {
56
+ if (!Array.isArray(operand)) throw new Error(`$notIn for '${key}' requires an array`);
57
+ if (operand.length === 0) continue; // no-op: everything is "not in" an empty set
58
+ parts.push(`${fieldName} NOT IN (${operand.map(() => '?').join(', ')})`);
59
+ values.push(...operand.map((v: any) => transformForStorage({ v }).v));
60
+ continue;
61
+ }
62
+
63
+ if (operator === '$like') {
64
+ parts.push(`${fieldName} LIKE ?`);
65
+ values.push(operand);
66
+ continue;
67
+ }
68
+
69
+ if (operator === '$between') {
70
+ if (!Array.isArray(operand) || operand.length !== 2) throw new Error(`$between for '${key}' requires [min, max]`);
71
+ parts.push(`${fieldName} BETWEEN ? AND ?`);
72
+ values.push(transformForStorage({ v: operand[0] }).v, transformForStorage({ v: operand[1] }).v);
73
+ continue;
74
+ }
75
+
76
+ const sqlOp = ({ $gt: '>', $gte: '>=', $lt: '<', $lte: '<=', $ne: '!=' } as Record<string, string>)[operator];
77
+ if (!sqlOp) throw new Error(`Unsupported operator '${operator}' on '${key}'`);
78
+ parts.push(`${fieldName} ${sqlOp} ?`);
79
+ values.push(transformForStorage({ operand }).operand);
80
+ } else {
81
+ parts.push(`${fieldName} = ?`);
82
+ values.push(transformForStorage({ value }).value);
83
+ }
84
+ }
85
+
86
+ return { clause: parts.length > 0 ? `WHERE ${parts.join(' AND ')}` : '', values };
87
+ }
package/src/index.ts CHANGED
@@ -9,14 +9,13 @@ export type { DatabaseType } from './database';
9
9
  export type {
10
10
  SchemaMap, DatabaseOptions, Relationship,
11
11
  EntityAccessor, TypedAccessors, AugmentedEntity, UpdateBuilder,
12
- InferSchema, EntityData, IndexDef,
12
+ InferSchema, EntityData, IndexDef, ChangeEvent,
13
13
  ProxyColumns, ColumnRef,
14
14
  } from './types';
15
15
 
16
16
  export { z } from 'zod';
17
17
 
18
- export { QueryBuilder } from './query-builder';
19
- export { ColumnNode, type ProxyQueryResult } from './proxy-query';
18
+ export { QueryBuilder, ColumnNode, compileIQO, type ProxyQueryResult } from './query';
20
19
  export {
21
20
  type ASTNode, type WhereCallback, type SetCallback,
22
21
  type TypedColumnProxy, type FunctionProxy, type Operators,
package/src/iqo.ts ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * iqo.ts — Internal Query Object types and SQL compiler
3
+ *
4
+ * Defines the IQO structure, WHERE operators, and the `compileIQO` function
5
+ * that transforms an IQO into executable SQL + params.
6
+ */
7
+
8
+ import { type ASTNode, compileAST } from './ast';
9
+
10
+ // =============================================================================
11
+ // IQO — Internal Query Object
12
+ // =============================================================================
13
+
14
+ export type OrderDirection = 'asc' | 'desc';
15
+ export type WhereOperator = '$gt' | '$gte' | '$lt' | '$lte' | '$ne' | '$in' | '$like' | '$notIn' | '$between';
16
+
17
+ export interface WhereCondition {
18
+ field: string;
19
+ operator: '=' | '>' | '>=' | '<' | '<=' | '!=' | 'IN' | 'LIKE' | 'NOT IN' | 'BETWEEN';
20
+ value: any;
21
+ }
22
+
23
+ export interface JoinClause {
24
+ table: string;
25
+ fromCol: string;
26
+ toCol: string;
27
+ columns: string[]; // columns to SELECT from the joined table
28
+ }
29
+
30
+ export interface IQO {
31
+ selects: string[];
32
+ wheres: WhereCondition[];
33
+ whereOrs: WhereCondition[][]; // Each sub-array is an OR group
34
+ whereAST: ASTNode | null;
35
+ joins: JoinClause[];
36
+ groupBy: string[];
37
+ limit: number | null;
38
+ offset: number | null;
39
+ orderBy: { field: string; direction: OrderDirection }[];
40
+ includes: string[];
41
+ raw: boolean;
42
+ }
43
+
44
+ export const OPERATOR_MAP: Record<WhereOperator, string> = {
45
+ $gt: '>',
46
+ $gte: '>=',
47
+ $lt: '<',
48
+ $lte: '<=',
49
+ $ne: '!=',
50
+ $in: 'IN',
51
+ $like: 'LIKE',
52
+ $notIn: 'NOT IN',
53
+ $between: 'BETWEEN',
54
+ };
55
+
56
+ export function transformValueForStorage(value: any): any {
57
+ if (value instanceof Date) return value.toISOString();
58
+ if (typeof value === 'boolean') return value ? 1 : 0;
59
+ return value;
60
+ }
61
+
62
+ /**
63
+ * Compile an Internal Query Object into executable SQL + params.
64
+ * Handles SELECT, JOIN, WHERE (object + AST + $or), ORDER BY, LIMIT, OFFSET.
65
+ */
66
+ export function compileIQO(tableName: string, iqo: IQO): { sql: string; params: any[] } {
67
+ const params: any[] = [];
68
+
69
+ // SELECT clause
70
+ const selectParts: string[] = [];
71
+ if (iqo.selects.length > 0) {
72
+ selectParts.push(...iqo.selects.map(s => `${tableName}.${s}`));
73
+ } else {
74
+ selectParts.push(`${tableName}.*`);
75
+ }
76
+ for (const j of iqo.joins) {
77
+ if (j.columns.length > 0) {
78
+ selectParts.push(...j.columns.map(c => `${j.table}.${c} AS ${j.table}_${c}`));
79
+ } else {
80
+ selectParts.push(`${j.table}.*`);
81
+ }
82
+ }
83
+
84
+ let sql = `SELECT ${selectParts.join(', ')} FROM ${tableName}`;
85
+
86
+ // JOIN clauses
87
+ for (const j of iqo.joins) {
88
+ sql += ` JOIN ${j.table} ON ${tableName}.${j.fromCol} = ${j.table}.${j.toCol}`;
89
+ }
90
+
91
+ // WHERE clause — AST-based takes precedence if set
92
+ if (iqo.whereAST) {
93
+ const compiled = compileAST(iqo.whereAST);
94
+ sql += ` WHERE ${compiled.sql}`;
95
+ params.push(...compiled.params);
96
+ } else if (iqo.wheres.length > 0) {
97
+ const hasJoins = iqo.joins.length > 0;
98
+ const qualify = (field: string) =>
99
+ hasJoins && !field.includes('.') ? `${tableName}.${field}` : field;
100
+
101
+ const whereParts: string[] = [];
102
+ for (const w of iqo.wheres) {
103
+ if (w.operator === 'IN') {
104
+ const arr = w.value as any[];
105
+ if (arr.length === 0) {
106
+ whereParts.push('1 = 0');
107
+ } else {
108
+ const placeholders = arr.map(() => '?').join(', ');
109
+ whereParts.push(`${qualify(w.field)} IN (${placeholders})`);
110
+ params.push(...arr.map(transformValueForStorage));
111
+ }
112
+ } else if (w.operator === 'NOT IN') {
113
+ const arr = w.value as any[];
114
+ if (arr.length === 0) continue; // no-op
115
+ const placeholders = arr.map(() => '?').join(', ');
116
+ whereParts.push(`${qualify(w.field)} NOT IN (${placeholders})`);
117
+ params.push(...arr.map(transformValueForStorage));
118
+ } else if (w.operator === 'BETWEEN') {
119
+ const [min, max] = w.value as [any, any];
120
+ whereParts.push(`${qualify(w.field)} BETWEEN ? AND ?`);
121
+ params.push(transformValueForStorage(min), transformValueForStorage(max));
122
+ } else {
123
+ whereParts.push(`${qualify(w.field)} ${w.operator} ?`);
124
+ params.push(transformValueForStorage(w.value));
125
+ }
126
+ }
127
+ if (whereParts.length > 0) {
128
+ sql += ` WHERE ${whereParts.join(' AND ')}`;
129
+ }
130
+ }
131
+
132
+ // Append OR groups (from $or)
133
+ if (iqo.whereOrs.length > 0) {
134
+ for (const orGroup of iqo.whereOrs) {
135
+ const orParts: string[] = [];
136
+ for (const w of orGroup) {
137
+ if (w.operator === 'IN') {
138
+ const arr = w.value as any[];
139
+ if (arr.length === 0) {
140
+ orParts.push('1 = 0');
141
+ } else {
142
+ orParts.push(`${w.field} IN (${arr.map(() => '?').join(', ')})`);
143
+ params.push(...arr.map(transformValueForStorage));
144
+ }
145
+ } else {
146
+ orParts.push(`${w.field} ${w.operator} ?`);
147
+ params.push(transformValueForStorage(w.value));
148
+ }
149
+ }
150
+ if (orParts.length > 0) {
151
+ const orClause = `(${orParts.join(' OR ')})`;
152
+ sql += sql.includes(' WHERE ') ? ` AND ${orClause}` : ` WHERE ${orClause}`;
153
+ }
154
+ }
155
+ }
156
+
157
+ // GROUP BY
158
+ if (iqo.groupBy.length > 0) {
159
+ sql += ` GROUP BY ${iqo.groupBy.join(', ')}`;
160
+ }
161
+
162
+ // ORDER BY
163
+ if (iqo.orderBy.length > 0) {
164
+ const parts = iqo.orderBy.map(o => `${o.field} ${o.direction.toUpperCase()}`);
165
+ sql += ` ORDER BY ${parts.join(', ')}`;
166
+ }
167
+
168
+ if (iqo.limit !== null) sql += ` LIMIT ${iqo.limit}`;
169
+ if (iqo.offset !== null) sql += ` OFFSET ${iqo.offset}`;
170
+
171
+ return { sql, params };
172
+ }
@@ -1,14 +1,15 @@
1
1
  /**
2
- * proxy-query.ts — Proxy-based SQL query builder
2
+ * proxy.ts — Proxy Query System
3
3
  *
4
- * Enables db.query(c => { const { users: u, posts: p } = c; ... }) syntax.
5
- * Table/column access through the proxy creates ColumnNode AST nodes,
6
- * which are then compiled into parameterized SQL.
4
+ * Enables `db.query(c => { ... })` for SQL-like multi-table queries
5
+ * using destructured proxied table references.
7
6
  */
8
7
 
9
- import { z } from 'zod';
8
+ import type { z } from 'zod';
10
9
 
11
- // ---------- Column Node ----------
10
+ // =============================================================================
11
+ // ColumnNode
12
+ // =============================================================================
12
13
 
13
14
  /**
14
15
  * Represents a reference to a specific table column with an alias.
@@ -34,22 +35,16 @@ export class ColumnNode {
34
35
 
35
36
  // ---------- SQL Quoting Helpers ----------
36
37
 
37
- /** Quote an identifier with double quotes */
38
38
  function q(name: string): string {
39
39
  return `"${name}"`;
40
40
  }
41
41
 
42
- /** Quote a fully qualified reference: alias.column */
43
42
  function qRef(alias: string, column: string): string {
44
43
  return `"${alias}"."${column}"`;
45
44
  }
46
45
 
47
46
  // ---------- Table Proxy ----------
48
47
 
49
- /**
50
- * Creates a proxy representing a table with a given alias.
51
- * Property access returns ColumnNode objects.
52
- */
53
48
  function createTableProxy(
54
49
  tableName: string,
55
50
  alias: string,
@@ -82,10 +77,6 @@ interface AliasEntry {
82
77
  proxy: Record<string, ColumnNode>;
83
78
  }
84
79
 
85
- /**
86
- * Creates the root context proxy `c` that the user destructures.
87
- * Each table access generates a unique alias.
88
- */
89
80
  export function createContextProxy(
90
81
  schemas: Record<string, z.ZodType<any>>,
91
82
  ): { proxy: Record<string, Record<string, ColumnNode>>; aliasMap: Map<string, AliasEntry[]> } {
@@ -106,7 +97,6 @@ export function createContextProxy(
106
97
  const alias = `t${aliasCounter}`;
107
98
  const tableProxy = createTableProxy(tableName, alias, columns);
108
99
 
109
- // Track alias
110
100
  const entries = aliases.get(tableName) || [];
111
101
  entries.push({ tableName, alias, proxy: tableProxy });
112
102
  aliases.set(tableName, entries);
@@ -118,7 +108,7 @@ export function createContextProxy(
118
108
  return { proxy, aliasMap: aliases };
119
109
  }
120
110
 
121
- // ---------- Query Result Shape ----------
111
+ // ---------- Proxy Query Result ----------
122
112
 
123
113
  type AnyColumn = ColumnNode | (ColumnNode & string);
124
114
 
@@ -132,31 +122,26 @@ export interface ProxyQueryResult {
132
122
  groupBy?: (AnyColumn | undefined)[];
133
123
  }
134
124
 
135
- // ---------- Query Compiler ----------
125
+ // ---------- Proxy Query Compiler ----------
136
126
 
137
127
  function isColumnNode(val: any): val is ColumnNode {
138
128
  return val && typeof val === 'object' && val._type === 'COL';
139
129
  }
140
130
 
141
- /**
142
- * Compile the result of the user's callback into SQL.
143
- */
144
131
  export function compileProxyQuery(
145
132
  queryResult: ProxyQueryResult,
146
133
  aliasMap: Map<string, AliasEntry[]>,
147
134
  ): { sql: string; params: any[] } {
148
135
  const params: any[] = [];
149
136
 
150
- // Collect all tables/aliases referenced
151
137
  const tablesUsed = new Map<string, { tableName: string; alias: string }>();
152
-
153
138
  for (const [tableName, entries] of aliasMap) {
154
139
  for (const entry of entries) {
155
140
  tablesUsed.set(entry.alias, { tableName, alias: entry.alias });
156
141
  }
157
142
  }
158
143
 
159
- // ---------- SELECT ----------
144
+ // SELECT
160
145
  const selectParts: string[] = [];
161
146
  for (const [outputName, colOrValue] of Object.entries(queryResult.select)) {
162
147
  if (isColumnNode(colOrValue)) {
@@ -166,20 +151,18 @@ export function compileProxyQuery(
166
151
  selectParts.push(`${qRef(colOrValue.alias, colOrValue.column)} AS ${q(outputName)}`);
167
152
  }
168
153
  } else {
169
- // Literal value
170
154
  selectParts.push(`? AS ${q(outputName)}`);
171
155
  params.push(colOrValue);
172
156
  }
173
157
  }
174
158
 
175
- // ---------- FROM / JOIN ----------
159
+ // FROM / JOIN
176
160
  const allAliases = [...tablesUsed.values()];
177
161
  if (allAliases.length === 0) throw new Error('No tables referenced in query.');
178
162
 
179
163
  const primaryAlias = allAliases[0]!;
180
164
  let sql = `SELECT ${selectParts.join(', ')} FROM ${q(primaryAlias.tableName)} ${q(primaryAlias.alias)}`;
181
165
 
182
- // Process JOINs
183
166
  if (queryResult.join) {
184
167
  const joins: [ColumnNode, ColumnNode][] = Array.isArray(queryResult.join[0])
185
168
  ? queryResult.join as [ColumnNode, ColumnNode][]
@@ -188,23 +171,18 @@ export function compileProxyQuery(
188
171
  for (const [left, right] of joins) {
189
172
  const leftTable = tablesUsed.get(left.alias);
190
173
  const rightTable = tablesUsed.get(right.alias);
191
-
192
174
  if (!leftTable || !rightTable) throw new Error('Join references unknown table alias.');
193
-
194
175
  const joinAlias = leftTable.alias === primaryAlias.alias ? rightTable : leftTable;
195
-
196
176
  sql += ` JOIN ${q(joinAlias.tableName)} ${q(joinAlias.alias)} ON ${qRef(left.alias, left.column)} = ${qRef(right.alias, right.column)}`;
197
177
  }
198
178
  }
199
179
 
200
- // ---------- WHERE ----------
180
+ // WHERE
201
181
  if (queryResult.where && Object.keys(queryResult.where).length > 0) {
202
182
  const whereParts: string[] = [];
203
183
 
204
184
  for (const [key, value] of Object.entries(queryResult.where)) {
205
185
  let fieldRef: string;
206
-
207
- // Match quoted alias.column pattern: "alias"."column"
208
186
  const quotedMatch = key.match(/^"([^"]+)"\."([^"]+)"$/);
209
187
  if (quotedMatch && tablesUsed.has(quotedMatch[1]!)) {
210
188
  fieldRef = key;
@@ -223,8 +201,8 @@ export function compileProxyQuery(
223
201
  params.push(...value);
224
202
  }
225
203
  } else if (typeof value === 'object' && value !== null && !(value instanceof Date)) {
226
- for (const [op, operand] of Object.entries(value)) {
227
- if (op === '$in') {
204
+ for (const [pOp, operand] of Object.entries(value)) {
205
+ if (pOp === '$in') {
228
206
  const arr = operand as any[];
229
207
  if (arr.length === 0) {
230
208
  whereParts.push('1 = 0');
@@ -238,8 +216,8 @@ export function compileProxyQuery(
238
216
  const opMap: Record<string, string> = {
239
217
  $gt: '>', $gte: '>=', $lt: '<', $lte: '<=', $ne: '!=',
240
218
  };
241
- const sqlOp = opMap[op];
242
- if (!sqlOp) throw new Error(`Unsupported where operator: ${op}`);
219
+ const sqlOp = opMap[pOp];
220
+ if (!sqlOp) throw new Error(`Unsupported where operator: ${pOp}`);
243
221
  whereParts.push(`${fieldRef} ${sqlOp} ?`);
244
222
  params.push(operand);
245
223
  }
@@ -254,7 +232,7 @@ export function compileProxyQuery(
254
232
  }
255
233
  }
256
234
 
257
- // ---------- ORDER BY ----------
235
+ // ORDER BY
258
236
  if (queryResult.orderBy) {
259
237
  const parts: string[] = [];
260
238
  for (const [key, dir] of Object.entries(queryResult.orderBy)) {
@@ -272,33 +250,20 @@ export function compileProxyQuery(
272
250
  }
273
251
  }
274
252
 
275
- // ---------- GROUP BY ----------
253
+ // GROUP BY
276
254
  if (queryResult.groupBy && queryResult.groupBy.length > 0) {
277
255
  const parts = queryResult.groupBy.filter(Boolean).map(col => qRef(col!.alias, col!.column));
278
256
  sql += ` GROUP BY ${parts.join(', ')}`;
279
257
  }
280
258
 
281
- // ---------- LIMIT / OFFSET ----------
282
- if (queryResult.limit !== undefined) {
283
- sql += ` LIMIT ${queryResult.limit}`;
284
- }
285
- if (queryResult.offset !== undefined) {
286
- sql += ` OFFSET ${queryResult.offset}`;
287
- }
259
+ // LIMIT / OFFSET
260
+ if (queryResult.limit !== undefined) sql += ` LIMIT ${queryResult.limit}`;
261
+ if (queryResult.offset !== undefined) sql += ` OFFSET ${queryResult.offset}`;
288
262
 
289
263
  return { sql, params };
290
264
  }
291
265
 
292
- // ---------- Public API ----------
293
-
294
- /**
295
- * The main `db.query(c => {...})` entry point.
296
- *
297
- * @param schemas The schema map for all registered tables.
298
- * @param callback The user's query callback that receives the context proxy.
299
- * @param executor A function that runs the compiled SQL and returns rows.
300
- * @returns The query results.
301
- */
266
+ /** The main `db.query(c => {...})` entry point. */
302
267
  export function executeProxyQuery<T>(
303
268
  schemas: Record<string, z.ZodType<any>>,
304
269
  callback: (ctx: any) => ProxyQueryResult,
@@ -309,4 +274,3 @@ export function executeProxyQuery<T>(
309
274
  const { sql, params } = compileProxyQuery(queryResult, aliasMap);
310
275
  return executor(sql, params);
311
276
  }
312
-