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.
- package/README.md +51 -2
- package/dist/cjs/cli/config.js +161 -0
- package/dist/cjs/cli/index.js +977 -0
- package/dist/cjs/cli/migrate.js +421 -0
- package/dist/cjs/cli/ui.js +237 -0
- package/dist/cjs/client.js +449 -0
- package/dist/cjs/generate.js +301 -0
- package/dist/cjs/index.js +75 -0
- package/dist/cjs/introspect.js +289 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/pipeline.js +71 -0
- package/dist/cjs/query.js +1558 -0
- package/dist/cjs/schema-builder.js +169 -0
- package/dist/cjs/schema-sql.js +371 -0
- package/dist/cjs/schema.js +137 -0
- package/dist/cjs/serverless.js +199 -0
- package/dist/cli/config.js +1 -1
- package/dist/cli/index.js +16 -8
- package/dist/cli/migrate.d.ts +29 -5
- package/dist/cli/migrate.js +58 -35
- package/dist/cli/ui.js +1 -1
- package/dist/client.d.ts +15 -4
- package/dist/client.js +28 -15
- package/dist/generate.d.ts +1 -1
- package/dist/generate.js +13 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +1 -1
- package/dist/introspect.js +1 -1
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +1 -1
- package/dist/query.d.ts +55 -11
- package/dist/query.js +135 -140
- package/dist/schema-builder.d.ts +2 -2
- package/dist/schema-builder.js +2 -2
- package/dist/schema-sql.d.ts +1 -1
- package/dist/schema-sql.js +31 -15
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -1
- package/dist/serverless.d.ts +3 -3
- package/dist/serverless.js +4 -4
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +17 -11
- package/dist/cli/config.d.ts.map +0 -1
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/migrate.d.ts.map +0 -1
- package/dist/cli/ui.d.ts.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/generate.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/introspect.d.ts.map +0 -1
- package/dist/pipeline.d.ts.map +0 -1
- package/dist/query.d.ts.map +0 -1
- package/dist/schema-builder.d.ts.map +0 -1
- package/dist/schema-sql.d.ts.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/serverless.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
package/dist/query.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine
|
|
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
|
|
119
|
+
sqlCache = new LRUCache(1000);
|
|
76
120
|
middlewares;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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.
|
|
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.
|
|
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'] &&
|
|
218
|
+
const isSimpleWhere = !whereObj['OR'] && whereKeys.every((k) => {
|
|
141
219
|
const v = whereObj[k];
|
|
142
|
-
return v !== null && !isWhereOperator(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 ?
|
|
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.
|
|
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 (
|
|
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) =>
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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}
|
|
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}
|
|
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}
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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'))
|
package/dist/schema-builder.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine
|
|
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
|
|
10
|
+
* import { defineSchema } from '@batadata/turbine';
|
|
11
11
|
*
|
|
12
12
|
* export default defineSchema({
|
|
13
13
|
* users: {
|
package/dist/schema-builder.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* turbine
|
|
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
|
|
10
|
+
* import { defineSchema } from '@batadata/turbine';
|
|
11
11
|
*
|
|
12
12
|
* export default defineSchema({
|
|
13
13
|
* users: {
|
package/dist/schema-sql.d.ts
CHANGED