turbine-orm 0.4.0 → 0.5.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.
Files changed (59) hide show
  1. package/README.md +51 -2
  2. package/dist/cjs/cli/config.js +161 -0
  3. package/dist/cjs/cli/index.js +977 -0
  4. package/dist/cjs/cli/migrate.js +421 -0
  5. package/dist/cjs/cli/ui.js +237 -0
  6. package/dist/cjs/client.js +449 -0
  7. package/dist/cjs/generate.js +301 -0
  8. package/dist/cjs/index.js +75 -0
  9. package/dist/cjs/introspect.js +289 -0
  10. package/dist/cjs/package.json +1 -0
  11. package/dist/cjs/pipeline.js +71 -0
  12. package/dist/cjs/query.js +1558 -0
  13. package/dist/cjs/schema-builder.js +169 -0
  14. package/dist/cjs/schema-sql.js +371 -0
  15. package/dist/cjs/schema.js +137 -0
  16. package/dist/cjs/serverless.js +199 -0
  17. package/dist/cli/config.js +1 -1
  18. package/dist/cli/index.js +16 -8
  19. package/dist/cli/migrate.d.ts +29 -5
  20. package/dist/cli/migrate.js +58 -35
  21. package/dist/cli/ui.js +1 -1
  22. package/dist/client.d.ts +15 -4
  23. package/dist/client.js +28 -15
  24. package/dist/generate.d.ts +1 -1
  25. package/dist/generate.js +13 -7
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/introspect.d.ts +1 -1
  29. package/dist/introspect.js +1 -1
  30. package/dist/pipeline.d.ts +1 -1
  31. package/dist/pipeline.js +1 -1
  32. package/dist/query.d.ts +55 -11
  33. package/dist/query.js +135 -140
  34. package/dist/schema-builder.d.ts +2 -2
  35. package/dist/schema-builder.js +2 -2
  36. package/dist/schema-sql.d.ts +1 -1
  37. package/dist/schema-sql.js +31 -15
  38. package/dist/schema.d.ts +1 -1
  39. package/dist/schema.js +1 -1
  40. package/dist/serverless.d.ts +3 -3
  41. package/dist/serverless.js +4 -4
  42. package/dist/types.d.ts +1 -1
  43. package/dist/types.js +1 -1
  44. package/package.json +17 -11
  45. package/dist/cli/config.d.ts.map +0 -1
  46. package/dist/cli/index.d.ts.map +0 -1
  47. package/dist/cli/migrate.d.ts.map +0 -1
  48. package/dist/cli/ui.d.ts.map +0 -1
  49. package/dist/client.d.ts.map +0 -1
  50. package/dist/generate.d.ts.map +0 -1
  51. package/dist/index.d.ts.map +0 -1
  52. package/dist/introspect.d.ts.map +0 -1
  53. package/dist/pipeline.d.ts.map +0 -1
  54. package/dist/query.d.ts.map +0 -1
  55. package/dist/schema-builder.d.ts.map +0 -1
  56. package/dist/schema-sql.d.ts.map +0 -1
  57. package/dist/schema.d.ts.map +0 -1
  58. package/dist/serverless.d.ts.map +0 -1
  59. package/dist/types.d.ts.map +0 -1
package/dist/query.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Query builder
2
+ * @batadata/turbine — Query builder
3
3
  *
4
4
  * Each table accessor (db.users, db.posts, etc.) returns a QueryInterface<T>
5
5
  * that builds parameterized SQL and executes it through the connection pool.
@@ -26,6 +26,13 @@ import { snakeToCamel, camelToSnake } from './schema.js';
26
26
  export function quoteIdent(name) {
27
27
  return `"${name.replace(/"/g, '""')}"`;
28
28
  }
