@bunnykit/orm 0.1.20 → 0.1.22
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/dist/src/connection/Connection.js +3 -0
- package/dist/src/connection/ConnectionManager.js +17 -1
- package/dist/src/model/BelongsToMany.js +9 -0
- package/dist/src/model/IdentityMap.d.ts +1 -0
- package/dist/src/model/IdentityMap.js +6 -0
- package/dist/src/model/Model.js +51 -1
- package/dist/src/model/MorphRelations.js +36 -0
- package/dist/src/query/Builder.d.ts +3 -1
- package/dist/src/query/Builder.js +125 -50
- package/dist/src/query/grammars/Grammar.d.ts +6 -6
- package/dist/src/query/grammars/Grammar.js +2 -2
- package/dist/src/query/grammars/MySqlGrammar.d.ts +5 -5
- package/dist/src/query/grammars/MySqlGrammar.js +16 -15
- package/dist/src/query/grammars/PostgresGrammar.d.ts +5 -5
- package/dist/src/query/grammars/PostgresGrammar.js +16 -15
- package/dist/src/query/grammars/SQLiteGrammar.d.ts +5 -5
- package/dist/src/query/grammars/SQLiteGrammar.js +16 -15
- package/dist/src/schema/Schema.js +22 -13
- package/dist/src/schema/grammars/Grammar.js +1 -1
- package/dist/src/types/index.d.ts +5 -1
- package/package.json +1 -1
|
@@ -111,6 +111,9 @@ export class Connection {
|
|
|
111
111
|
if (this.driverName !== "postgres") {
|
|
112
112
|
return await this.transaction(callback);
|
|
113
113
|
}
|
|
114
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/.test(setting)) {
|
|
115
|
+
throw new Error(`Invalid PostgreSQL setting name: ${setting}`);
|
|
116
|
+
}
|
|
114
117
|
return await this.transaction(async (connection) => {
|
|
115
118
|
await connection.run(`SET LOCAL ${setting} = ${connection.getGrammar().placeholder(1)}`, [tenantId]);
|
|
116
119
|
return await callback(connection);
|
|
@@ -28,12 +28,12 @@ export class ConnectionManager {
|
|
|
28
28
|
}
|
|
29
29
|
const now = Date.now();
|
|
30
30
|
const idleTimeout = poolConfig.idleTimeout || 30000;
|
|
31
|
+
// Reuse a healthy idle connection
|
|
31
32
|
while (pool.length > 0) {
|
|
32
33
|
const idx = pool.findIndex((c) => !c.inUse && (now - c.lastUsed) < idleTimeout);
|
|
33
34
|
if (idx === -1)
|
|
34
35
|
break;
|
|
35
36
|
const pooled = pool[idx];
|
|
36
|
-
pool.splice(idx, 1);
|
|
37
37
|
try {
|
|
38
38
|
await pooled.connection.query("SELECT 1");
|
|
39
39
|
pooled.inUse = true;
|
|
@@ -41,6 +41,14 @@ export class ConnectionManager {
|
|
|
41
41
|
}
|
|
42
42
|
catch {
|
|
43
43
|
await pooled.connection.close().catch(() => null);
|
|
44
|
+
pool.splice(idx, 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Clean up expired idle connections
|
|
48
|
+
for (let i = pool.length - 1; i >= 0; i--) {
|
|
49
|
+
if (!pool[i].inUse && (now - pool[i].lastUsed) >= idleTimeout) {
|
|
50
|
+
await pool[i].connection.close().catch(() => null);
|
|
51
|
+
pool.splice(i, 1);
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
if (pool.length < (poolConfig.maxConnections || 10)) {
|
|
@@ -203,6 +211,14 @@ export class ConnectionManager {
|
|
|
203
211
|
this.connections.clear();
|
|
204
212
|
this.pools.clear();
|
|
205
213
|
this.tenantCache.clear();
|
|
214
|
+
// Reject all pending waiters
|
|
215
|
+
for (const [name, poolWaiters] of this.waiters) {
|
|
216
|
+
for (const waiter of poolWaiters) {
|
|
217
|
+
clearTimeout(waiter.timer);
|
|
218
|
+
waiter.reject(new Error(`Connection pool "${name}" is closing`));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.waiters.clear();
|
|
206
222
|
for (const connection of connections) {
|
|
207
223
|
await connection.close();
|
|
208
224
|
}
|
|
@@ -23,6 +23,15 @@ export class BelongsToMany {
|
|
|
23
23
|
this.relatedPivotKey = relatedPivotKey || `${snakeCase(related.name)}_id`;
|
|
24
24
|
this.builder = related.on(parent.getConnection());
|
|
25
25
|
this.addConstraints();
|
|
26
|
+
// Wrap getResults with lazy-loading guard
|
|
27
|
+
const originalGetResults = this.getResults.bind(this);
|
|
28
|
+
this.getResults = async () => {
|
|
29
|
+
if (this.parent.constructor.preventLazyLoading) {
|
|
30
|
+
throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
|
|
31
|
+
`Eager load the relation using with().`);
|
|
32
|
+
}
|
|
33
|
+
return await originalGetResults();
|
|
34
|
+
};
|
|
26
35
|
}
|
|
27
36
|
addConstraints() {
|
|
28
37
|
const relatedTable = this.related.getTable();
|
package/dist/src/model/Model.js
CHANGED
|
@@ -332,6 +332,29 @@ export class Model {
|
|
|
332
332
|
}
|
|
333
333
|
return Reflect.set(target, prop, value, receiver);
|
|
334
334
|
},
|
|
335
|
+
has(target, prop) {
|
|
336
|
+
if (typeof prop === "string" && prop in target.$attributes)
|
|
337
|
+
return true;
|
|
338
|
+
return Reflect.has(target, prop);
|
|
339
|
+
},
|
|
340
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
341
|
+
if (typeof prop === "string" && prop in target.$attributes) {
|
|
342
|
+
return {
|
|
343
|
+
enumerable: true,
|
|
344
|
+
configurable: true,
|
|
345
|
+
value: target.getAttribute(prop),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
349
|
+
},
|
|
350
|
+
ownKeys(target) {
|
|
351
|
+
const keys = new Set(Reflect.ownKeys(target));
|
|
352
|
+
for (const key of Object.keys(target.$attributes)) {
|
|
353
|
+
if (!key.startsWith("$"))
|
|
354
|
+
keys.add(key);
|
|
355
|
+
}
|
|
356
|
+
return Array.from(keys);
|
|
357
|
+
},
|
|
335
358
|
});
|
|
336
359
|
}
|
|
337
360
|
defineAttributeProperty(key) {
|
|
@@ -345,7 +368,17 @@ export class Model {
|
|
|
345
368
|
});
|
|
346
369
|
}
|
|
347
370
|
syncAttributeProperties() {
|
|
348
|
-
|
|
371
|
+
const currentKeys = new Set(Object.keys(this.$attributes));
|
|
372
|
+
// Remove stale attribute properties
|
|
373
|
+
for (const key of Reflect.ownKeys(this)) {
|
|
374
|
+
if (key.startsWith("$") || typeof key !== "string")
|
|
375
|
+
continue;
|
|
376
|
+
const desc = Object.getOwnPropertyDescriptor(this, key);
|
|
377
|
+
if (desc && desc.get && desc.configurable && !currentKeys.has(key)) {
|
|
378
|
+
delete this[key];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
for (const key of currentKeys) {
|
|
349
382
|
this.defineAttributeProperty(key);
|
|
350
383
|
}
|
|
351
384
|
}
|
|
@@ -918,6 +951,10 @@ export class Model {
|
|
|
918
951
|
.delete();
|
|
919
952
|
this.$exists = false;
|
|
920
953
|
}
|
|
954
|
+
const identityMap = IdentityMap.current();
|
|
955
|
+
if (identityMap) {
|
|
956
|
+
IdentityMap.delete(constructor.getTable(), pk);
|
|
957
|
+
}
|
|
921
958
|
await ObserverRegistry.dispatch("deleted", this);
|
|
922
959
|
return true;
|
|
923
960
|
}
|
|
@@ -948,6 +985,10 @@ export class Model {
|
|
|
948
985
|
.where(constructor.primaryKey, pk)
|
|
949
986
|
.delete();
|
|
950
987
|
this.$exists = false;
|
|
988
|
+
const identityMap = IdentityMap.current();
|
|
989
|
+
if (identityMap) {
|
|
990
|
+
IdentityMap.delete(constructor.getTable(), pk);
|
|
991
|
+
}
|
|
951
992
|
return true;
|
|
952
993
|
}
|
|
953
994
|
async refresh() {
|
|
@@ -955,11 +996,20 @@ export class Model {
|
|
|
955
996
|
const pk = this.getAttribute(constructor.primaryKey);
|
|
956
997
|
if (!pk)
|
|
957
998
|
return this;
|
|
999
|
+
// Bypass identity map to fetch fresh data
|
|
1000
|
+
const identityMap = IdentityMap.current();
|
|
1001
|
+
if (identityMap) {
|
|
1002
|
+
IdentityMap.delete(constructor.getTable(), pk);
|
|
1003
|
+
}
|
|
958
1004
|
const result = await constructor.find(pk);
|
|
959
1005
|
if (result) {
|
|
960
1006
|
this.$attributes = result.$attributes;
|
|
961
1007
|
this.$original = { ...result.$attributes };
|
|
962
1008
|
this.syncAttributeProperties();
|
|
1009
|
+
// Ensure this instance is the canonical one in the identity map
|
|
1010
|
+
if (identityMap) {
|
|
1011
|
+
IdentityMap.set(constructor.getTable(), pk, this);
|
|
1012
|
+
}
|
|
963
1013
|
}
|
|
964
1014
|
return this;
|
|
965
1015
|
}
|
|
@@ -13,6 +13,15 @@ export class MorphTo {
|
|
|
13
13
|
this.typeColumn = `${name}_type`;
|
|
14
14
|
this.idColumn = `${name}_id`;
|
|
15
15
|
this.typeMap = typeMap;
|
|
16
|
+
// Wrap getResults with lazy-loading guard
|
|
17
|
+
const originalGetResults = this.getResults.bind(this);
|
|
18
|
+
this.getResults = async () => {
|
|
19
|
+
if (this.parent.constructor.preventLazyLoading) {
|
|
20
|
+
throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
|
|
21
|
+
`Eager load the relation using with().`);
|
|
22
|
+
}
|
|
23
|
+
return await originalGetResults();
|
|
24
|
+
};
|
|
16
25
|
}
|
|
17
26
|
async getResults() {
|
|
18
27
|
const type = this.parent.getAttribute(this.typeColumn);
|
|
@@ -99,6 +108,15 @@ export class MorphOne {
|
|
|
99
108
|
this.builder = related.on(parent.getConnection());
|
|
100
109
|
this.builder.where(this.typeColumn, this.getMorphType());
|
|
101
110
|
this.builder.where(this.idColumn, this.parent.getAttribute(this.localKey));
|
|
111
|
+
// Wrap getResults with lazy-loading guard
|
|
112
|
+
const originalGetResults = this.getResults.bind(this);
|
|
113
|
+
this.getResults = async () => {
|
|
114
|
+
if (this.parent.constructor.preventLazyLoading) {
|
|
115
|
+
throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
|
|
116
|
+
`Eager load the relation using with().`);
|
|
117
|
+
}
|
|
118
|
+
return await originalGetResults();
|
|
119
|
+
};
|
|
102
120
|
}
|
|
103
121
|
getMorphType() {
|
|
104
122
|
return this.parent.constructor.morphName || this.parent.constructor.name;
|
|
@@ -168,6 +186,15 @@ export class MorphMany {
|
|
|
168
186
|
this.builder = related.on(parent.getConnection());
|
|
169
187
|
this.builder.where(this.typeColumn, this.getMorphType());
|
|
170
188
|
this.builder.where(this.idColumn, this.parent.getAttribute(this.localKey));
|
|
189
|
+
// Wrap getResults with lazy-loading guard
|
|
190
|
+
const originalGetResults = this.getResults.bind(this);
|
|
191
|
+
this.getResults = async () => {
|
|
192
|
+
if (this.parent.constructor.preventLazyLoading) {
|
|
193
|
+
throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
|
|
194
|
+
`Eager load the relation using with().`);
|
|
195
|
+
}
|
|
196
|
+
return await originalGetResults();
|
|
197
|
+
};
|
|
171
198
|
}
|
|
172
199
|
getMorphType() {
|
|
173
200
|
return this.parent.constructor.morphName || this.parent.constructor.name;
|
|
@@ -244,6 +271,15 @@ export class MorphToMany {
|
|
|
244
271
|
this.relatedPivotKey = relatedPivotKey || `${snakeCase(related.name)}_id`;
|
|
245
272
|
this.builder = related.on(parent.getConnection());
|
|
246
273
|
this.addConstraints();
|
|
274
|
+
// Wrap getResults with lazy-loading guard
|
|
275
|
+
const originalGetResults = this.getResults.bind(this);
|
|
276
|
+
this.getResults = async () => {
|
|
277
|
+
if (this.parent.constructor.preventLazyLoading) {
|
|
278
|
+
throw new Error(`Lazy loading is prevented on ${this.parent.constructor.name}. ` +
|
|
279
|
+
`Eager load the relation using with().`);
|
|
280
|
+
}
|
|
281
|
+
return await originalGetResults();
|
|
282
|
+
};
|
|
247
283
|
}
|
|
248
284
|
addConstraints() {
|
|
249
285
|
const relatedTable = this.related.getTable();
|
|
@@ -134,7 +134,7 @@ export declare class Builder<T = Record<string, any>> {
|
|
|
134
134
|
private addBinding;
|
|
135
135
|
private compileWhereClause;
|
|
136
136
|
private compileWheres;
|
|
137
|
-
private
|
|
137
|
+
private compileWhereClauses;
|
|
138
138
|
private compileOrders;
|
|
139
139
|
private compileGroups;
|
|
140
140
|
private compileHavings;
|
|
@@ -161,6 +161,8 @@ export declare class Builder<T = Record<string, any>> {
|
|
|
161
161
|
chunk(count: number, callback: (items: T[]) => void | Promise<void>): Promise<void>;
|
|
162
162
|
each(count: number, callback: (item: T) => void | Promise<void>): Promise<void>;
|
|
163
163
|
cursor(chunkSize?: number): AsyncGenerator<T>;
|
|
164
|
+
private getResultAccessColumn;
|
|
165
|
+
private compileCursorWheres;
|
|
164
166
|
lazy(count?: number): AsyncGenerator<T>;
|
|
165
167
|
insert(data: ModelAttributeInput<T> | ModelAttributeInput<T>[]): Promise<any>;
|
|
166
168
|
insertGetId(data: ModelAttributeInput<T>, idColumn?: ModelColumn<T>): Promise<any>;
|
|
@@ -65,8 +65,7 @@ export class Builder {
|
|
|
65
65
|
const nested = new Builder(this.connection, this.tableName);
|
|
66
66
|
callback(nested);
|
|
67
67
|
if (nested.wheres.length > 0) {
|
|
68
|
-
|
|
69
|
-
this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
|
|
68
|
+
this.wheres.push({ type: "nested", column: "", query: nested.wheres, boolean, scope: undefined });
|
|
70
69
|
}
|
|
71
70
|
return this;
|
|
72
71
|
}
|
|
@@ -182,10 +181,7 @@ export class Builder {
|
|
|
182
181
|
return this.whereRaw(sql, "or", scope);
|
|
183
182
|
}
|
|
184
183
|
whereJsonContains(column, value, boolean = "and", not = false) {
|
|
185
|
-
|
|
186
|
-
if (not)
|
|
187
|
-
sql = `NOT (${sql})`;
|
|
188
|
-
this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
|
|
184
|
+
this.wheres.push({ type: "json_contains", column, value, boolean, scope: undefined, not });
|
|
189
185
|
return this;
|
|
190
186
|
}
|
|
191
187
|
whereJsonLength(column, operator = "=", value, boolean = "and", not = false) {
|
|
@@ -193,41 +189,31 @@ export class Builder {
|
|
|
193
189
|
value = operator;
|
|
194
190
|
operator = "=";
|
|
195
191
|
}
|
|
196
|
-
|
|
197
|
-
if (not)
|
|
198
|
-
sql = `NOT (${sql})`;
|
|
199
|
-
this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
|
|
192
|
+
this.wheres.push({ type: "json_length", column, operator: String(operator), value, boolean, scope: undefined, not });
|
|
200
193
|
return this;
|
|
201
194
|
}
|
|
202
195
|
whereLike(column, value, boolean = "and", not = false) {
|
|
203
|
-
|
|
204
|
-
this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
|
|
196
|
+
this.wheres.push({ type: "like", column, value, boolean, scope: undefined, not });
|
|
205
197
|
return this;
|
|
206
198
|
}
|
|
207
199
|
whereNotLike(column, value) {
|
|
208
200
|
return this.whereLike(column, value, "and", true);
|
|
209
201
|
}
|
|
210
202
|
whereRegexp(column, value, boolean = "and", not = false) {
|
|
211
|
-
|
|
212
|
-
this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
|
|
203
|
+
this.wheres.push({ type: "regexp", column, value, boolean, scope: undefined, not });
|
|
213
204
|
return this;
|
|
214
205
|
}
|
|
215
206
|
whereFullText(columns, value, boolean = "and", not = false) {
|
|
216
207
|
const cols = Array.isArray(columns) ? columns : [columns];
|
|
217
|
-
|
|
218
|
-
if (not)
|
|
219
|
-
sql = `NOT (${sql})`;
|
|
220
|
-
this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
|
|
208
|
+
this.wheres.push({ type: "fulltext", column: "", columns: cols, value, boolean, scope: undefined, not });
|
|
221
209
|
return this;
|
|
222
210
|
}
|
|
223
211
|
whereAll(columns, operator, value, boolean = "and") {
|
|
224
|
-
|
|
225
|
-
this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
|
|
212
|
+
this.wheres.push({ type: "all", column: "", columns: columns, operator, value, boolean, scope: undefined });
|
|
226
213
|
return this;
|
|
227
214
|
}
|
|
228
215
|
whereAny(columns, operator, value, boolean = "and") {
|
|
229
|
-
|
|
230
|
-
this.wheres.push({ type: "raw", column: `(${sql})`, boolean, scope: undefined });
|
|
216
|
+
this.wheres.push({ type: "any", column: "", columns: columns, operator, value, boolean, scope: undefined });
|
|
231
217
|
return this;
|
|
232
218
|
}
|
|
233
219
|
orderBy(column, direction = "asc") {
|
|
@@ -459,6 +445,7 @@ export class Builder {
|
|
|
459
445
|
cloned.fromRaw = this.fromRaw;
|
|
460
446
|
cloned.updateJoins = [...this.updateJoins];
|
|
461
447
|
cloned.bindings = [...this.bindings];
|
|
448
|
+
cloned.parameterize = this.parameterize;
|
|
462
449
|
return cloned;
|
|
463
450
|
}
|
|
464
451
|
wrapColumn(value) {
|
|
@@ -496,6 +483,57 @@ export class Builder {
|
|
|
496
483
|
else if (where.type === "raw") {
|
|
497
484
|
return `${prefix} ${where.column}`;
|
|
498
485
|
}
|
|
486
|
+
else if (where.type === "nested") {
|
|
487
|
+
const sql = this.compileWhereClauses(where.query || [], "");
|
|
488
|
+
return `${prefix} (${sql})`;
|
|
489
|
+
}
|
|
490
|
+
else if (where.type === "like") {
|
|
491
|
+
const sql = this.grammar.compileLike(this.grammar.wrap(where.column), where.value, !!where.not, this.parameterize ? (v) => this.addBinding(v) : undefined);
|
|
492
|
+
return `${prefix} ${sql}`;
|
|
493
|
+
}
|
|
494
|
+
else if (where.type === "regexp") {
|
|
495
|
+
const sql = this.grammar.compileRegexp(this.grammar.wrap(where.column), where.value, !!where.not, this.parameterize ? (v) => this.addBinding(v) : undefined);
|
|
496
|
+
return `${prefix} ${sql}`;
|
|
497
|
+
}
|
|
498
|
+
else if (where.type === "fulltext") {
|
|
499
|
+
const cols = (where.columns || []).map((c) => this.grammar.wrap(c));
|
|
500
|
+
let sql = this.grammar.compileFullText(cols, where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
|
|
501
|
+
if (where.not)
|
|
502
|
+
sql = `NOT (${sql})`;
|
|
503
|
+
return `${prefix} ${sql}`;
|
|
504
|
+
}
|
|
505
|
+
else if (where.type === "json_contains") {
|
|
506
|
+
let sql = this.grammar.compileJsonContains(this.grammar.wrap(where.column), where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
|
|
507
|
+
if (where.not)
|
|
508
|
+
sql = `NOT (${sql})`;
|
|
509
|
+
return `${prefix} ${sql}`;
|
|
510
|
+
}
|
|
511
|
+
else if (where.type === "json_length") {
|
|
512
|
+
let sql = this.grammar.compileJsonLength(this.grammar.wrap(where.column), where.operator || "=", where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
|
|
513
|
+
if (where.not)
|
|
514
|
+
sql = `NOT (${sql})`;
|
|
515
|
+
return `${prefix} ${sql}`;
|
|
516
|
+
}
|
|
517
|
+
else if (where.type === "date") {
|
|
518
|
+
const sql = this.grammar.compileDateWhere(where.dateType || "date", this.grammar.wrap(where.column), where.operator || "=", where.value, this.parameterize ? (v) => this.addBinding(v) : undefined);
|
|
519
|
+
return `${prefix} ${sql}`;
|
|
520
|
+
}
|
|
521
|
+
else if (where.type === "all") {
|
|
522
|
+
const cols = (where.columns || []).map((c) => this.grammar.wrap(c));
|
|
523
|
+
const inner = cols.map((c) => {
|
|
524
|
+
const val = this.parameterize ? this.addBinding(where.value) : this.grammar.escape(where.value);
|
|
525
|
+
return `${c} ${where.operator} ${val}`;
|
|
526
|
+
}).join(" AND ");
|
|
527
|
+
return `${prefix} (${inner})`;
|
|
528
|
+
}
|
|
529
|
+
else if (where.type === "any") {
|
|
530
|
+
const cols = (where.columns || []).map((c) => this.grammar.wrap(c));
|
|
531
|
+
const inner = cols.map((c) => {
|
|
532
|
+
const val = this.parameterize ? this.addBinding(where.value) : this.grammar.escape(where.value);
|
|
533
|
+
return `${c} ${where.operator} ${val}`;
|
|
534
|
+
}).join(" OR ");
|
|
535
|
+
return `${prefix} (${inner})`;
|
|
536
|
+
}
|
|
499
537
|
else if (where.type === "column") {
|
|
500
538
|
return `${prefix} ${this.grammar.wrap(where.column)} ${where.operator} ${this.grammar.wrap(where.value)}`;
|
|
501
539
|
}
|
|
@@ -505,20 +543,15 @@ export class Builder {
|
|
|
505
543
|
return "";
|
|
506
544
|
}
|
|
507
545
|
compileWheres() {
|
|
508
|
-
|
|
509
|
-
return "";
|
|
510
|
-
const clauses = this.wheres.map((where, index) => {
|
|
511
|
-
const prefix = index === 0 ? "WHERE" : where.boolean.toUpperCase();
|
|
512
|
-
return this.compileWhereClause(where, prefix);
|
|
513
|
-
});
|
|
514
|
-
return clauses.join(" ");
|
|
546
|
+
return this.compileWhereClauses(this.wheres, "WHERE");
|
|
515
547
|
}
|
|
516
|
-
|
|
517
|
-
if (
|
|
548
|
+
compileWhereClauses(wheres, firstPrefix) {
|
|
549
|
+
if (wheres.length === 0)
|
|
518
550
|
return "";
|
|
519
|
-
const clauses =
|
|
520
|
-
const prefix = index === 0 ? "" : where.boolean.toUpperCase();
|
|
521
|
-
|
|
551
|
+
const clauses = wheres.map((where, index) => {
|
|
552
|
+
const prefix = index === 0 ? "WHERE" : where.boolean.toUpperCase();
|
|
553
|
+
const adjustedPrefix = index === 0 ? firstPrefix : prefix;
|
|
554
|
+
return this.compileWhereClause(where, adjustedPrefix);
|
|
522
555
|
});
|
|
523
556
|
return clauses.join(" ").trim();
|
|
524
557
|
}
|
|
@@ -740,24 +773,32 @@ export class Builder {
|
|
|
740
773
|
async *cursor(chunkSize = 1000) {
|
|
741
774
|
const model = this.model;
|
|
742
775
|
const primaryKey = model ? model.primaryKey || "id" : "id";
|
|
776
|
+
// Cursor pagination is incompatible with random ordering
|
|
777
|
+
if (this.randomOrderFlag) {
|
|
778
|
+
throw new Error("cursor() does not support inRandomOrder(). Use lazy() instead.");
|
|
779
|
+
}
|
|
743
780
|
const orderColumn = this.orders[0]?.column || primaryKey;
|
|
744
781
|
const orderDirection = this.orders[0]?.direction || "asc";
|
|
745
|
-
let
|
|
782
|
+
let lastValues = undefined;
|
|
746
783
|
while (true) {
|
|
747
784
|
const builder = this.clone();
|
|
748
|
-
|
|
785
|
+
// Preserve multi-column ORDER BY, appending PK tie-breaker if not present
|
|
786
|
+
builder.orders = this.orders.length > 0 ? [...this.orders] : [{ column: orderColumn, direction: orderDirection }];
|
|
787
|
+
const hasPkOrder = builder.orders.some((o) => o.column === primaryKey);
|
|
788
|
+
if (!hasPkOrder) {
|
|
789
|
+
builder.orders.push({ column: primaryKey, direction: orderDirection });
|
|
790
|
+
}
|
|
749
791
|
builder.offsetValue = undefined;
|
|
750
792
|
builder.limitValue = chunkSize;
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
builder.wheres.
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
});
|
|
793
|
+
if (lastValues !== undefined) {
|
|
794
|
+
// Parenthesize existing wheres when appending cursor condition to preserve OR precedence
|
|
795
|
+
if (builder.wheres.length > 0) {
|
|
796
|
+
const hasOr = builder.wheres.some((w) => w.boolean === "or");
|
|
797
|
+
if (hasOr) {
|
|
798
|
+
builder.wheres = [{ type: "nested", column: "", query: builder.wheres, boolean: "and", scope: undefined }];
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
builder.wheres.push({ type: "nested", column: "", query: this.compileCursorWheres(builder.orders, lastValues), boolean: "and", scope: undefined });
|
|
761
802
|
}
|
|
762
803
|
const items = await builder.get();
|
|
763
804
|
if (items.length === 0)
|
|
@@ -768,11 +809,46 @@ export class Builder {
|
|
|
768
809
|
if (items.length < chunkSize)
|
|
769
810
|
break;
|
|
770
811
|
const lastItem = items[items.length - 1];
|
|
771
|
-
|
|
772
|
-
? lastItem[
|
|
812
|
+
lastValues = lastItem && typeof lastItem === "object"
|
|
813
|
+
? builder.orders.map((order) => lastItem[this.getResultAccessColumn(order.column)])
|
|
773
814
|
: undefined;
|
|
774
815
|
}
|
|
775
816
|
}
|
|
817
|
+
getResultAccessColumn(column) {
|
|
818
|
+
return column.includes(".") ? column.split(".").at(-1) : column;
|
|
819
|
+
}
|
|
820
|
+
compileCursorWheres(orders, values, index = 0) {
|
|
821
|
+
const order = orders[index];
|
|
822
|
+
const op = order.direction === "asc" ? ">" : "<";
|
|
823
|
+
const clauses = [{
|
|
824
|
+
type: "basic",
|
|
825
|
+
column: order.column,
|
|
826
|
+
operator: op,
|
|
827
|
+
value: values[index],
|
|
828
|
+
boolean: "and",
|
|
829
|
+
scope: undefined,
|
|
830
|
+
}];
|
|
831
|
+
if (index < orders.length - 1) {
|
|
832
|
+
clauses.push({
|
|
833
|
+
type: "nested",
|
|
834
|
+
column: "",
|
|
835
|
+
query: [
|
|
836
|
+
{
|
|
837
|
+
type: "basic",
|
|
838
|
+
column: order.column,
|
|
839
|
+
operator: "=",
|
|
840
|
+
value: values[index],
|
|
841
|
+
boolean: "and",
|
|
842
|
+
scope: undefined,
|
|
843
|
+
},
|
|
844
|
+
...this.compileCursorWheres(orders, values, index + 1),
|
|
845
|
+
],
|
|
846
|
+
boolean: "or",
|
|
847
|
+
scope: undefined,
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
return clauses;
|
|
851
|
+
}
|
|
776
852
|
async *lazy(count = 1000) {
|
|
777
853
|
let page = 1;
|
|
778
854
|
while (true) {
|
|
@@ -958,8 +1034,7 @@ export class Builder {
|
|
|
958
1034
|
value = operator;
|
|
959
1035
|
operator = "=";
|
|
960
1036
|
}
|
|
961
|
-
|
|
962
|
-
this.wheres.push({ type: "raw", column: sql, boolean, scope: undefined });
|
|
1037
|
+
this.wheres.push({ type: "date", column: column, operator, value, boolean, scope: undefined, dateType: type });
|
|
963
1038
|
return this;
|
|
964
1039
|
}
|
|
965
1040
|
getModelRelation(relationName) {
|
|
@@ -6,15 +6,15 @@ export declare abstract class Grammar {
|
|
|
6
6
|
abstract compileRandomOrder(): string;
|
|
7
7
|
compileOffset(offset: number, _limit?: number): string;
|
|
8
8
|
compileLock(lockMode?: string): string;
|
|
9
|
-
abstract compileDateWhere(type: string, column: string, operator: string, value: any): string;
|
|
9
|
+
abstract compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
|
|
10
10
|
abstract compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
|
|
11
11
|
abstract compileUpsert(table: string, columns: string[], values: string[], uniqueBy: string[], updateColumns: string[]): string;
|
|
12
12
|
compileUpdate(table: string, sets: string[], wheres: string, joins?: string[]): string;
|
|
13
13
|
compileDelete(table: string, wheres: string, joins?: string[], limit?: number): string;
|
|
14
|
-
abstract compileJsonContains(column: string, value: any): string;
|
|
15
|
-
abstract compileJsonLength(column: string, operator: string, value: any): string;
|
|
16
|
-
compileLike(column: string, value: string, not: boolean): string;
|
|
17
|
-
abstract compileRegexp(column: string, value: string, not: boolean): string;
|
|
18
|
-
abstract compileFullText(columns: string[], value: string): string;
|
|
14
|
+
abstract compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
|
|
15
|
+
abstract compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
|
|
16
|
+
compileLike(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
|
|
17
|
+
abstract compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
|
|
18
|
+
abstract compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
|
|
19
19
|
abstract compileExplain(sql: string): string;
|
|
20
20
|
}
|
|
@@ -40,8 +40,8 @@ export class Grammar {
|
|
|
40
40
|
sql += ` LIMIT ${limit}`;
|
|
41
41
|
return sql.trim();
|
|
42
42
|
}
|
|
43
|
-
compileLike(column, value, not) {
|
|
43
|
+
compileLike(column, value, not, binding) {
|
|
44
44
|
const op = not ? "NOT LIKE" : "LIKE";
|
|
45
|
-
return `${column} ${op} ${this.escape(value)}`;
|
|
45
|
+
return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -3,12 +3,12 @@ export declare class MySqlGrammar extends Grammar {
|
|
|
3
3
|
wrap(value: string): string;
|
|
4
4
|
placeholder(_index: number): string;
|
|
5
5
|
compileRandomOrder(): string;
|
|
6
|
-
compileDateWhere(type: string, column: string, operator: string, value: any): string;
|
|
6
|
+
compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
|
|
7
7
|
compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
|
|
8
8
|
compileUpsert(table: string, columns: string[], values: string[], _uniqueBy: string[], updateColumns: string[]): string;
|
|
9
|
-
compileJsonContains(column: string, value: any): string;
|
|
10
|
-
compileJsonLength(column: string, operator: string, value: any): string;
|
|
11
|
-
compileRegexp(column: string, value: string, not: boolean): string;
|
|
12
|
-
compileFullText(columns: string[], value: string): string;
|
|
9
|
+
compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
|
|
10
|
+
compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
|
|
11
|
+
compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
|
|
12
|
+
compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
|
|
13
13
|
compileExplain(sql: string): string;
|
|
14
14
|
}
|
|
@@ -18,20 +18,21 @@ export class MySqlGrammar extends Grammar {
|
|
|
18
18
|
compileRandomOrder() {
|
|
19
19
|
return "ORDER BY RAND()";
|
|
20
20
|
}
|
|
21
|
-
compileDateWhere(type, column, operator, value) {
|
|
21
|
+
compileDateWhere(type, column, operator, value, binding) {
|
|
22
|
+
const val = binding ? binding(value) : this.escape(value);
|
|
22
23
|
switch (type) {
|
|
23
24
|
case "date":
|
|
24
|
-
return `DATE(${column}) ${operator} ${
|
|
25
|
+
return `DATE(${column}) ${operator} ${val}`;
|
|
25
26
|
case "day":
|
|
26
|
-
return `DAY(${column}) ${operator} ${
|
|
27
|
+
return `DAY(${column}) ${operator} ${val}`;
|
|
27
28
|
case "month":
|
|
28
|
-
return `MONTH(${column}) ${operator} ${
|
|
29
|
+
return `MONTH(${column}) ${operator} ${val}`;
|
|
29
30
|
case "year":
|
|
30
|
-
return `YEAR(${column}) ${operator} ${
|
|
31
|
+
return `YEAR(${column}) ${operator} ${val}`;
|
|
31
32
|
case "time":
|
|
32
|
-
return `TIME(${column}) ${operator} ${
|
|
33
|
+
return `TIME(${column}) ${operator} ${val}`;
|
|
33
34
|
default:
|
|
34
|
-
return `${column} ${operator} ${
|
|
35
|
+
return `${column} ${operator} ${val}`;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
compileInsertOrIgnore(table, columns, values) {
|
|
@@ -43,18 +44,18 @@ export class MySqlGrammar extends Grammar {
|
|
|
43
44
|
.join(", ");
|
|
44
45
|
return `INSERT INTO ${table} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")} ON DUPLICATE KEY UPDATE ${updateCols}`;
|
|
45
46
|
}
|
|
46
|
-
compileJsonContains(column, value) {
|
|
47
|
-
return `JSON_CONTAINS(${column}, ${this.escape(JSON.stringify(value))})`;
|
|
47
|
+
compileJsonContains(column, value, binding) {
|
|
48
|
+
return `JSON_CONTAINS(${column}, ${binding ? binding(JSON.stringify(value)) : this.escape(JSON.stringify(value))})`;
|
|
48
49
|
}
|
|
49
|
-
compileJsonLength(column, operator, value) {
|
|
50
|
-
return `JSON_LENGTH(${column}) ${operator} ${this.escape(value)}`;
|
|
50
|
+
compileJsonLength(column, operator, value, binding) {
|
|
51
|
+
return `JSON_LENGTH(${column}) ${operator} ${binding ? binding(value) : this.escape(value)}`;
|
|
51
52
|
}
|
|
52
|
-
compileRegexp(column, value, not) {
|
|
53
|
+
compileRegexp(column, value, not, binding) {
|
|
53
54
|
const op = not ? "NOT REGEXP" : "REGEXP";
|
|
54
|
-
return `${column} ${op} ${this.escape(value)}`;
|
|
55
|
+
return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
|
|
55
56
|
}
|
|
56
|
-
compileFullText(columns, value) {
|
|
57
|
-
return `MATCH (${columns.join(", ")}) AGAINST (${this.escape(value)})`;
|
|
57
|
+
compileFullText(columns, value, binding) {
|
|
58
|
+
return `MATCH (${columns.join(", ")}) AGAINST (${binding ? binding(value) : this.escape(value)})`;
|
|
58
59
|
}
|
|
59
60
|
compileExplain(sql) {
|
|
60
61
|
return `EXPLAIN ${sql}`;
|
|
@@ -3,12 +3,12 @@ export declare class PostgresGrammar extends Grammar {
|
|
|
3
3
|
wrap(value: string): string;
|
|
4
4
|
placeholder(index: number): string;
|
|
5
5
|
compileRandomOrder(): string;
|
|
6
|
-
compileDateWhere(type: string, column: string, operator: string, value: any): string;
|
|
6
|
+
compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
|
|
7
7
|
compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
|
|
8
8
|
compileUpsert(table: string, columns: string[], values: string[], uniqueBy: string[], updateColumns: string[]): string;
|
|
9
|
-
compileJsonContains(column: string, value: any): string;
|
|
10
|
-
compileJsonLength(column: string, operator: string, value: any): string;
|
|
11
|
-
compileRegexp(column: string, value: string, not: boolean): string;
|
|
12
|
-
compileFullText(columns: string[], value: string): string;
|
|
9
|
+
compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
|
|
10
|
+
compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
|
|
11
|
+
compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
|
|
12
|
+
compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
|
|
13
13
|
compileExplain(sql: string): string;
|
|
14
14
|
}
|
|
@@ -18,20 +18,21 @@ export class PostgresGrammar extends Grammar {
|
|
|
18
18
|
compileRandomOrder() {
|
|
19
19
|
return "ORDER BY RANDOM()";
|
|
20
20
|
}
|
|
21
|
-
compileDateWhere(type, column, operator, value) {
|
|
21
|
+
compileDateWhere(type, column, operator, value, binding) {
|
|
22
|
+
const val = binding ? binding(value) : this.escape(value);
|
|
22
23
|
switch (type) {
|
|
23
24
|
case "date":
|
|
24
|
-
return `(${column})::date ${operator} ${
|
|
25
|
+
return `(${column})::date ${operator} ${val}`;
|
|
25
26
|
case "day":
|
|
26
|
-
return `EXTRACT(DAY FROM ${column}) ${operator} ${
|
|
27
|
+
return `EXTRACT(DAY FROM ${column}) ${operator} ${val}`;
|
|
27
28
|
case "month":
|
|
28
|
-
return `EXTRACT(MONTH FROM ${column}) ${operator} ${
|
|
29
|
+
return `EXTRACT(MONTH FROM ${column}) ${operator} ${val}`;
|
|
29
30
|
case "year":
|
|
30
|
-
return `EXTRACT(YEAR FROM ${column}) ${operator} ${
|
|
31
|
+
return `EXTRACT(YEAR FROM ${column}) ${operator} ${val}`;
|
|
31
32
|
case "time":
|
|
32
|
-
return `(${column})::time ${operator} ${
|
|
33
|
+
return `(${column})::time ${operator} ${val}`;
|
|
33
34
|
default:
|
|
34
|
-
return `${column} ${operator} ${
|
|
35
|
+
return `${column} ${operator} ${val}`;
|
|
35
36
|
}
|
|
36
37
|
}
|
|
37
38
|
compileInsertOrIgnore(table, columns, values) {
|
|
@@ -43,21 +44,21 @@ export class PostgresGrammar extends Grammar {
|
|
|
43
44
|
.join(", ");
|
|
44
45
|
return `INSERT INTO ${table} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")} ON CONFLICT (${uniqueBy.map((c) => this.wrap(c)).join(", ")}) DO UPDATE SET ${updateCols}`;
|
|
45
46
|
}
|
|
46
|
-
compileJsonContains(column, value) {
|
|
47
|
-
return `${column} @> ${this.escape(JSON.stringify([value]))}`;
|
|
47
|
+
compileJsonContains(column, value, binding) {
|
|
48
|
+
return `${column} @> ${binding ? binding(JSON.stringify([value])) : this.escape(JSON.stringify([value]))}`;
|
|
48
49
|
}
|
|
49
|
-
compileJsonLength(column, operator, value) {
|
|
50
|
-
return `jsonb_array_length(${column}) ${operator} ${this.escape(value)}`;
|
|
50
|
+
compileJsonLength(column, operator, value, binding) {
|
|
51
|
+
return `jsonb_array_length(${column}) ${operator} ${binding ? binding(value) : this.escape(value)}`;
|
|
51
52
|
}
|
|
52
|
-
compileRegexp(column, value, not) {
|
|
53
|
+
compileRegexp(column, value, not, binding) {
|
|
53
54
|
const op = not ? "!~" : "~";
|
|
54
|
-
return `${column} ${op} ${this.escape(value)}`;
|
|
55
|
+
return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
|
|
55
56
|
}
|
|
56
|
-
compileFullText(columns, value) {
|
|
57
|
+
compileFullText(columns, value, binding) {
|
|
57
58
|
const cols = columns.length > 1
|
|
58
59
|
? `concat_ws(' ', ${columns.join(", ")})`
|
|
59
60
|
: columns[0];
|
|
60
|
-
return `to_tsvector('english', ${cols}) @@ plainto_tsquery('english', ${this.escape(value)})`;
|
|
61
|
+
return `to_tsvector('english', ${cols}) @@ plainto_tsquery('english', ${binding ? binding(value) : this.escape(value)})`;
|
|
61
62
|
}
|
|
62
63
|
compileExplain(sql) {
|
|
63
64
|
return `EXPLAIN (FORMAT JSON) ${sql}`;
|
|
@@ -4,12 +4,12 @@ export declare class SQLiteGrammar extends Grammar {
|
|
|
4
4
|
placeholder(_index: number): string;
|
|
5
5
|
compileRandomOrder(): string;
|
|
6
6
|
compileOffset(offset: number, limit?: number): string;
|
|
7
|
-
compileDateWhere(type: string, column: string, operator: string, value: any): string;
|
|
7
|
+
compileDateWhere(type: string, column: string, operator: string, value: any, binding?: (value: any) => string): string;
|
|
8
8
|
compileInsertOrIgnore(table: string, columns: string[], values: string[]): string;
|
|
9
9
|
compileUpsert(table: string, columns: string[], values: string[], uniqueBy: string[], updateColumns: string[]): string;
|
|
10
|
-
compileJsonContains(column: string, value: any): string;
|
|
11
|
-
compileJsonLength(column: string, operator: string, value: any): string;
|
|
12
|
-
compileRegexp(column: string, value: string, not: boolean): string;
|
|
13
|
-
compileFullText(columns: string[], value: string): string;
|
|
10
|
+
compileJsonContains(column: string, value: any, binding?: (value: any) => string): string;
|
|
11
|
+
compileJsonLength(column: string, operator: string, value: any, binding?: (value: any) => string): string;
|
|
12
|
+
compileRegexp(column: string, value: string, not: boolean, binding?: (value: any) => string): string;
|
|
13
|
+
compileFullText(columns: string[], value: string, binding?: (value: any) => string): string;
|
|
14
14
|
compileExplain(sql: string): string;
|
|
15
15
|
}
|
|
@@ -22,20 +22,21 @@ export class SQLiteGrammar extends Grammar {
|
|
|
22
22
|
const limitSql = limit === undefined ? "LIMIT -1 " : "";
|
|
23
23
|
return `${limitSql}OFFSET ${offset}`;
|
|
24
24
|
}
|
|
25
|
-
compileDateWhere(type, column, operator, value) {
|
|
25
|
+
compileDateWhere(type, column, operator, value, binding) {
|
|
26
|
+
const val = binding ? binding(value) : this.escape(value);
|
|
26
27
|
switch (type) {
|
|
27
28
|
case "date":
|
|
28
|
-
return `date(${column}) ${operator} ${
|
|
29
|
+
return `date(${column}) ${operator} ${val}`;
|
|
29
30
|
case "day":
|
|
30
|
-
return `CAST(strftime('%d', ${column}) AS INTEGER) ${operator} ${
|
|
31
|
+
return `CAST(strftime('%d', ${column}) AS INTEGER) ${operator} ${val}`;
|
|
31
32
|
case "month":
|
|
32
|
-
return `CAST(strftime('%m', ${column}) AS INTEGER) ${operator} ${
|
|
33
|
+
return `CAST(strftime('%m', ${column}) AS INTEGER) ${operator} ${val}`;
|
|
33
34
|
case "year":
|
|
34
|
-
return `CAST(strftime('%Y', ${column}) AS INTEGER) ${operator} ${
|
|
35
|
+
return `CAST(strftime('%Y', ${column}) AS INTEGER) ${operator} ${val}`;
|
|
35
36
|
case "time":
|
|
36
|
-
return `time(${column}) ${operator} ${
|
|
37
|
+
return `time(${column}) ${operator} ${val}`;
|
|
37
38
|
default:
|
|
38
|
-
return `${column} ${operator} ${
|
|
39
|
+
return `${column} ${operator} ${val}`;
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
compileInsertOrIgnore(table, columns, values) {
|
|
@@ -47,18 +48,18 @@ export class SQLiteGrammar extends Grammar {
|
|
|
47
48
|
.join(", ");
|
|
48
49
|
return `INSERT INTO ${table} (${columns.map((c) => this.wrap(c)).join(", ")}) VALUES ${values.join(", ")} ON CONFLICT(${uniqueBy.map((c) => this.wrap(c)).join(", ")}) DO UPDATE SET ${updateCols}`;
|
|
49
50
|
}
|
|
50
|
-
compileJsonContains(column, value) {
|
|
51
|
-
return `${column} IN (SELECT value FROM json_each(${column})) AND ${this.escape(value)} IN (SELECT value FROM json_each(${column}))`;
|
|
51
|
+
compileJsonContains(column, value, binding) {
|
|
52
|
+
return `${column} IN (SELECT value FROM json_each(${column})) AND ${binding ? binding(value) : this.escape(value)} IN (SELECT value FROM json_each(${column}))`;
|
|
52
53
|
}
|
|
53
|
-
compileJsonLength(column, operator, value) {
|
|
54
|
-
return `(SELECT COUNT(*) FROM json_each(${column})) ${operator} ${this.escape(value)}`;
|
|
54
|
+
compileJsonLength(column, operator, value, binding) {
|
|
55
|
+
return `(SELECT COUNT(*) FROM json_each(${column})) ${operator} ${binding ? binding(value) : this.escape(value)}`;
|
|
55
56
|
}
|
|
56
|
-
compileRegexp(column, value, not) {
|
|
57
|
+
compileRegexp(column, value, not, binding) {
|
|
57
58
|
const op = not ? "NOT REGEXP" : "REGEXP";
|
|
58
|
-
return `${column} ${op} ${this.escape(value)}`;
|
|
59
|
+
return `${column} ${op} ${binding ? binding(value) : this.escape(value)}`;
|
|
59
60
|
}
|
|
60
|
-
compileFullText(columns, value) {
|
|
61
|
-
return columns.map((c) => `${this.wrap(c)} LIKE ${this.escape(`%${value}%`)}`).join(" OR ");
|
|
61
|
+
compileFullText(columns, value, binding) {
|
|
62
|
+
return columns.map((c) => `${this.wrap(c)} LIKE ${binding ? binding(`%${value}%`) : this.escape(`%${value}%`)}`).join(" OR ");
|
|
62
63
|
}
|
|
63
64
|
compileExplain(sql) {
|
|
64
65
|
return `EXPLAIN QUERY PLAN ${sql}`;
|
|
@@ -135,48 +135,57 @@ export class Schema {
|
|
|
135
135
|
const driver = connection.getDriverName();
|
|
136
136
|
const schema = connection.getSchema() || "public";
|
|
137
137
|
let sql;
|
|
138
|
+
let bindings = [];
|
|
138
139
|
if (driver === "sqlite") {
|
|
139
|
-
sql =
|
|
140
|
+
sql = "SELECT name FROM sqlite_master WHERE type='table' AND name = ?";
|
|
141
|
+
bindings = [table];
|
|
140
142
|
}
|
|
141
143
|
else if (driver === "mysql") {
|
|
142
|
-
sql =
|
|
144
|
+
sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?";
|
|
145
|
+
bindings = [table];
|
|
143
146
|
}
|
|
144
147
|
else {
|
|
145
|
-
sql =
|
|
148
|
+
sql = "SELECT table_name FROM information_schema.tables WHERE table_schema = $1 AND table_name = $2";
|
|
149
|
+
bindings = [schema, table];
|
|
146
150
|
}
|
|
147
|
-
const result = await connection.query(sql);
|
|
151
|
+
const result = await connection.query(sql, bindings);
|
|
148
152
|
return result.length > 0;
|
|
149
153
|
}
|
|
150
154
|
static async hasColumn(table, column) {
|
|
151
155
|
const connection = this.getConnection();
|
|
152
156
|
const driver = connection.getDriverName();
|
|
153
157
|
const schema = connection.getSchema() || "public";
|
|
158
|
+
const grammar = this.getGrammar();
|
|
154
159
|
let sql;
|
|
160
|
+
let bindings = [];
|
|
155
161
|
if (driver === "sqlite") {
|
|
156
|
-
sql = `PRAGMA table_info(${table})`;
|
|
162
|
+
sql = `PRAGMA table_info(${grammar.wrap(table)})`;
|
|
157
163
|
const result = await connection.query(sql);
|
|
158
164
|
return result.some((row) => row.name === column);
|
|
159
165
|
}
|
|
160
166
|
else if (driver === "mysql") {
|
|
161
|
-
sql =
|
|
167
|
+
sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?";
|
|
168
|
+
bindings = [table, column];
|
|
162
169
|
}
|
|
163
170
|
else {
|
|
164
|
-
sql =
|
|
171
|
+
sql = "SELECT column_name FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 AND column_name = $3";
|
|
172
|
+
bindings = [schema, table, column];
|
|
165
173
|
}
|
|
166
|
-
const result = await connection.query(sql);
|
|
174
|
+
const result = await connection.query(sql, bindings);
|
|
167
175
|
return result.length > 0;
|
|
168
176
|
}
|
|
169
177
|
static async getColumn(table, column) {
|
|
170
178
|
const connection = this.getConnection();
|
|
171
179
|
const driver = connection.getDriverName();
|
|
172
180
|
const schema = connection.getSchema() || "public";
|
|
181
|
+
const grammar = this.getGrammar();
|
|
173
182
|
if (driver === "sqlite") {
|
|
174
|
-
const rows = await connection.query(`PRAGMA table_info(${table})`);
|
|
183
|
+
const rows = await connection.query(`PRAGMA table_info(${grammar.wrap(table)})`);
|
|
175
184
|
const row = rows.find((item) => item.name === column);
|
|
176
185
|
return row ? { name: row.name, type: row.type, primary: row.pk > 0, autoIncrement: false } : null;
|
|
177
186
|
}
|
|
178
187
|
if (driver === "mysql") {
|
|
179
|
-
const rows = await connection.query(`
|
|
188
|
+
const rows = await connection.query("SELECT column_name AS Field, column_type AS Type, column_key AS `Key`, extra AS Extra FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name = ? AND column_name = ?", [table, column]);
|
|
180
189
|
const row = rows[0];
|
|
181
190
|
return row ? { name: row.Field, type: row.Type, primary: row.Key === "PRI", autoIncrement: String(row.Extra || "").toLowerCase().includes("auto_increment") } : null;
|
|
182
191
|
}
|
|
@@ -189,9 +198,9 @@ export class Schema {
|
|
|
189
198
|
LEFT JOIN information_schema.table_constraints tc
|
|
190
199
|
ON kcu.table_schema = tc.table_schema
|
|
191
200
|
AND kcu.constraint_name = tc.constraint_name
|
|
192
|
-
WHERE c.table_schema =
|
|
193
|
-
AND c.table_name =
|
|
194
|
-
AND c.column_name =
|
|
201
|
+
WHERE c.table_schema = $1
|
|
202
|
+
AND c.table_name = $2
|
|
203
|
+
AND c.column_name = $3`, [schema, table, column]);
|
|
195
204
|
const row = rows[0];
|
|
196
205
|
return row ? { name: row.column_name, type: row.data_type, primary: !!row.primary_key, autoIncrement: false } : null;
|
|
197
206
|
}
|
|
@@ -5,7 +5,7 @@ export class Grammar {
|
|
|
5
5
|
return value.split(".").map((v) => this.wrap(v)).join(".");
|
|
6
6
|
}
|
|
7
7
|
const { prefix, suffix } = this.wrappers;
|
|
8
|
-
return `${prefix}${value}${suffix}`;
|
|
8
|
+
return `${prefix}${value.replaceAll(suffix, `${suffix}${suffix}`)}${suffix}`;
|
|
9
9
|
}
|
|
10
10
|
wrapArray(values) {
|
|
11
11
|
return values.map((v) => this.wrap(v));
|
|
@@ -29,12 +29,16 @@ export interface ForeignKeyDefinition {
|
|
|
29
29
|
onUpdate?: string;
|
|
30
30
|
}
|
|
31
31
|
export interface WhereClause {
|
|
32
|
-
type: "basic" | "in" | "null" | "raw" | "between" | "column" | "exists";
|
|
32
|
+
type: "basic" | "in" | "null" | "raw" | "nested" | "between" | "column" | "exists" | "like" | "regexp" | "fulltext" | "json_contains" | "json_length" | "date" | "all" | "any";
|
|
33
33
|
column: string;
|
|
34
|
+
columns?: string[];
|
|
34
35
|
operator?: string;
|
|
35
36
|
value?: any;
|
|
36
37
|
boolean: "and" | "or";
|
|
37
38
|
scope?: string;
|
|
39
|
+
not?: boolean;
|
|
40
|
+
dateType?: string;
|
|
41
|
+
query?: WhereClause[];
|
|
38
42
|
}
|
|
39
43
|
export interface OrderClause {
|
|
40
44
|
column: string;
|
package/package.json
CHANGED