@expo/entity-database-adapter-knex 0.59.0 → 0.61.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/build/src/AuthorizationResultBasedKnexEntityLoader.d.ts +8 -9
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js +5 -11
- package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +4 -3
- package/build/src/BasePostgresEntityDatabaseAdapter.js +13 -19
- package/build/src/BaseSQLQueryBuilder.d.ts +4 -3
- package/build/src/BaseSQLQueryBuilder.js +4 -9
- package/build/src/EnforcingKnexEntityLoader.d.ts +7 -7
- package/build/src/EnforcingKnexEntityLoader.js +4 -10
- package/build/src/EntityFields.js +4 -11
- package/build/src/KnexEntityLoaderFactory.d.ts +4 -4
- package/build/src/KnexEntityLoaderFactory.js +8 -13
- package/build/src/PaginationStrategy.js +2 -6
- package/build/src/PostgresEntity.d.ts +4 -3
- package/build/src/PostgresEntity.js +5 -10
- package/build/src/PostgresEntityDatabaseAdapter.d.ts +6 -5
- package/build/src/PostgresEntityDatabaseAdapter.js +19 -24
- package/build/src/PostgresEntityDatabaseAdapterProvider.d.ts +1 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.js +3 -8
- package/build/src/PostgresEntityQueryContextProvider.d.ts +3 -2
- package/build/src/PostgresEntityQueryContextProvider.js +5 -10
- package/build/src/ReadonlyPostgresEntity.d.ts +4 -3
- package/build/src/ReadonlyPostgresEntity.js +5 -10
- package/build/src/SQLOperator.d.ts +324 -56
- package/build/src/SQLOperator.js +490 -150
- package/build/src/errors/wrapNativePostgresCallAsync.js +11 -15
- package/build/src/index.d.ts +20 -20
- package/build/src/index.js +20 -37
- package/build/src/internal/EntityKnexDataManager.d.ts +5 -5
- package/build/src/internal/EntityKnexDataManager.js +76 -84
- package/build/src/internal/getKnexDataManager.d.ts +2 -2
- package/build/src/internal/getKnexDataManager.js +7 -14
- package/build/src/internal/getKnexEntityLoaderFactory.d.ts +2 -2
- package/build/src/internal/getKnexEntityLoaderFactory.js +5 -9
- package/build/src/internal/utilityTypes.js +1 -3
- package/build/src/internal/weakMaps.js +1 -6
- package/build/src/knexLoader.d.ts +3 -3
- package/build/src/knexLoader.js +5 -10
- package/package.json +7 -6
- package/src/AuthorizationResultBasedKnexEntityLoader.ts +15 -12
- package/src/BasePostgresEntityDatabaseAdapter.ts +3 -3
- package/src/BaseSQLQueryBuilder.ts +5 -4
- package/src/EnforcingKnexEntityLoader.ts +8 -8
- package/src/KnexEntityLoaderFactory.ts +6 -6
- package/src/PostgresEntity.ts +5 -5
- package/src/PostgresEntityDatabaseAdapter.ts +13 -15
- package/src/PostgresEntityDatabaseAdapterProvider.ts +2 -2
- package/src/PostgresEntityQueryContextProvider.ts +3 -6
- package/src/ReadonlyPostgresEntity.ts +5 -5
- package/src/SQLOperator.ts +875 -194
- package/src/__integration-tests__/EntityCreationUtils-test.ts +5 -4
- package/src/__integration-tests__/PostgresEntityIntegration-test.ts +42 -15
- package/src/__integration-tests__/PostgresEntityQueryContextProvider-test.ts +6 -5
- package/src/__integration-tests__/PostgresInvalidSetup-test.ts +5 -4
- package/src/__integration-tests__/errors-test.ts +5 -4
- package/src/__testfixtures__/ErrorsTestEntity.ts +2 -3
- package/src/__testfixtures__/InvalidTestEntity.ts +2 -3
- package/src/__testfixtures__/PostgresTestEntity.ts +5 -6
- package/src/__testfixtures__/PostgresTriggerTestEntity.ts +7 -5
- package/src/__testfixtures__/PostgresUniqueTestEntity.ts +6 -4
- package/src/__testfixtures__/PostgresValidatorTestEntity.ts +7 -5
- package/src/__testfixtures__/createKnexIntegrationTestEntityCompanionProvider.ts +5 -8
- package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +12 -14
- package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +7 -5
- package/src/__tests__/EnforcingKnexEntityLoader-test.ts +7 -6
- package/src/__tests__/EntityFields-test.ts +1 -1
- package/src/__tests__/PostgresEntity-test.ts +6 -6
- package/src/__tests__/ReadonlyEntity-test.ts +5 -5
- package/src/__tests__/SQLOperator-test.ts +675 -95
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +9 -8
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts +2 -2
- package/src/__tests__/fixtures/TestEntity.ts +7 -7
- package/src/__tests__/fixtures/TestPaginationEntity.ts +9 -7
- package/src/__tests__/fixtures/createUnitTestPostgresEntityCompanionProvider.ts +3 -6
- package/src/errors/__tests__/wrapNativePostgresCallAsync-test.ts +1 -1
- package/src/errors/wrapNativePostgresCallAsync.ts +2 -2
- package/src/index.ts +20 -20
- package/src/internal/EntityKnexDataManager.ts +14 -17
- package/src/internal/__tests__/EntityKnexDataManager-test.ts +8 -15
- package/src/internal/__tests__/weakMaps-test.ts +1 -1
- package/src/internal/getKnexDataManager.ts +4 -4
- package/src/internal/getKnexEntityLoaderFactory.ts +4 -4
- package/src/knexLoader.ts +4 -4
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js.map +0 -1
- package/build/src/BasePostgresEntityDatabaseAdapter.js.map +0 -1
- package/build/src/BaseSQLQueryBuilder.js.map +0 -1
- package/build/src/EnforcingKnexEntityLoader.js.map +0 -1
- package/build/src/EntityFields.js.map +0 -1
- package/build/src/KnexEntityLoaderFactory.js.map +0 -1
- package/build/src/PaginationStrategy.js.map +0 -1
- package/build/src/PostgresEntity.js.map +0 -1
- package/build/src/PostgresEntityDatabaseAdapter.js.map +0 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.js.map +0 -1
- package/build/src/PostgresEntityQueryContextProvider.js.map +0 -1
- package/build/src/ReadonlyPostgresEntity.js.map +0 -1
- package/build/src/SQLOperator.js.map +0 -1
- package/build/src/errors/wrapNativePostgresCallAsync.js.map +0 -1
- package/build/src/index.js.map +0 -1
- package/build/src/internal/EntityKnexDataManager.js.map +0 -1
- package/build/src/internal/getKnexDataManager.js.map +0 -1
- package/build/src/internal/getKnexEntityLoaderFactory.js.map +0 -1
- package/build/src/internal/utilityTypes.js.map +0 -1
- package/build/src/internal/weakMaps.js.map +0 -1
- package/build/src/knexLoader.js.map +0 -1
package/build/src/SQLOperator.js
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.SQLFragmentHelpers = exports.SQLUnsafeRaw = exports.SQLArrayValue = exports.SQLEntityField = exports.SQLIdentifier = exports.SQLFragment = void 0;
|
|
7
|
-
exports.identifier = identifier;
|
|
8
|
-
exports.entityField = entityField;
|
|
9
|
-
exports.arrayValue = arrayValue;
|
|
10
|
-
exports.unsafeRaw = unsafeRaw;
|
|
11
|
-
exports.sql = sql;
|
|
12
|
-
const assert_1 = __importDefault(require("assert"));
|
|
1
|
+
import assert from 'assert';
|
|
13
2
|
/**
|
|
14
3
|
* SQL Fragment class that safely handles parameterized queries.
|
|
15
4
|
*/
|
|
16
|
-
class SQLFragment {
|
|
5
|
+
export class SQLFragment {
|
|
17
6
|
sql;
|
|
18
7
|
bindings;
|
|
19
8
|
constructor(sql, bindings) {
|
|
@@ -153,52 +142,47 @@ class SQLFragment {
|
|
|
153
142
|
return proto.constructor === Object;
|
|
154
143
|
}
|
|
155
144
|
}
|
|
156
|
-
exports.SQLFragment = SQLFragment;
|
|
157
145
|
/**
|
|
158
146
|
* Helper for SQL identifiers (table/column names).
|
|
159
147
|
* Stores the raw identifier name to be escaped by Knex using ?? placeholder.
|
|
160
148
|
*/
|
|
161
|
-
class SQLIdentifier {
|
|
149
|
+
export class SQLIdentifier {
|
|
162
150
|
name;
|
|
163
151
|
constructor(name) {
|
|
164
152
|
this.name = name;
|
|
165
153
|
}
|
|
166
154
|
}
|
|
167
|
-
exports.SQLIdentifier = SQLIdentifier;
|
|
168
155
|
/**
|
|
169
156
|
* Helper for referencing entity fields that can be used in SQL queries. This allows for type-safe references to fields of an entity
|
|
170
157
|
* and does automatic translation to DB field names.
|
|
171
158
|
*/
|
|
172
|
-
class SQLEntityField {
|
|
159
|
+
export class SQLEntityField {
|
|
173
160
|
fieldName;
|
|
174
161
|
constructor(fieldName) {
|
|
175
162
|
this.fieldName = fieldName;
|
|
176
163
|
}
|
|
177
164
|
}
|
|
178
|
-
exports.SQLEntityField = SQLEntityField;
|
|
179
165
|
/**
|
|
180
166
|
* Helper for passing an array as a single bound parameter (e.g. for PostgreSQL's = ANY(?)).
|
|
181
167
|
* Unlike bare arrays interpolated in the sql template (which expand to (?, ?, ?) for IN clauses),
|
|
182
168
|
* this binds the entire array as one parameter, letting knex handle the array encoding.
|
|
183
169
|
*/
|
|
184
|
-
class SQLArrayValue {
|
|
170
|
+
export class SQLArrayValue {
|
|
185
171
|
values;
|
|
186
172
|
constructor(values) {
|
|
187
173
|
this.values = values;
|
|
188
174
|
}
|
|
189
175
|
}
|
|
190
|
-
exports.SQLArrayValue = SQLArrayValue;
|
|
191
176
|
/**
|
|
192
177
|
* Helper for raw SQL that should not be parameterized
|
|
193
178
|
* WARNING: Only use this with trusted input to avoid SQL injection
|
|
194
179
|
*/
|
|
195
|
-
class SQLUnsafeRaw {
|
|
180
|
+
export class SQLUnsafeRaw {
|
|
196
181
|
rawSql;
|
|
197
182
|
constructor(rawSql) {
|
|
198
183
|
this.rawSql = rawSql;
|
|
199
184
|
}
|
|
200
185
|
}
|
|
201
|
-
exports.SQLUnsafeRaw = SQLUnsafeRaw;
|
|
202
186
|
/**
|
|
203
187
|
* Create a SQL identifier (table/column name) that will be escaped by Knex using ??.
|
|
204
188
|
*
|
|
@@ -209,7 +193,7 @@ exports.SQLUnsafeRaw = SQLUnsafeRaw;
|
|
|
209
193
|
* identifier('column"; DROP TABLE users; --') // Will be safely escaped
|
|
210
194
|
* ```
|
|
211
195
|
*/
|
|
212
|
-
function identifier(name) {
|
|
196
|
+
export function identifier(name) {
|
|
213
197
|
return new SQLIdentifier(name);
|
|
214
198
|
}
|
|
215
199
|
/**
|
|
@@ -218,7 +202,7 @@ function identifier(name) {
|
|
|
218
202
|
*
|
|
219
203
|
* @param fieldName - The entity field name to reference.
|
|
220
204
|
*/
|
|
221
|
-
function entityField(fieldName) {
|
|
205
|
+
export function entityField(fieldName) {
|
|
222
206
|
return new SQLEntityField(fieldName);
|
|
223
207
|
}
|
|
224
208
|
/**
|
|
@@ -232,7 +216,7 @@ function entityField(fieldName) {
|
|
|
232
216
|
* // Generates: ?? = ANY(?) with the array bound as a single parameter
|
|
233
217
|
* ```
|
|
234
218
|
*/
|
|
235
|
-
function arrayValue(values) {
|
|
219
|
+
export function arrayValue(values) {
|
|
236
220
|
return new SQLArrayValue(values);
|
|
237
221
|
}
|
|
238
222
|
/**
|
|
@@ -249,7 +233,7 @@ function arrayValue(values) {
|
|
|
249
233
|
* const query = sql`WHERE ${unsafeRaw('EXTRACT(year FROM created_at)')} = ${2024}`;
|
|
250
234
|
* ```
|
|
251
235
|
*/
|
|
252
|
-
function unsafeRaw(sqlString) {
|
|
236
|
+
export function unsafeRaw(sqlString) {
|
|
253
237
|
return new SQLUnsafeRaw(sqlString);
|
|
254
238
|
}
|
|
255
239
|
/**
|
|
@@ -261,7 +245,7 @@ function unsafeRaw(sqlString) {
|
|
|
261
245
|
* const query = sql`age >= ${age} AND status = ${'active'}`;
|
|
262
246
|
* ```
|
|
263
247
|
*/
|
|
264
|
-
function sql(strings, ...values) {
|
|
248
|
+
export function sql(strings, ...values) {
|
|
265
249
|
let sqlString = '';
|
|
266
250
|
const bindings = [];
|
|
267
251
|
strings.forEach((string, i) => {
|
|
@@ -307,25 +291,373 @@ function sql(strings, ...values) {
|
|
|
307
291
|
return new SQLFragment(sqlString, bindings);
|
|
308
292
|
}
|
|
309
293
|
/**
|
|
310
|
-
*
|
|
294
|
+
* An SQL expression that supports fluent comparison methods.
|
|
295
|
+
* Extends SQLFragment so it can be used anywhere a SQLFragment is accepted.
|
|
296
|
+
* The fluent methods return plain SQLFragment instances since they produce
|
|
297
|
+
* complete conditions, not further chainable expressions.
|
|
311
298
|
*/
|
|
312
|
-
|
|
299
|
+
export class SQLChainableFragment extends SQLFragment {
|
|
300
|
+
/**
|
|
301
|
+
* Generates an equality condition (`= value`).
|
|
302
|
+
* Automatically converts `null`/`undefined` to `IS NULL`.
|
|
303
|
+
*
|
|
304
|
+
* @param value - The value to compare against
|
|
305
|
+
* @returns A {@link SQLFragment} representing the equality condition
|
|
306
|
+
*/
|
|
307
|
+
eq(value) {
|
|
308
|
+
if (value === null || value === undefined) {
|
|
309
|
+
return this.isNull();
|
|
310
|
+
}
|
|
311
|
+
return sql `${this} = ${value}`;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Generates an inequality condition (`!= value`).
|
|
315
|
+
* Automatically converts `null`/`undefined` to `IS NOT NULL`.
|
|
316
|
+
*
|
|
317
|
+
* @param value - The value to compare against
|
|
318
|
+
* @returns A {@link SQLFragment} representing the inequality condition
|
|
319
|
+
*/
|
|
320
|
+
neq(value) {
|
|
321
|
+
if (value === null || value === undefined) {
|
|
322
|
+
return this.isNotNull();
|
|
323
|
+
}
|
|
324
|
+
return sql `${this} != ${value}`;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Generates a greater-than condition (`> value`).
|
|
328
|
+
*
|
|
329
|
+
* @param value - The value to compare against
|
|
330
|
+
* @returns A {@link SQLFragment} representing the condition
|
|
331
|
+
*/
|
|
332
|
+
gt(value) {
|
|
333
|
+
return sql `${this} > ${value}`;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Generates a greater-than-or-equal-to condition (`>= value`).
|
|
337
|
+
*
|
|
338
|
+
* @param value - The value to compare against
|
|
339
|
+
* @returns A {@link SQLFragment} representing the condition
|
|
340
|
+
*/
|
|
341
|
+
gte(value) {
|
|
342
|
+
return sql `${this} >= ${value}`;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Generates a less-than condition (`< value`).
|
|
346
|
+
*
|
|
347
|
+
* @param value - The value to compare against
|
|
348
|
+
* @returns A {@link SQLFragment} representing the condition
|
|
349
|
+
*/
|
|
350
|
+
lt(value) {
|
|
351
|
+
return sql `${this} < ${value}`;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Generates a less-than-or-equal-to condition (`<= value`).
|
|
355
|
+
*
|
|
356
|
+
* @param value - The value to compare against
|
|
357
|
+
* @returns A {@link SQLFragment} representing the condition
|
|
358
|
+
*/
|
|
359
|
+
lte(value) {
|
|
360
|
+
return sql `${this} <= ${value}`;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Generates an `IS NULL` condition.
|
|
364
|
+
*
|
|
365
|
+
* @returns A {@link SQLFragment} representing the IS NULL check
|
|
366
|
+
*/
|
|
367
|
+
isNull() {
|
|
368
|
+
return sql `${this} IS NULL`;
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Generates an `IS NOT NULL` condition.
|
|
372
|
+
*
|
|
373
|
+
* @returns A {@link SQLFragment} representing the IS NOT NULL check
|
|
374
|
+
*/
|
|
375
|
+
isNotNull() {
|
|
376
|
+
return sql `${this} IS NOT NULL`;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Generates a case-sensitive `LIKE` condition for pattern matching.
|
|
380
|
+
*
|
|
381
|
+
* @param pattern - The LIKE pattern (use `%` for wildcards, `_` for single character)
|
|
382
|
+
* @returns A {@link SQLFragment} representing the LIKE condition
|
|
383
|
+
*/
|
|
384
|
+
like(pattern) {
|
|
385
|
+
return sql `${this} LIKE ${pattern}`;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Generates a case-sensitive `NOT LIKE` condition.
|
|
389
|
+
*
|
|
390
|
+
* @param pattern - The LIKE pattern (use `%` for wildcards, `_` for single character)
|
|
391
|
+
* @returns A {@link SQLFragment} representing the NOT LIKE condition
|
|
392
|
+
*/
|
|
393
|
+
notLike(pattern) {
|
|
394
|
+
return sql `${this} NOT LIKE ${pattern}`;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Generates a case-insensitive `ILIKE` condition (PostgreSQL-specific).
|
|
398
|
+
*
|
|
399
|
+
* @param pattern - The LIKE pattern (use `%` for wildcards, `_` for single character)
|
|
400
|
+
* @returns A {@link SQLFragment} representing the ILIKE condition
|
|
401
|
+
*/
|
|
402
|
+
ilike(pattern) {
|
|
403
|
+
return sql `${this} ILIKE ${pattern}`;
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Generates a case-insensitive `NOT ILIKE` condition (PostgreSQL-specific).
|
|
407
|
+
*
|
|
408
|
+
* @param pattern - The LIKE pattern (use `%` for wildcards, `_` for single character)
|
|
409
|
+
* @returns A {@link SQLFragment} representing the NOT ILIKE condition
|
|
410
|
+
*/
|
|
411
|
+
notIlike(pattern) {
|
|
412
|
+
return sql `${this} NOT ILIKE ${pattern}`;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Generates an `IN (...)` condition. Each array element becomes a separate
|
|
416
|
+
* bound parameter (`IN (?, ?, ?)`).
|
|
417
|
+
* Returns `FALSE` when the values array is empty.
|
|
418
|
+
*
|
|
419
|
+
* @param values - The values to check membership against
|
|
420
|
+
* @returns A {@link SQLFragment} representing the IN condition
|
|
421
|
+
*/
|
|
422
|
+
inArray(values) {
|
|
423
|
+
if (values.length === 0) {
|
|
424
|
+
return sql `FALSE`;
|
|
425
|
+
}
|
|
426
|
+
return sql `${this} IN ${values}`;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Generates a `NOT IN (...)` condition. Each array element becomes a separate
|
|
430
|
+
* bound parameter.
|
|
431
|
+
* Returns `TRUE` when the values array is empty.
|
|
432
|
+
*
|
|
433
|
+
* @param values - The values to check non-membership against
|
|
434
|
+
* @returns A {@link SQLFragment} representing the NOT IN condition
|
|
435
|
+
*/
|
|
436
|
+
notInArray(values) {
|
|
437
|
+
if (values.length === 0) {
|
|
438
|
+
return sql `TRUE`;
|
|
439
|
+
}
|
|
440
|
+
return sql `${this} NOT IN ${values}`;
|
|
441
|
+
}
|
|
313
442
|
/**
|
|
314
|
-
*
|
|
443
|
+
* Generates an `= ANY(?)` condition. Unlike {@link inArray}, the array is bound
|
|
444
|
+
* as a single parameter, producing a consistent query shape for query metrics.
|
|
445
|
+
* Returns `FALSE` when the values array is empty.
|
|
446
|
+
*
|
|
447
|
+
* @param values - The values to check membership against
|
|
448
|
+
* @returns A {@link SQLFragment} representing the = ANY condition
|
|
449
|
+
*/
|
|
450
|
+
anyArray(values) {
|
|
451
|
+
if (values.length === 0) {
|
|
452
|
+
return sql `FALSE`;
|
|
453
|
+
}
|
|
454
|
+
return sql `${this} = ANY(${arrayValue(values)})`;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Generates a `BETWEEN min AND max` condition (inclusive on both ends).
|
|
458
|
+
*
|
|
459
|
+
* @param min - The lower bound
|
|
460
|
+
* @param max - The upper bound
|
|
461
|
+
* @returns A {@link SQLFragment} representing the BETWEEN condition
|
|
462
|
+
*/
|
|
463
|
+
between(min, max) {
|
|
464
|
+
return sql `${this} BETWEEN ${min} AND ${max}`;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Generates a `NOT BETWEEN min AND max` condition.
|
|
468
|
+
*
|
|
469
|
+
* @param min - The lower bound
|
|
470
|
+
* @param max - The upper bound
|
|
471
|
+
* @returns A {@link SQLFragment} representing the NOT BETWEEN condition
|
|
472
|
+
*/
|
|
473
|
+
notBetween(min, max) {
|
|
474
|
+
return sql `${this} NOT BETWEEN ${min} AND ${max}`;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Allowed PostgreSQL type names for the cast() helper.
|
|
479
|
+
* Only these types can be used to prevent SQL injection through type name interpolation.
|
|
480
|
+
*/
|
|
481
|
+
const ALLOWED_CAST_TYPES = [
|
|
482
|
+
'int',
|
|
483
|
+
'integer',
|
|
484
|
+
'int2',
|
|
485
|
+
'int4',
|
|
486
|
+
'int8',
|
|
487
|
+
'smallint',
|
|
488
|
+
'bigint',
|
|
489
|
+
'numeric',
|
|
490
|
+
'decimal',
|
|
491
|
+
'real',
|
|
492
|
+
'double precision',
|
|
493
|
+
'float',
|
|
494
|
+
'float4',
|
|
495
|
+
'float8',
|
|
496
|
+
'text',
|
|
497
|
+
'varchar',
|
|
498
|
+
'char',
|
|
499
|
+
'character varying',
|
|
500
|
+
'boolean',
|
|
501
|
+
'bool',
|
|
502
|
+
'date',
|
|
503
|
+
'time',
|
|
504
|
+
'timestamp',
|
|
505
|
+
'timestamptz',
|
|
506
|
+
'interval',
|
|
507
|
+
'json',
|
|
508
|
+
'jsonb',
|
|
509
|
+
'uuid',
|
|
510
|
+
'bytea',
|
|
511
|
+
];
|
|
512
|
+
const ALLOWED_CAST_TYPES_SET = new Set(ALLOWED_CAST_TYPES);
|
|
513
|
+
// Helper to resolve expressionOrFieldName to a SQLFragment
|
|
514
|
+
function resolveInner(expressionOrFieldName) {
|
|
515
|
+
return expressionOrFieldName instanceof SQLFragment
|
|
516
|
+
? expressionOrFieldName
|
|
517
|
+
: sql `${entityField(expressionOrFieldName)}`;
|
|
518
|
+
}
|
|
519
|
+
// Helper to resolve expressionOrFieldName to a SQLChainableFragment for fluent chaining
|
|
520
|
+
function resolveInnerExpr(expressionOrFieldName) {
|
|
521
|
+
if (expressionOrFieldName instanceof SQLChainableFragment) {
|
|
522
|
+
return expressionOrFieldName;
|
|
523
|
+
}
|
|
524
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
525
|
+
return new SQLChainableFragment(inner.sql, inner.bindings);
|
|
526
|
+
}
|
|
527
|
+
function inArrayHelper(expressionOrFieldName, values) {
|
|
528
|
+
return resolveInnerExpr(expressionOrFieldName).inArray(values);
|
|
529
|
+
}
|
|
530
|
+
function anyArrayHelper(expressionOrFieldName, values) {
|
|
531
|
+
return resolveInnerExpr(expressionOrFieldName).anyArray(values);
|
|
532
|
+
}
|
|
533
|
+
function notInArrayHelper(expressionOrFieldName, values) {
|
|
534
|
+
return resolveInnerExpr(expressionOrFieldName).notInArray(values);
|
|
535
|
+
}
|
|
536
|
+
function betweenHelper(expressionOrFieldName, min, max) {
|
|
537
|
+
return resolveInnerExpr(expressionOrFieldName).between(min, max);
|
|
538
|
+
}
|
|
539
|
+
function notBetweenHelper(expressionOrFieldName, min, max) {
|
|
540
|
+
return resolveInnerExpr(expressionOrFieldName).notBetween(min, max);
|
|
541
|
+
}
|
|
542
|
+
function likeHelper(expressionOrFieldName, pattern) {
|
|
543
|
+
return resolveInnerExpr(expressionOrFieldName).like(pattern);
|
|
544
|
+
}
|
|
545
|
+
function notLikeHelper(expressionOrFieldName, pattern) {
|
|
546
|
+
return resolveInnerExpr(expressionOrFieldName).notLike(pattern);
|
|
547
|
+
}
|
|
548
|
+
function ilikeHelper(expressionOrFieldName, pattern) {
|
|
549
|
+
return resolveInnerExpr(expressionOrFieldName).ilike(pattern);
|
|
550
|
+
}
|
|
551
|
+
function notIlikeHelper(expressionOrFieldName, pattern) {
|
|
552
|
+
return resolveInnerExpr(expressionOrFieldName).notIlike(pattern);
|
|
553
|
+
}
|
|
554
|
+
function isNullHelper(expressionOrFieldName) {
|
|
555
|
+
return resolveInnerExpr(expressionOrFieldName).isNull();
|
|
556
|
+
}
|
|
557
|
+
function isNotNullHelper(expressionOrFieldName) {
|
|
558
|
+
return resolveInnerExpr(expressionOrFieldName).isNotNull();
|
|
559
|
+
}
|
|
560
|
+
function eqHelper(expressionOrFieldName, value) {
|
|
561
|
+
return resolveInnerExpr(expressionOrFieldName).eq(value);
|
|
562
|
+
}
|
|
563
|
+
function neqHelper(expressionOrFieldName, value) {
|
|
564
|
+
return resolveInnerExpr(expressionOrFieldName).neq(value);
|
|
565
|
+
}
|
|
566
|
+
function gtHelper(expressionOrFieldName, value) {
|
|
567
|
+
return resolveInnerExpr(expressionOrFieldName).gt(value);
|
|
568
|
+
}
|
|
569
|
+
function gteHelper(expressionOrFieldName, value) {
|
|
570
|
+
return resolveInnerExpr(expressionOrFieldName).gte(value);
|
|
571
|
+
}
|
|
572
|
+
function ltHelper(expressionOrFieldName, value) {
|
|
573
|
+
return resolveInnerExpr(expressionOrFieldName).lt(value);
|
|
574
|
+
}
|
|
575
|
+
function lteHelper(expressionOrFieldName, value) {
|
|
576
|
+
return resolveInnerExpr(expressionOrFieldName).lte(value);
|
|
577
|
+
}
|
|
578
|
+
function jsonContainsHelper(expressionOrFieldName, value) {
|
|
579
|
+
const serialized = JSON.stringify(value);
|
|
580
|
+
assert(serialized !== undefined || value === undefined, 'jsonContains: value is not JSON-serializable');
|
|
581
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
582
|
+
return sql `${inner} @> ${serialized}::jsonb`;
|
|
583
|
+
}
|
|
584
|
+
function jsonContainedByHelper(expressionOrFieldName, value) {
|
|
585
|
+
const serialized = JSON.stringify(value);
|
|
586
|
+
assert(serialized !== undefined || value === undefined, 'jsonContainedBy: value is not JSON-serializable');
|
|
587
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
588
|
+
return sql `${inner} <@ ${serialized}::jsonb`;
|
|
589
|
+
}
|
|
590
|
+
function jsonPathHelper(expressionOrFieldName, path) {
|
|
591
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
592
|
+
const wrapped = sql `${inner}->${path}`;
|
|
593
|
+
return new SQLChainableFragment(wrapped.sql, wrapped.bindings);
|
|
594
|
+
}
|
|
595
|
+
function jsonPathTextHelper(expressionOrFieldName, path) {
|
|
596
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
597
|
+
const wrapped = sql `${inner}->>${path}`;
|
|
598
|
+
return new SQLChainableFragment(wrapped.sql, wrapped.bindings);
|
|
599
|
+
}
|
|
600
|
+
function jsonDeepPathHelper(expressionOrFieldName, path) {
|
|
601
|
+
const pathLiteral = `{${path.map(quotePostgresArrayElement).join(',')}}`;
|
|
602
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
603
|
+
const wrapped = sql `${inner} #> ${pathLiteral}`;
|
|
604
|
+
return new SQLChainableFragment(wrapped.sql, wrapped.bindings);
|
|
605
|
+
}
|
|
606
|
+
function jsonDeepPathTextHelper(expressionOrFieldName, path) {
|
|
607
|
+
const pathLiteral = `{${path.map(quotePostgresArrayElement).join(',')}}`;
|
|
608
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
609
|
+
const wrapped = sql `${inner} #>> ${pathLiteral}`;
|
|
610
|
+
return new SQLChainableFragment(wrapped.sql, wrapped.bindings);
|
|
611
|
+
}
|
|
612
|
+
function castHelper(expressionOrFieldName, typeName) {
|
|
613
|
+
assert(ALLOWED_CAST_TYPES_SET.has(typeName), `cast: unsupported type name "${typeName}". Allowed types: ${[...ALLOWED_CAST_TYPES_SET].join(', ')}`);
|
|
614
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
615
|
+
const wrapped = sql `(${inner})::${unsafeRaw(typeName)}`;
|
|
616
|
+
return new SQLChainableFragment(wrapped.sql, wrapped.bindings);
|
|
617
|
+
}
|
|
618
|
+
function lowerHelper(expressionOrFieldName) {
|
|
619
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
620
|
+
const wrapped = sql `LOWER(${inner})`;
|
|
621
|
+
return new SQLChainableFragment(wrapped.sql, wrapped.bindings);
|
|
622
|
+
}
|
|
623
|
+
function upperHelper(expressionOrFieldName) {
|
|
624
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
625
|
+
const wrapped = sql `UPPER(${inner})`;
|
|
626
|
+
return new SQLChainableFragment(wrapped.sql, wrapped.bindings);
|
|
627
|
+
}
|
|
628
|
+
function trimHelper(expressionOrFieldName) {
|
|
629
|
+
const inner = resolveInner(expressionOrFieldName);
|
|
630
|
+
const wrapped = sql `TRIM(${inner})`;
|
|
631
|
+
return new SQLChainableFragment(wrapped.sql, wrapped.bindings);
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Common SQL helper functions for building queries.
|
|
635
|
+
*
|
|
636
|
+
* All methods accept either a field name (string) or a SQLFragment/SQLChainableFragment as the
|
|
637
|
+
* first argument. When a SQLChainableFragment with a known TValue is passed (e.g. from trim, lower),
|
|
638
|
+
* value parameters are type-checked against that TValue.
|
|
639
|
+
*
|
|
640
|
+
* @example
|
|
641
|
+
* ```ts
|
|
642
|
+
* // Field name usage
|
|
643
|
+
* SQLExpression.eq('status', 'active')
|
|
644
|
+
*
|
|
645
|
+
* // SQLFragment/SQLChainableFragment usage
|
|
646
|
+
* SQLExpression.eq(sql`${entityField('status')}`, 'active')
|
|
647
|
+
* SQLExpression.eq(SQLExpression.trim('name'), 'hello') // value constrained to string
|
|
648
|
+
* ```
|
|
649
|
+
*/
|
|
650
|
+
export const SQLExpression = {
|
|
651
|
+
/**
|
|
652
|
+
* IN clause helper.
|
|
315
653
|
*
|
|
316
654
|
* @example
|
|
317
655
|
* ```ts
|
|
318
|
-
*
|
|
319
|
-
*
|
|
656
|
+
* SQLExpression.inArray('status', ['active', 'pending'])
|
|
657
|
+
* SQLExpression.inArray(SQLExpression.lower('status'), ['active', 'pending'])
|
|
320
658
|
* ```
|
|
321
659
|
*/
|
|
322
|
-
inArray
|
|
323
|
-
if (values.length === 0) {
|
|
324
|
-
// Handle empty array case - always false
|
|
325
|
-
return sql `1 = 0`;
|
|
326
|
-
}
|
|
327
|
-
return sql `${entityField(fieldName)} IN ${values}`;
|
|
328
|
-
},
|
|
660
|
+
inArray: inArrayHelper,
|
|
329
661
|
/**
|
|
330
662
|
* = ANY() clause helper. Binds the array as a single parameter instead of expanding it.
|
|
331
663
|
* Semantically equivalent to IN for most cases, but retains a consistent query shape for
|
|
@@ -333,192 +665,200 @@ exports.SQLFragmentHelpers = {
|
|
|
333
665
|
*
|
|
334
666
|
* @example
|
|
335
667
|
* ```ts
|
|
336
|
-
*
|
|
337
|
-
* // Generates: ?? = ANY(?) with entityField binding for 'status' and a single array value binding
|
|
668
|
+
* SQLExpression.anyArray('status', ['active', 'pending'])
|
|
338
669
|
* ```
|
|
339
670
|
*/
|
|
340
|
-
anyArray
|
|
341
|
-
if (values.length === 0) {
|
|
342
|
-
// Handle empty array case - always false
|
|
343
|
-
return sql `1 = 0`;
|
|
344
|
-
}
|
|
345
|
-
return sql `${entityField(fieldName)} = ANY(${arrayValue(values)})`;
|
|
346
|
-
},
|
|
671
|
+
anyArray: anyArrayHelper,
|
|
347
672
|
/**
|
|
348
|
-
* NOT IN clause helper
|
|
673
|
+
* NOT IN clause helper.
|
|
349
674
|
*/
|
|
350
|
-
notInArray
|
|
351
|
-
if (values.length === 0) {
|
|
352
|
-
// Handle empty array case - always true
|
|
353
|
-
return sql `1 = 1`;
|
|
354
|
-
}
|
|
355
|
-
return sql `${entityField(fieldName)} NOT IN ${values}`;
|
|
356
|
-
},
|
|
675
|
+
notInArray: notInArrayHelper,
|
|
357
676
|
/**
|
|
358
|
-
* BETWEEN helper
|
|
677
|
+
* BETWEEN helper.
|
|
359
678
|
*
|
|
360
679
|
* @example
|
|
361
680
|
* ```ts
|
|
362
|
-
*
|
|
363
|
-
*
|
|
681
|
+
* SQLExpression.between('age', 18, 65)
|
|
682
|
+
* SQLExpression.between(SQLExpression.cast('age', 'int'), 18, 65)
|
|
364
683
|
* ```
|
|
365
684
|
*/
|
|
366
|
-
between
|
|
367
|
-
return sql `${entityField(fieldName)} BETWEEN ${min} AND ${max}`;
|
|
368
|
-
},
|
|
685
|
+
between: betweenHelper,
|
|
369
686
|
/**
|
|
370
|
-
* NOT BETWEEN helper
|
|
687
|
+
* NOT BETWEEN helper.
|
|
371
688
|
*/
|
|
372
|
-
notBetween
|
|
373
|
-
return sql `${entityField(fieldName)} NOT BETWEEN ${min} AND ${max}`;
|
|
374
|
-
},
|
|
689
|
+
notBetween: notBetweenHelper,
|
|
375
690
|
/**
|
|
376
|
-
* LIKE helper
|
|
691
|
+
* LIKE helper for case-sensitive pattern matching.
|
|
377
692
|
*
|
|
378
693
|
* @example
|
|
379
694
|
* ```ts
|
|
380
|
-
*
|
|
381
|
-
* // Generates: ?? LIKE ? with entityField binding for 'name' and value binding
|
|
695
|
+
* SQLExpression.like('name', '%John%')
|
|
382
696
|
* ```
|
|
383
697
|
*/
|
|
384
|
-
like
|
|
385
|
-
return sql `${entityField(fieldName)} LIKE ${pattern}`;
|
|
386
|
-
},
|
|
698
|
+
like: likeHelper,
|
|
387
699
|
/**
|
|
388
|
-
* NOT LIKE helper
|
|
700
|
+
* NOT LIKE helper.
|
|
389
701
|
*/
|
|
390
|
-
notLike
|
|
391
|
-
return sql `${entityField(fieldName)} NOT LIKE ${pattern}`;
|
|
392
|
-
},
|
|
702
|
+
notLike: notLikeHelper,
|
|
393
703
|
/**
|
|
394
|
-
* ILIKE helper for case-insensitive matching
|
|
704
|
+
* ILIKE helper for case-insensitive matching (PostgreSQL-specific).
|
|
395
705
|
*/
|
|
396
|
-
ilike
|
|
397
|
-
return sql `${entityField(fieldName)} ILIKE ${pattern}`;
|
|
398
|
-
},
|
|
706
|
+
ilike: ilikeHelper,
|
|
399
707
|
/**
|
|
400
|
-
* NOT ILIKE helper for case-insensitive non-matching
|
|
708
|
+
* NOT ILIKE helper for case-insensitive non-matching (PostgreSQL-specific).
|
|
401
709
|
*/
|
|
402
|
-
notIlike
|
|
403
|
-
return sql `${entityField(fieldName)} NOT ILIKE ${pattern}`;
|
|
404
|
-
},
|
|
710
|
+
notIlike: notIlikeHelper,
|
|
405
711
|
/**
|
|
406
|
-
* NULL check helper
|
|
712
|
+
* IS NULL check helper.
|
|
407
713
|
*/
|
|
408
|
-
isNull
|
|
409
|
-
return sql `${entityField(fieldName)} IS NULL`;
|
|
410
|
-
},
|
|
714
|
+
isNull: isNullHelper,
|
|
411
715
|
/**
|
|
412
|
-
* NOT NULL check helper
|
|
716
|
+
* IS NOT NULL check helper.
|
|
413
717
|
*/
|
|
414
|
-
isNotNull
|
|
415
|
-
return sql `${entityField(fieldName)} IS NOT NULL`;
|
|
416
|
-
},
|
|
718
|
+
isNotNull: isNotNullHelper,
|
|
417
719
|
/**
|
|
418
|
-
*
|
|
720
|
+
* Equality operator. Automatically converts null/undefined to IS NULL.
|
|
419
721
|
*/
|
|
420
|
-
eq
|
|
421
|
-
if (value === null || value === undefined) {
|
|
422
|
-
return exports.SQLFragmentHelpers.isNull(fieldName);
|
|
423
|
-
}
|
|
424
|
-
return sql `${entityField(fieldName)} = ${value}`;
|
|
425
|
-
},
|
|
722
|
+
eq: eqHelper,
|
|
426
723
|
/**
|
|
427
|
-
*
|
|
724
|
+
* Inequality operator. Automatically converts null/undefined to IS NOT NULL.
|
|
428
725
|
*/
|
|
429
|
-
neq
|
|
430
|
-
if (value === null || value === undefined) {
|
|
431
|
-
return exports.SQLFragmentHelpers.isNotNull(fieldName);
|
|
432
|
-
}
|
|
433
|
-
return sql `${entityField(fieldName)} != ${value}`;
|
|
434
|
-
},
|
|
726
|
+
neq: neqHelper,
|
|
435
727
|
/**
|
|
436
|
-
* Greater-than comparison operator
|
|
728
|
+
* Greater-than comparison operator.
|
|
437
729
|
*/
|
|
438
|
-
gt
|
|
439
|
-
return sql `${entityField(fieldName)} > ${value}`;
|
|
440
|
-
},
|
|
730
|
+
gt: gtHelper,
|
|
441
731
|
/**
|
|
442
|
-
* Greater-than-or-equal-to comparison operator
|
|
732
|
+
* Greater-than-or-equal-to comparison operator.
|
|
443
733
|
*/
|
|
444
|
-
gte
|
|
445
|
-
return sql `${entityField(fieldName)} >= ${value}`;
|
|
446
|
-
},
|
|
734
|
+
gte: gteHelper,
|
|
447
735
|
/**
|
|
448
|
-
* Less-than comparison operator
|
|
736
|
+
* Less-than comparison operator.
|
|
449
737
|
*/
|
|
450
|
-
lt
|
|
451
|
-
return sql `${entityField(fieldName)} < ${value}`;
|
|
452
|
-
},
|
|
738
|
+
lt: ltHelper,
|
|
453
739
|
/**
|
|
454
|
-
* Less-than-or-equal-to comparison operator
|
|
740
|
+
* Less-than-or-equal-to comparison operator.
|
|
455
741
|
*/
|
|
456
|
-
lte
|
|
457
|
-
return sql `${entityField(fieldName)} <= ${value}`;
|
|
458
|
-
},
|
|
742
|
+
lte: lteHelper,
|
|
459
743
|
/**
|
|
460
|
-
* JSON contains operator (\@\>)
|
|
744
|
+
* JSON contains operator (\@\>).
|
|
461
745
|
*/
|
|
462
|
-
jsonContains
|
|
463
|
-
const serialized = JSON.stringify(value);
|
|
464
|
-
// JSON.stringify returns undefined for unsupported types, but we also want to allow undefined as a value
|
|
465
|
-
(0, assert_1.default)(serialized !== undefined || value === undefined, 'jsonContains: value is not JSON-serializable');
|
|
466
|
-
return sql `${entityField(fieldName)} @> ${serialized}::jsonb`;
|
|
467
|
-
},
|
|
746
|
+
jsonContains: jsonContainsHelper,
|
|
468
747
|
/**
|
|
469
|
-
* JSON contained by operator (\<\@\)
|
|
748
|
+
* JSON contained by operator (\<\@\).
|
|
470
749
|
*/
|
|
471
|
-
jsonContainedBy
|
|
472
|
-
const serialized = JSON.stringify(value);
|
|
473
|
-
// JSON.stringify returns undefined for unsupported types, but we also want to allow undefined as a value
|
|
474
|
-
(0, assert_1.default)(serialized !== undefined || value === undefined, 'jsonContainedBy: value is not JSON-serializable');
|
|
475
|
-
return sql `${entityField(fieldName)} <@ ${serialized}::jsonb`;
|
|
476
|
-
},
|
|
750
|
+
jsonContainedBy: jsonContainedByHelper,
|
|
477
751
|
/**
|
|
478
|
-
* JSON path extraction helper (-\>)
|
|
752
|
+
* JSON path extraction helper (-\>).
|
|
753
|
+
* Returns an SQLChainableFragment so that fluent comparison methods can be chained.
|
|
479
754
|
*/
|
|
480
|
-
jsonPath
|
|
481
|
-
return sql `${entityField(fieldName)}->${path}`;
|
|
482
|
-
},
|
|
755
|
+
jsonPath: jsonPathHelper,
|
|
483
756
|
/**
|
|
484
|
-
* JSON path text extraction helper (-\>\>)
|
|
757
|
+
* JSON path text extraction helper (-\>\>).
|
|
758
|
+
* Returns an SQLChainableFragment so that fluent comparison methods can be chained.
|
|
485
759
|
*/
|
|
486
|
-
jsonPathText
|
|
487
|
-
|
|
760
|
+
jsonPathText: jsonPathTextHelper,
|
|
761
|
+
/**
|
|
762
|
+
* JSON deep path extraction helper (#\>).
|
|
763
|
+
* Extracts a JSON sub-object at the specified key path, returning jsonb.
|
|
764
|
+
* Returns an SQLChainableFragment so that fluent comparison methods can be chained.
|
|
765
|
+
*
|
|
766
|
+
* @param expressionOrFieldName - A SQLFragment/SQLChainableFragment or entity field name
|
|
767
|
+
* @param path - Array of keys forming the path (e.g., ['user', 'address', 'city'])
|
|
768
|
+
*/
|
|
769
|
+
jsonDeepPath: jsonDeepPathHelper,
|
|
770
|
+
/**
|
|
771
|
+
* JSON deep path text extraction helper (#\>\>).
|
|
772
|
+
* Extracts a JSON sub-object at the specified key path as text.
|
|
773
|
+
* Returns an SQLChainableFragment so that fluent comparison methods can be chained.
|
|
774
|
+
*
|
|
775
|
+
* @param expressionOrFieldName - A SQLFragment/SQLChainableFragment or entity field name
|
|
776
|
+
* @param path - Array of keys forming the path (e.g., ['user', 'address', 'city'])
|
|
777
|
+
*/
|
|
778
|
+
jsonDeepPathText: jsonDeepPathTextHelper,
|
|
779
|
+
/**
|
|
780
|
+
* SQL type cast helper (::type).
|
|
781
|
+
* Casts an expression or field to a PostgreSQL type.
|
|
782
|
+
* Returns an SQLChainableFragment so that fluent comparison methods can be chained.
|
|
783
|
+
*
|
|
784
|
+
* @param expressionOrFieldName - A SQLFragment/SQLChainableFragment or entity field name to cast
|
|
785
|
+
* @param typeName - The PostgreSQL type name (e.g., 'int', 'text', 'timestamptz')
|
|
786
|
+
*/
|
|
787
|
+
cast: castHelper,
|
|
788
|
+
/**
|
|
789
|
+
* COALESCE helper.
|
|
790
|
+
* Returns the first non-null value from the given expressions/values.
|
|
791
|
+
* Returns an SQLChainableFragment so that fluent comparison methods can be chained.
|
|
792
|
+
*/
|
|
793
|
+
coalesce(...args) {
|
|
794
|
+
const fragments = args.map((arg) => {
|
|
795
|
+
if (arg instanceof SQLFragment) {
|
|
796
|
+
return arg;
|
|
797
|
+
}
|
|
798
|
+
return sql `${arg}`;
|
|
799
|
+
});
|
|
800
|
+
const inner = sql `COALESCE(${SQLFragment.joinWithCommaSeparator(...fragments)})`;
|
|
801
|
+
return new SQLChainableFragment(inner.sql, inner.bindings);
|
|
488
802
|
},
|
|
803
|
+
/**
|
|
804
|
+
* LOWER helper
|
|
805
|
+
* Converts a string expression to lowercase.
|
|
806
|
+
* Returns an SQLChainableFragment so that fluent comparison methods can be chained.
|
|
807
|
+
*/
|
|
808
|
+
lower: lowerHelper,
|
|
809
|
+
/**
|
|
810
|
+
* UPPER helper
|
|
811
|
+
* Converts a string expression to uppercase.
|
|
812
|
+
* Returns an SQLChainableFragment so that fluent comparison methods can be chained.
|
|
813
|
+
*/
|
|
814
|
+
upper: upperHelper,
|
|
815
|
+
/**
|
|
816
|
+
* TRIM helper
|
|
817
|
+
* Removes leading and trailing whitespace from a string expression.
|
|
818
|
+
* Returns an SQLChainableFragment so that fluent comparison methods can be chained.
|
|
819
|
+
*/
|
|
820
|
+
trim: trimHelper,
|
|
489
821
|
/**
|
|
490
822
|
* Logical AND of multiple fragments
|
|
491
823
|
*/
|
|
492
824
|
and(...conditions) {
|
|
493
825
|
if (conditions.length === 0) {
|
|
494
|
-
return sql `
|
|
826
|
+
return sql `TRUE`;
|
|
495
827
|
}
|
|
496
|
-
return joinSQLFragments(conditions.map((c) =>
|
|
828
|
+
return joinSQLFragments(conditions.map((c) => SQLExpression.group(c)), ' AND ');
|
|
497
829
|
},
|
|
498
830
|
/**
|
|
499
831
|
* Logical OR of multiple fragments
|
|
500
832
|
*/
|
|
501
833
|
or(...conditions) {
|
|
502
834
|
if (conditions.length === 0) {
|
|
503
|
-
return sql `
|
|
835
|
+
return sql `FALSE`;
|
|
504
836
|
}
|
|
505
|
-
return joinSQLFragments(conditions.map((c) =>
|
|
837
|
+
return joinSQLFragments(conditions.map((c) => SQLExpression.group(c)), ' OR ');
|
|
506
838
|
},
|
|
507
839
|
/**
|
|
508
840
|
* Logical NOT of a fragment
|
|
509
841
|
*/
|
|
510
842
|
not(condition) {
|
|
511
|
-
return
|
|
843
|
+
return sql `NOT (${condition})`;
|
|
512
844
|
},
|
|
513
845
|
/**
|
|
514
846
|
* Parentheses helper for grouping conditions
|
|
515
847
|
*/
|
|
516
848
|
group(condition) {
|
|
517
|
-
return
|
|
849
|
+
return sql `(${condition})`;
|
|
518
850
|
},
|
|
519
851
|
};
|
|
520
852
|
// Internal helper function to join SQL fragments with a specified separator
|
|
521
853
|
function joinSQLFragments(fragments, separator) {
|
|
522
854
|
return new SQLFragment(fragments.map((f) => f.sql).join(separator), fragments.flatMap((f) => f.bindings));
|
|
523
855
|
}
|
|
524
|
-
|
|
856
|
+
// Internal helper to properly quote elements for PostgreSQL array literals.
|
|
857
|
+
// Elements containing special characters (commas, braces, quotes, backslashes, whitespace)
|
|
858
|
+
// or empty strings must be double-quoted with internal escaping.
|
|
859
|
+
function quotePostgresArrayElement(element) {
|
|
860
|
+
if (element === '' || /[,{}"\\\s]/.test(element)) {
|
|
861
|
+
return `"${element.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
862
|
+
}
|
|
863
|
+
return element;
|
|
864
|
+
}
|