29
+ /**
30
+ * Escape single quotes for use as string keys in json_build_object().
31
+ * Doubles single quotes per SQL quoting rules.
32
+ */
33
+ function escSingleQuote(s) {
34
+ return s.replace(/'/g, "''");
35
+ }
29
36
  /**
30
37
  * Escape LIKE pattern metacharacters: %, _, and \.
31
38
  * Must be used with `ESCAPE '\'` in the LIKE clause.
@@ -36,7 +43,7 @@ function escapeLike(value) {
36
43
  /** Known operator keys — used to detect operator objects vs plain values */
37
44
  const OPERATOR_KEYS = new Set([
38
45
  'gt', 'gte', 'lt', 'lte', 'not', 'in', 'notIn',
39
- 'contains', 'startsWith', 'endsWith',
46
+ 'contains', 'startsWith', 'endsWith', 'mode',
40
47
  ]);
41
48
  /** Check if a value is a where operator object (has at least one known operator key) */
42
49
  function isWhereOperator(value) {
@@ -66,18 +73,57 @@ function isArrayFilter(value) {
66
73
  const keys = Object.keys(value);
67
74
  return keys.length > 0 && keys.some((k) => ARRAY_OPERATOR_KEYS.has(k));
68
75
  }
76
+ // ---------------------------------------------------------------------------
77
+ // LRU cache — bounded SQL template cache to prevent memory leaks
78
+ // ---------------------------------------------------------------------------
79
+ /**
80
+ * Simple LRU (Least Recently Used) cache with a fixed maximum size.
81
+ * When the cache exceeds maxSize, the oldest (least recently used) entry is evicted.
82
+ * Uses Map insertion order for O(1) eviction.
83
+ */
84
+ class LRUCache {
85
+ maxSize;
86
+ cache = new Map();
87
+ constructor(maxSize) {
88
+ this.maxSize = maxSize;
89
+ }
90
+ get(key) {
91
+ const value = this.cache.get(key);
92
+ if (value !== undefined) {
93
+ // Move to end (most recently used)
94
+ this.cache.delete(key);
95
+ this.cache.set(key, value);
96
+ }
97
+ return value;
98
+ }
99
+ set(key, value) {
100
+ if (this.cache.has(key)) {
101
+ this.cache.delete(key);
102
+ }
103
+ else if (this.cache.size >= this.maxSize) {
104
+ // Delete oldest (first) entry
105
+ const firstKey = this.cache.keys().next().value;
106
+ if (firstKey !== undefined)
107
+ this.cache.delete(firstKey);
108
+ }
109
+ this.cache.set(key, value);
110
+ }
111
+ get size() { return this.cache.size; }
112
+ }
69
113
  export class QueryInterface {
70
114
  pool;
71
115
  table;
72
116
  schema;
73
117
  tableMeta;
74
118
  /** SQL template cache: cacheKey → sql string (params are always positional $1,$2,...) */
75
- sqlCache = new Map();
119
+ sqlCache = new LRUCache(1000);
76
120
  middlewares;
77
- // Fast path: pre-computed flag — true when this table has zero date/timestamp columns.
78
- // When true, parseRow can skip the per-field dateCols.has() check entirely.
79
- hasNoDateColumns;
80
- constructor(pool, table, schema, middlewares) {
121
+ defaultLimit;
122
+ warnOnUnlimited;
123
+ /** Pre-computed column type lookups (avoids linear scans per query) */
124
+ columnPgTypeMap;
125
+ columnArrayTypeMap;
126
+ constructor(pool, table, schema, middlewares, options) {
81
127
  this.pool = pool;
82
128
  this.table = table;
83
129
  this.schema = schema;
@@ -87,11 +133,43 @@ export class QueryInterface {
87
133
  }
88
134
  this.tableMeta = meta;
89
135
  this.middlewares = middlewares ?? [];
90
- this.hasNoDateColumns = meta.dateColumns.size === 0;
136
+ this.defaultLimit = options?.defaultLimit;
137
+ this.warnOnUnlimited = options?.warnOnUnlimited ?? false;
138
+ // Pre-compute column type lookup maps (TASK-26)
139
+ this.columnPgTypeMap = new Map();
140
+ this.columnArrayTypeMap = new Map();
141
+ for (const col of this.tableMeta.columns) {
142
+ this.columnPgTypeMap.set(col.name, col.pgType);
143
+ this.columnArrayTypeMap.set(col.name, col.pgArrayType);
144
+ }
145
+ }
146
+ /**
147
+ * Execute a pool.query with an optional timeout.
148
+ * If timeout is set, races the query against a timer and rejects on expiry.
149
+ */
150
+ async queryWithTimeout(sql, params, timeout) {
151
+ if (!timeout) {
152
+ return this.pool.query(sql, params);
153
+ }
154
+ let timer;
155
+ const timeoutPromise = new Promise((_, reject) => {
156
+ timer = setTimeout(() => reject(new Error(`[turbine] Query timed out after ${timeout}ms`)), timeout);
157
+ });
158
+ try {
159
+ return await Promise.race([this.pool.query(sql, params), timeoutPromise]);
160
+ }
161
+ finally {
162
+ clearTimeout(timer);
163
+ }
91
164
  }
92
165
  /**
93
166
  * Execute a query through the middleware chain.
94
167
  * If no middlewares are registered, executes directly.
168
+ *
169
+ * Middleware can inspect and log query parameters, modify results after execution,
170
+ * and measure timing. Note: query SQL is generated before middleware runs, so
171
+ * modifying params.args in middleware will NOT affect the executed SQL.
172
+ * To intercept queries before SQL generation, use the raw() method instead.
95
173
  */
96
174
  async executeWithMiddleware(action, args, executor) {
97
175
  if (this.middlewares.length === 0) {
@@ -128,7 +206,7 @@ export class QueryInterface {
128
206
  async findUnique(args) {
129
207
  return this.executeWithMiddleware('findUnique', args, async () => {
130
208
  const deferred = this.buildFindUnique(args);
131
- const result = await this.pool.query(deferred.sql, deferred.params);
209
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
132
210
  return deferred.transform(result);
133
211
  });
134
212
  }
@@ -137,15 +215,11 @@ export class QueryInterface {
137
215
  const whereObj = args.where;
138
216
  // Check if all where values are simple (plain equality, no operators/null/OR)
139
217
  const whereKeys = Object.keys(whereObj).filter((k) => whereObj[k] !== undefined);
140
- const isSimpleWhere = !whereObj['OR'] && !whereObj['AND'] && !whereObj['NOT'] && whereKeys.every((k) => {
218
+ const isSimpleWhere = !whereObj['OR'] && whereKeys.every((k) => {
141
219
  const v = whereObj[k];
142
- return v !== null && !isWhereOperator(v) && !isJsonFilter(v) && !isArrayFilter(v);
220
+ return v !== null && !isWhereOperator(v);
143
221
  });
144
- // -----------------------------------------------------------------------
145
- // Fast path: no relations, simple equality where — cache SQL template.
146
- // Generates: SELECT col1, col2 FROM "table" WHERE "id" = $1 LIMIT 1
147
- // No json_build_object, no subqueries, no COALESCE wrappers.
148
- // -----------------------------------------------------------------------
222
+ // For simple queries (no nested with, no operators), use cached SQL template
149
223
  if (!args.with && isSimpleWhere) {
150
224
  const colKey = columnsList ? columnsList.join(',') : '*';
151
225
  const ck = `fu:${whereKeys.sort().join(',')}:c=${colKey}`;
@@ -161,23 +235,18 @@ export class QueryInterface {
161
235
  sql = `SELECT ${selectExpr} FROM ${qt}${whereSql} LIMIT 1`;
162
236
  this.sqlCache.set(ck, sql);
163
237
  }
164
- // Fast path: skip date coercion when table has no date columns
165
- const transformRow = this.hasNoDateColumns
166
- ? (row) => this.parseRowFast(row)
167
- : (row) => this.parseRow(row, this.table);
168
238
  return {
169
239
  sql,
170
240
  params,
171
241
  transform: (result) => {
172
242
  const row = result.rows[0];
173
- return row ? transformRow(row) : null;
243
+ return row ? this.parseRow(row, this.table) : null;
174
244
  },
175
245
  tag: `${this.table}.findUnique`,
176
246
  };
177
247
  }
178
248
  // General path: supports operators, null, OR, nested with
179
249
  const { sql: whereSql, params } = this.buildWhere(args.where);
180
- // Fast path: no relations, skip json_agg (but where has operators/null/OR)
181
250
  if (!args.with) {
182
251
  const qt = quoteIdent(this.table);
183
252
  const selectExpr = columnsList
@@ -211,85 +280,23 @@ export class QueryInterface {
211
280
  // findMany
212
281
  // -------------------------------------------------------------------------
213
282
  async findMany(args) {
283
+ // Warn if no limit specified and warnOnUnlimited is enabled
284
+ const hasExplicitLimit = args?.limit !== undefined || args?.take !== undefined;
285
+ if (this.warnOnUnlimited && !hasExplicitLimit) {
286
+ console.warn(`[turbine] findMany() called without limit on table "${this.table}". Set defaultLimit in config to prevent unbounded queries.`);
287
+ }
214
288
  return this.executeWithMiddleware('findMany', (args ?? {}), async () => {
215
289
  const deferred = this.buildFindMany(args);
216
- const result = await this.pool.query(deferred.sql, deferred.params);
290
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout);
217
291
  return deferred.transform(result);
218
292
  });
219
293
  }
220
294
  buildFindMany(args) {
221
- const columnsList = this.resolveColumns(args?.select, args?.omit);
222
- const qt = quoteIdent(this.table);
223
- const hasWith = !!(args?.with);
224
- // -----------------------------------------------------------------------
225
- // Fast path: no relations, no cursor, no distinct — cache SQL template
226
- // Skip json_agg subquery machinery entirely for simple table scans.
227
- // -----------------------------------------------------------------------
228
- if (!hasWith && !args?.cursor && !args?.distinct) {
229
- const whereObj = args?.where;
230
- const whereKeys = whereObj
231
- ? Object.keys(whereObj).filter((k) => whereObj[k] !== undefined)
232
- : [];
233
- // Check if all where values are simple equality (no operators, null, OR/AND/NOT)
234
- const isSimpleWhere = !whereObj || (!whereObj['OR'] && !whereObj['AND'] && !whereObj['NOT'] && whereKeys.every((k) => {
235
- const v = whereObj[k];
236
- return v !== null && !isWhereOperator(v) && !isJsonFilter(v) && !isArrayFilter(v);
237
- }));
238
- if (isSimpleWhere) {
239
- // Build cache key: operation + where keys + columns + orderBy + hasLimit + hasOffset
240
- const colKey = columnsList ? columnsList.join(',') : '*';
241
- const orderKey = args?.orderBy ? Object.entries(args.orderBy).map(([k, d]) => `${k}:${d}`).join(',') : '';
242
- const hasLimit = args?.limit !== undefined || args?.take !== undefined;
243
- const hasOffset = args?.offset !== undefined;
244
- const ck = `fm:${whereKeys.sort().join(',')}:c=${colKey}:o=${orderKey}:l=${hasLimit ? '1' : '0'}:off=${hasOffset ? '1' : '0'}`;
245
- let sql = this.sqlCache.get(ck);
246
- const params = whereKeys.map((k) => whereObj[k]);
247
- if (!sql) {
248
- // Build SQL template once and cache it
249
- const selectExpr = columnsList
250
- ? columnsList.map((c) => `${qt}.${quoteIdent(c)}`).join(', ')
251
- : `${qt}.*`;
252
- const whereClauses = whereKeys.map((k, i) => `${this.toSqlColumn(k)} = $${i + 1}`);
253
- const whereSql = whereClauses.length > 0 ? ` WHERE ${whereClauses.join(' AND ')}` : '';
254
- sql = `SELECT ${selectExpr} FROM ${qt}${whereSql}`;
255
- if (args?.orderBy) {
256
- sql += ` ORDER BY ${this.buildOrderBy(args.orderBy)}`;
257
- }
258
- // Placeholders for LIMIT/OFFSET — positions are stable since where params come first
259
- if (hasLimit) {
260
- sql += ` LIMIT $${params.length + 1}`;
261
- }
262
- if (hasOffset) {
263
- sql += ` OFFSET $${params.length + (hasLimit ? 2 : 1)}`;
264
- }
265
- this.sqlCache.set(ck, sql);
266
- }
267
- // Append runtime param values for LIMIT and OFFSET
268
- const effectiveLimit = args?.take ?? args?.limit;
269
- if (effectiveLimit !== undefined) {
270
- params.push(Number(effectiveLimit));
271
- }
272
- if (args?.offset !== undefined) {
273
- params.push(Number(args.offset));
274
- }
275
- // Fast path: no relations, use parseRow (or parseRowFast when no date columns)
276
- const parseRow = this.hasNoDateColumns
277
- ? (row) => this.parseRowFast(row)
278
- : (row) => this.parseRow(row, this.table);
279
- return {
280
- sql,
281
- params,
282
- transform: (result) => result.rows.map(parseRow),
283
- tag: `${this.table}.findMany`,
284
- };
285
- }
286
- }
287
- // -----------------------------------------------------------------------
288
- // General path: supports operators, null, OR, cursor, distinct, nested with
289
- // -----------------------------------------------------------------------
290
295
  const { sql: whereSql, params } = args?.where
291
296
  ? this.buildWhere(args.where)
292
297
  : { sql: '', params: [] };
298
+ const columnsList = this.resolveColumns(args?.select, args?.omit);
299
+ const qt = quoteIdent(this.table);
293
300
  // Distinct support
294
301
  let distinctPrefix = '';
295
302
  if (args?.distinct && args.distinct.length > 0) {
@@ -297,7 +304,7 @@ export class QueryInterface {
297
304
  distinctPrefix = `DISTINCT ON (${distinctCols.join(', ')}) `;
298
305
  }
299
306
  let selectClause;
300
- if (hasWith) {
307
+ if (args?.with) {
301
308
  selectClause = this.buildSelectWithRelations(this.table, args.with, params, columnsList);
302
309
  }
303
310
  else if (columnsList) {
@@ -331,8 +338,8 @@ export class QueryInterface {
331
338
  if (args?.orderBy) {
332
339
  sql += ` ORDER BY ${this.buildOrderBy(args.orderBy)}`;
333
340
  }
334
- // take overrides limit when cursor pagination is used
335
- const effectiveLimit = args?.take ?? args?.limit;
341
+ // take overrides limit when cursor pagination is used; fall back to defaultLimit
342
+ const effectiveLimit = args?.take ?? args?.limit ?? this.defaultLimit;
336
343
  if (effectiveLimit !== undefined) {
337
344
  params.push(Number(effectiveLimit));
338
345
  sql += ` LIMIT $${params.length}`;
@@ -344,7 +351,7 @@ export class QueryInterface {
344
351
  return {
345
352
  sql,
346
353
  params,
347
- transform: (result) => result.rows.map((row) => hasWith
354
+ transform: (result) => result.rows.map((row) => args?.with
348
355
  ? this.parseNestedRow(row, this.table)
349
356
  : this.parseRow(row, this.table)),
350
357
  tag: `${this.table}.findMany`,
@@ -356,7 +363,7 @@ export class QueryInterface {
356
363
  async findFirst(args) {
357
364
  return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
358
365
  const deferred = this.buildFindFirst(args);
359
- const result = await this.pool.query(deferred.sql, deferred.params);
366
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout);
360
367
  return deferred.transform(result);
361
368
  });
362
369
  }
@@ -380,7 +387,7 @@ export class QueryInterface {
380
387
  async findFirstOrThrow(args) {
381
388
  return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
382
389
  const deferred = this.buildFindFirstOrThrow(args);
383
- const result = await this.pool.query(deferred.sql, deferred.params);
390
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout);
384
391
  return deferred.transform(result);
385
392
  });
386
393
  }
@@ -405,7 +412,7 @@ export class QueryInterface {
405
412
  async findUniqueOrThrow(args) {
406
413
  return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
407
414
  const deferred = this.buildFindUniqueOrThrow(args);
408
- const result = await this.pool.query(deferred.sql, deferred.params);
415
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
409
416
  return deferred.transform(result);
410
417
  });
411
418
  }
@@ -430,7 +437,7 @@ export class QueryInterface {
430
437
  async create(args) {
431
438
  return this.executeWithMiddleware('create', args, async () => {
432
439
  const deferred = this.buildCreate(args);
433
- const result = await this.pool.query(deferred.sql, deferred.params);
440
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
434
441
  return deferred.transform(result);
435
442
  });
436
443
  }
@@ -458,7 +465,7 @@ export class QueryInterface {
458
465
  async createMany(args) {
459
466
  return this.executeWithMiddleware('createMany', args, async () => {
460
467
  const deferred = this.buildCreateMany(args);
461
- const result = await this.pool.query(deferred.sql, deferred.params);
468
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
462
469
  return deferred.transform(result);
463
470
  });
464
471
  }
@@ -505,7 +512,7 @@ export class QueryInterface {
505
512
  async update(args) {
506
513
  return this.executeWithMiddleware('update', args, async () => {
507
514
  const deferred = this.buildUpdate(args);
508
- const result = await this.pool.query(deferred.sql, deferred.params);
515
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
509
516
  return deferred.transform(result);
510
517
  });
511
518
  }
@@ -539,7 +546,7 @@ export class QueryInterface {
539
546
  async delete(args) {
540
547
  return this.executeWithMiddleware('delete', args, async () => {
541
548
  const deferred = this.buildDelete(args);
542
- const result = await this.pool.query(deferred.sql, deferred.params);
549
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
543
550
  return deferred.transform(result);
544
551
  });
545
552
  }
@@ -564,7 +571,7 @@ export class QueryInterface {
564
571
  async upsert(args) {
565
572
  return this.executeWithMiddleware('upsert', args, async () => {
566
573
  const deferred = this.buildUpsert(args);
567
- const result = await this.pool.query(deferred.sql, deferred.params);
574
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
568
575
  return deferred.transform(result);
569
576
  });
570
577
  }
@@ -608,7 +615,7 @@ export class QueryInterface {
608
615
  async updateMany(args) {
609
616
  return this.executeWithMiddleware('updateMany', args, async () => {
610
617
  const deferred = this.buildUpdateMany(args);
611
- const result = await this.pool.query(deferred.sql, deferred.params);
618
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
612
619
  return deferred.transform(result);
613
620
  });
614
621
  }
@@ -637,7 +644,7 @@ export class QueryInterface {
637
644
  async deleteMany(args) {
638
645
  return this.executeWithMiddleware('deleteMany', args, async () => {
639
646
  const deferred = this.buildDeleteMany(args);
640
- const result = await this.pool.query(deferred.sql, deferred.params);
647
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
641
648
  return deferred.transform(result);
642
649
  });
643
650
  }
@@ -657,7 +664,7 @@ export class QueryInterface {
657
664
  async count(args) {
658
665
  return this.executeWithMiddleware('count', (args ?? {}), async () => {
659
666
  const deferred = this.buildCount(args);
660
- const result = await this.pool.query(deferred.sql, deferred.params);
667
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout);
661
668
  return deferred.transform(result);
662
669
  });
663
670
  }
@@ -679,7 +686,7 @@ export class QueryInterface {
679
686
  async groupBy(args) {
680
687
  return this.executeWithMiddleware('groupBy', args, async () => {
681
688
  const deferred = this.buildGroupBy(args);
682
- const result = await this.pool.query(deferred.sql, deferred.params);
689
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
683
690
  return deferred.transform(result);
684
691
  });
685
692
  }
@@ -806,7 +813,7 @@ export class QueryInterface {
806
813
  async aggregate(args) {
807
814
  return this.executeWithMiddleware('aggregate', args, async () => {
808
815
  const deferred = this.buildAggregate(args);
809
- const result = await this.pool.query(deferred.sql, deferred.params);
816
+ const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout);
810
817
  return deferred.transform(result);
811
818
  });
812
819
  }
@@ -1197,17 +1204,19 @@ export class QueryInterface {
1197
1204
  params.push(op.notIn);
1198
1205
  clauses.push(`${column} != ALL($${params.length})`);
1199
1206
  }
1207
+ // Use ILIKE for case-insensitive mode, LIKE otherwise
1208
+ const likeOp = op.mode === 'insensitive' ? 'ILIKE' : 'LIKE';
1200
1209
  if (op.contains !== undefined) {
1201
1210
  params.push(`%${escapeLike(op.contains)}%`);
1202
- clauses.push(`${column} LIKE $${params.length} ESCAPE '\\'`);
1211
+ clauses.push(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
1203
1212
  }
1204
1213
  if (op.startsWith !== undefined) {
1205
1214
  params.push(`${escapeLike(op.startsWith)}%`);
1206
- clauses.push(`${column} LIKE $${params.length} ESCAPE '\\'`);
1215
+ clauses.push(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
1207
1216
  }
1208
1217
  if (op.endsWith !== undefined) {
1209
1218
  params.push(`%${escapeLike(op.endsWith)}`);
1210
- clauses.push(`${column} LIKE $${params.length} ESCAPE '\\'`);
1219
+ clauses.push(`${column} ${likeOp} $${params.length} ESCAPE '\\'`);
1211
1220
  }
1212
1221
  return clauses;
1213
1222
  }
@@ -1249,22 +1258,6 @@ export class QueryInterface {
1249
1258
  }
1250
1259
  return parsed;
1251
1260
  }
1252
- /**
1253
- * Fast path: parse a flat row when the table has NO date columns.
1254
- * Only renames snake_case -> camelCase via the pre-computed reverseMap.
1255
- * Skips all date coercion checks — avoids Set.has() per field per row.
1256
- * Used by findUnique/findMany fast paths when hasNoDateColumns is true.
1257
- */
1258
- parseRowFast(row) {
1259
- const parsed = {};
1260
- const reverseMap = this.tableMeta.reverseColumnMap;
1261
- const keys = Object.keys(row);
1262
- for (let i = 0; i < keys.length; i++) {
1263
- const col = keys[i];
1264
- parsed[reverseMap[col] ?? col] = row[col];
1265
- }
1266
- return parsed;
1267
- }
1268
1261
  /** Parse a row that may contain JSON nested relation columns */
1269
1262
  parseNestedRow(row, table) {
1270
1263
  const parsed = this.parseRow(row, table);
@@ -1361,7 +1354,7 @@ export class QueryInterface {
1361
1354
  targetColumns = targetMeta.allColumns.filter((col) => !omittedFields.has(col));
1362
1355
  }
1363
1356
  // Build json_build_object pairs for resolved columns
1364
- const jsonPairs = targetColumns.map((col) => `'${targetMeta.reverseColumnMap[col] ?? snakeToCamel(col)}', ${alias}.${quoteIdent(col)}`);
1357
+ const jsonPairs = targetColumns.map((col) => `'${escSingleQuote(targetMeta.reverseColumnMap[col] ?? snakeToCamel(col))}', ${alias}.${quoteIdent(col)}`);
1365
1358
  // Nested relations?
1366
1359
  if (spec !== true && spec.with) {
1367
1360
  for (const [nestedRelName, nestedSpec] of Object.entries(spec.with)) {
@@ -1372,7 +1365,7 @@ export class QueryInterface {
1372
1365
  }
1373
1366
  // Recursively build nested subquery, passing THIS alias as the parent reference
1374
1367
  const nestedSubquery = this.buildRelationSubquery(nestedRelDef, nestedSpec, params, alias, aliasCounter);
1375
- jsonPairs.push(`'${nestedRelName}', COALESCE((${nestedSubquery}), '[]'::json)`);
1368
+ jsonPairs.push(`'${escSingleQuote(nestedRelName)}', COALESCE((${nestedSubquery}), '[]'::json)`);
1376
1369
  }
1377
1370
  }
1378
1371
  const jsonObj = `json_build_object(${jsonPairs.join(', ')})`;
@@ -1430,14 +1423,14 @@ export class QueryInterface {
1430
1423
  // Inner SELECT always needs all columns for WHERE/ORDER to work; json_build_object filters later
1431
1424
  const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${quoteIdent(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
1432
1425
  // For the json_build_object, reference the inner alias — only include resolved columns
1433
- const innerJsonPairs = targetColumns.map((col) => `'${targetMeta.reverseColumnMap[col] ?? snakeToCamel(col)}', ${innerAlias}.${quoteIdent(col)}`);
1426
+ const innerJsonPairs = targetColumns.map((col) => `'${escSingleQuote(targetMeta.reverseColumnMap[col] ?? snakeToCamel(col))}', ${innerAlias}.${quoteIdent(col)}`);
1434
1427
  // Re-add nested relation subqueries referencing innerAlias
1435
1428
  if (spec !== true && spec.with) {
1436
1429
  for (const [nestedRelName] of Object.entries(spec.with)) {
1437
1430
  const nestedRelDef = targetMeta.relations[nestedRelName];
1438
1431
  if (nestedRelDef) {
1439
1432
  const nestedSub = this.buildRelationSubquery(nestedRelDef, spec.with[nestedRelName], params, innerAlias, aliasCounter);
1440
- innerJsonPairs.push(`'${nestedRelName}', COALESCE((${nestedSub}), '[]'::json)`);
1433
+ innerJsonPairs.push(`'${escSingleQuote(nestedRelName)}', COALESCE((${nestedSub}), '[]'::json)`);
1441
1434
  }
1442
1435
  }
1443
1436
  }
@@ -1452,10 +1445,10 @@ export class QueryInterface {
1452
1445
  /**
1453
1446
  * Get the Postgres type for a column (e.g. 'jsonb', 'text', '_int4').
1454
1447
  * Used to detect JSONB/array columns for specialized operators.
1448
+ * Uses pre-computed Map for O(1) lookup instead of linear scan.
1455
1449
  */
1456
1450
  getColumnPgType(column) {
1457
- const col = this.tableMeta.columns.find((c) => c.name === column);
1458
- return col?.pgType ?? 'text';
1451
+ return this.columnPgTypeMap.get(column) ?? 'text';
1459
1452
  }
1460
1453
  /**
1461
1454
  * Get the Postgres base element type for an array column.
@@ -1542,13 +1535,15 @@ export class QueryInterface {
1542
1535
  }
1543
1536
  return clauses;
1544
1537
  }
1545
- /** Get the Postgres array type for a column (used by UNNEST in createMany) */
1538
+ /**
1539
+ * Get the Postgres array type for a column (used by UNNEST in createMany).
1540
+ * Uses pre-computed Map for O(1) lookup instead of linear scan.
1541
+ */
1546
1542
  getColumnArrayType(column) {
1547
- // Find the column metadata
1548
- const col = this.tableMeta.columns.find((c) => c.name === column);
1549
- if (col)
1550
- return col.pgArrayType;
1551
- // Fallback heuristic
1543
+ const arrayType = this.columnArrayTypeMap.get(column);
1544
+ if (arrayType)
1545
+ return arrayType;
1546
+ // Fallback heuristic for unknown columns
1552
1547
  if (column === 'id' || column.endsWith('_id'))
1553
1548
  return 'bigint[]';
1554
1549
  if (column.endsWith('_at'))
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Schema Builder
2
+ * @batadata/turbine — Schema Builder
3
3
  *
4
4
  * TypeScript-first schema definition API. Define your database schema
5
5
  * as plain objects — no method chaining, no DSL. Fully type-checked,
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @example
9
9
  * ```ts
10
- * import { defineSchema } from 'turbine-orm';
10
+ * import { defineSchema } from '@batadata/turbine';
11
11
  *
12
12
  * export default defineSchema({
13
13
  * users: {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Schema Builder
2
+ * @batadata/turbine — Schema Builder
3
3
  *
4
4
  * TypeScript-first schema definition API. Define your database schema
5
5
  * as plain objects — no method chaining, no DSL. Fully type-checked,
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @example
9
9
  * ```ts
10
- * import { defineSchema } from 'turbine-orm';
10
+ * import { defineSchema } from '@batadata/turbine';
11
11
  *
12
12
  * export default defineSchema({
13
13
  * users: {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * turbine-orm — Schema SQL Generator
2
+ * @batadata/turbine — Schema SQL Generator
3
3
  *
4
4
  * Converts a SchemaDef (from defineSchema) into executable DDL statements.
5
5
  * Also provides diff and push commands for syncing schema to a live database.