@c9up/atlas 0.2.0 → 0.2.2
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 +8 -0
- package/db.darwin-arm64.node +0 -0
- package/db.darwin-x64.node +0 -0
- package/db.linux-arm64-gnu.node +0 -0
- package/db.linux-x64-gnu.node +0 -0
- package/db.win32-x64-msvc.node +0 -0
- package/dist/AtlasProvider.d.ts +60 -7
- package/dist/AtlasProvider.d.ts.map +1 -1
- package/dist/AtlasProvider.js +204 -38
- package/dist/AtlasProvider.js.map +1 -1
- package/dist/BaseEntity.d.ts +17 -0
- package/dist/BaseEntity.d.ts.map +1 -1
- package/dist/BaseEntity.js.map +1 -1
- package/dist/BaseModel.d.ts +63 -27
- package/dist/BaseModel.d.ts.map +1 -1
- package/dist/BaseModel.js +108 -51
- package/dist/BaseModel.js.map +1 -1
- package/dist/BaseRepository.d.ts +11 -1
- package/dist/BaseRepository.d.ts.map +1 -1
- package/dist/BaseRepository.js +95 -20
- package/dist/BaseRepository.js.map +1 -1
- package/dist/ConnectionManager.d.ts +106 -0
- package/dist/ConnectionManager.d.ts.map +1 -0
- package/dist/ConnectionManager.js +228 -0
- package/dist/ConnectionManager.js.map +1 -0
- package/dist/ModelQuery.d.ts +256 -43
- package/dist/ModelQuery.d.ts.map +1 -1
- package/dist/ModelQuery.js +899 -231
- package/dist/ModelQuery.js.map +1 -1
- package/dist/Transaction.d.ts +54 -1
- package/dist/Transaction.d.ts.map +1 -1
- package/dist/Transaction.js +121 -59
- package/dist/Transaction.js.map +1 -1
- package/dist/adapters/NapiDbAdapter.d.ts +11 -0
- package/dist/adapters/NapiDbAdapter.d.ts.map +1 -1
- package/dist/adapters/NapiDbAdapter.js +35 -7
- package/dist/adapters/NapiDbAdapter.js.map +1 -1
- package/dist/console/contract.d.ts +62 -0
- package/dist/console/contract.d.ts.map +1 -0
- package/dist/console/contract.js +38 -0
- package/dist/console/contract.js.map +1 -0
- package/dist/console/factoryCommands.d.ts +23 -0
- package/dist/console/factoryCommands.d.ts.map +1 -0
- package/dist/console/factoryCommands.js +62 -0
- package/dist/console/factoryCommands.js.map +1 -0
- package/dist/console/migrationCommands.d.ts +38 -8
- package/dist/console/migrationCommands.d.ts.map +1 -1
- package/dist/console/migrationCommands.js +157 -50
- package/dist/console/migrationCommands.js.map +1 -1
- package/dist/console/schemaCheckCommand.d.ts +11 -17
- package/dist/console/schemaCheckCommand.d.ts.map +1 -1
- package/dist/console/schemaCheckCommand.js +21 -15
- package/dist/console/schemaCheckCommand.js.map +1 -1
- package/dist/console/schemaDumpCommand.d.ts +30 -0
- package/dist/console/schemaDumpCommand.d.ts.map +1 -0
- package/dist/console/schemaDumpCommand.js +69 -0
- package/dist/console/schemaDumpCommand.js.map +1 -0
- package/dist/console/schemaGenerateCommand.d.ts +100 -0
- package/dist/console/schemaGenerateCommand.d.ts.map +1 -0
- package/dist/console/schemaGenerateCommand.js +246 -0
- package/dist/console/schemaGenerateCommand.js.map +1 -0
- package/dist/console/seederCommands.d.ts +46 -0
- package/dist/console/seederCommands.d.ts.map +1 -0
- package/dist/console/seederCommands.js +136 -0
- package/dist/console/seederCommands.js.map +1 -0
- package/dist/decorators/entity.d.ts +13 -2
- package/dist/decorators/entity.d.ts.map +1 -1
- package/dist/decorators/entity.js +10 -0
- package/dist/decorators/entity.js.map +1 -1
- package/dist/decorators/hooks.d.ts +3 -3
- package/dist/decorators/hooks.d.ts.map +1 -1
- package/dist/decorators/hooks.js.map +1 -1
- package/dist/events.d.ts +5 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/factories.d.ts +8 -0
- package/dist/factories.d.ts.map +1 -0
- package/dist/factories.js +8 -0
- package/dist/factories.js.map +1 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -4
- package/dist/index.js.map +1 -1
- package/dist/lucid-schema.d.ts +15 -0
- package/dist/lucid-schema.d.ts.map +1 -0
- package/dist/lucid-schema.js +16 -0
- package/dist/lucid-schema.js.map +1 -0
- package/dist/orm.d.ts +14 -0
- package/dist/orm.d.ts.map +1 -0
- package/dist/orm.js +14 -0
- package/dist/orm.js.map +1 -0
- package/dist/query/DatabaseQueryBuilder.d.ts +557 -0
- package/dist/query/DatabaseQueryBuilder.d.ts.map +1 -0
- package/dist/query/DatabaseQueryBuilder.js +1798 -0
- package/dist/query/DatabaseQueryBuilder.js.map +1 -0
- package/dist/query/DmlBuilder.d.ts +62 -0
- package/dist/query/DmlBuilder.d.ts.map +1 -0
- package/dist/query/DmlBuilder.js +84 -0
- package/dist/query/DmlBuilder.js.map +1 -0
- package/dist/query/QueryBuilder.d.ts +5 -0
- package/dist/query/QueryBuilder.d.ts.map +1 -1
- package/dist/query/QueryBuilder.js +7 -0
- package/dist/query/QueryBuilder.js.map +1 -1
- package/dist/query/RawQueryBuilder.d.ts +47 -0
- package/dist/query/RawQueryBuilder.d.ts.map +1 -0
- package/dist/query/RawQueryBuilder.js +138 -0
- package/dist/query/RawQueryBuilder.js.map +1 -0
- package/dist/query/interpolate.d.ts +45 -0
- package/dist/query/interpolate.d.ts.map +1 -0
- package/dist/query/interpolate.js +51 -0
- package/dist/query/interpolate.js.map +1 -0
- package/dist/query/native.d.ts +8 -0
- package/dist/query/native.d.ts.map +1 -1
- package/dist/query/native.js +14 -0
- package/dist/query/native.js.map +1 -1
- package/dist/query/operators.d.ts +4 -0
- package/dist/query/operators.d.ts.map +1 -0
- package/dist/query/operators.js +27 -0
- package/dist/query/operators.js.map +1 -0
- package/dist/schema/Migration.d.ts +31 -7
- package/dist/schema/Migration.d.ts.map +1 -1
- package/dist/schema/Migration.js +34 -7
- package/dist/schema/Migration.js.map +1 -1
- package/dist/schema/MigrationRunner.d.ts +56 -6
- package/dist/schema/MigrationRunner.d.ts.map +1 -1
- package/dist/schema/MigrationRunner.js +382 -112
- package/dist/schema/MigrationRunner.js.map +1 -1
- package/dist/schema/Schema.d.ts +59 -2
- package/dist/schema/Schema.d.ts.map +1 -1
- package/dist/schema/Schema.js +143 -9
- package/dist/schema/Schema.js.map +1 -1
- package/dist/schema/SchemaDumper.d.ts +121 -0
- package/dist/schema/SchemaDumper.d.ts.map +1 -0
- package/dist/schema/SchemaDumper.js +365 -0
- package/dist/schema/SchemaDumper.js.map +1 -0
- package/dist/schema/Seeder.d.ts +13 -0
- package/dist/schema/Seeder.d.ts.map +1 -1
- package/dist/schema/Seeder.js +28 -5
- package/dist/schema/Seeder.js.map +1 -1
- package/dist/schema/TableBuilder.d.ts +27 -8
- package/dist/schema/TableBuilder.d.ts.map +1 -1
- package/dist/schema/TableBuilder.js +50 -12
- package/dist/schema/TableBuilder.js.map +1 -1
- package/dist/schema/catalog.d.ts +25 -1
- package/dist/schema/catalog.d.ts.map +1 -1
- package/dist/schema/catalog.js +69 -22
- package/dist/schema/catalog.js.map +1 -1
- package/dist/schema/introspect.d.ts +1 -1
- package/dist/schema/introspect.d.ts.map +1 -1
- package/dist/schema/introspect.js +26 -11
- package/dist/schema/introspect.js.map +1 -1
- package/dist/seeders.d.ts +7 -0
- package/dist/seeders.d.ts.map +1 -0
- package/dist/seeders.js +7 -0
- package/dist/seeders.js.map +1 -0
- package/dist/services/db.d.ts +120 -9
- package/dist/services/db.d.ts.map +1 -1
- package/dist/services/db.js +182 -26
- package/dist/services/db.js.map +1 -1
- package/dist/testing/DatabaseCleanup.d.ts +29 -6
- package/dist/testing/DatabaseCleanup.d.ts.map +1 -1
- package/dist/testing/DatabaseCleanup.js +44 -17
- package/dist/testing/DatabaseCleanup.js.map +1 -1
- package/dist/testing/DbAssertions.d.ts +54 -0
- package/dist/testing/DbAssertions.d.ts.map +1 -0
- package/dist/testing/DbAssertions.js +156 -0
- package/dist/testing/DbAssertions.js.map +1 -0
- package/dist/testing/Factory.d.ts +111 -25
- package/dist/testing/Factory.d.ts.map +1 -1
- package/dist/testing/Factory.js +331 -56
- package/dist/testing/Factory.js.map +1 -1
- package/dist/testing/TestUtils.d.ts +49 -0
- package/dist/testing/TestUtils.d.ts.map +1 -0
- package/dist/testing/TestUtils.js +79 -0
- package/dist/testing/TestUtils.js.map +1 -0
- package/dist/testing/index.d.ts +3 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +3 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/types/relations.d.ts +31 -0
- package/dist/types/relations.d.ts.map +1 -0
- package/dist/types/relations.js +20 -0
- package/dist/types/relations.js.map +1 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +30 -2
- package/src/AtlasProvider.ts +297 -51
- package/src/BaseEntity.ts +29 -0
- package/src/BaseModel.ts +174 -37
- package/src/BaseRepository.ts +136 -17
- package/src/ConnectionManager.ts +298 -0
- package/src/ModelQuery.ts +1309 -260
- package/src/Transaction.ts +194 -60
- package/src/adapters/NapiDbAdapter.ts +72 -14
- package/src/console/contract.ts +96 -0
- package/src/console/factoryCommands.ts +73 -0
- package/src/console/migrationCommands.ts +253 -63
- package/src/console/schemaCheckCommand.ts +26 -22
- package/src/console/schemaDumpCommand.ts +96 -0
- package/src/console/schemaGenerateCommand.ts +380 -0
- package/src/console/seederCommands.ts +174 -0
- package/src/decorators/entity.ts +18 -2
- package/src/decorators/hooks.ts +5 -3
- package/src/events.ts +5 -0
- package/src/factories.ts +10 -0
- package/src/index.ts +50 -3
- package/src/lucid-schema.ts +22 -0
- package/src/orm.ts +13 -0
- package/src/query/DatabaseQueryBuilder.ts +2724 -0
- package/src/query/DmlBuilder.ts +131 -0
- package/src/query/QueryBuilder.ts +8 -0
- package/src/query/RawQueryBuilder.ts +173 -0
- package/src/query/interpolate.ts +73 -0
- package/src/query/native.ts +29 -0
- package/src/query/operators.ts +27 -0
- package/src/schema/Migration.ts +36 -7
- package/src/schema/MigrationRunner.ts +483 -119
- package/src/schema/Schema.ts +168 -19
- package/src/schema/SchemaDumper.ts +518 -0
- package/src/schema/Seeder.ts +47 -10
- package/src/schema/TableBuilder.ts +48 -11
- package/src/schema/catalog.ts +94 -24
- package/src/schema/introspect.ts +26 -9
- package/src/seeders.ts +16 -0
- package/src/services/db.ts +343 -31
- package/src/testing/DatabaseCleanup.ts +73 -21
- package/src/testing/DbAssertions.ts +220 -0
- package/src/testing/Factory.ts +559 -79
- package/src/testing/TestUtils.ts +123 -0
- package/src/testing/index.ts +12 -1
- package/src/types/relations.ts +43 -0
package/dist/ModelQuery.js
CHANGED
|
@@ -15,7 +15,11 @@ import { assertNotPromise, BaseRepository, wrapAdapterError, } from "./BaseRepos
|
|
|
15
15
|
import { ensureEntityMetadata, getColumnMetadata, getDateColumnConfig, getPrimaryKey, getRelationMetadata, hasSoftDeletes, } from "./decorators/entity.js";
|
|
16
16
|
import { fireHooks } from "./decorators/hooks.js";
|
|
17
17
|
import { getNamingStrategy } from "./naming/NamingStrategy.js";
|
|
18
|
-
import {
|
|
18
|
+
import { DmlBuilder } from "./query/DmlBuilder.js";
|
|
19
|
+
import { compiledStatement, interpolateQuery, } from "./query/interpolate.js";
|
|
20
|
+
import { compileStatementNative, getAtlasDialect, normalizeDialect, } from "./query/native.js";
|
|
21
|
+
import { negateOperator } from "./query/operators.js";
|
|
22
|
+
import { RawSql } from "./query/QueryBuilder.js";
|
|
19
23
|
import { camelToSnake, snakeToCamel } from "./utils/casing.js";
|
|
20
24
|
/**
|
|
21
25
|
* Comparison operators allowed in `whereExpr`'s raw 4-arg form (where
|
|
@@ -405,10 +409,27 @@ export class ModelQuery {
|
|
|
405
409
|
#orderBys = [];
|
|
406
410
|
#select = ["*"];
|
|
407
411
|
#limit;
|
|
412
|
+
/** Top-N-per-parent limit for a has-many preload (Lucid `groupLimit`). */
|
|
413
|
+
#groupLimit;
|
|
414
|
+
/** ORDER BY for the per-parent window (Lucid `groupOrderBy`). */
|
|
415
|
+
#groupOrderBy;
|
|
408
416
|
#offset;
|
|
409
417
|
#preloads = new Map();
|
|
418
|
+
// Per-query row transformers (Adonis Lucid `rowTransformer`). Stored via a
|
|
419
|
+
// method-signature wrapper (not a bare `(row: T) => void` property) so T stays
|
|
420
|
+
// in a bivariant position — a function-typed property would force ModelQuery<T>
|
|
421
|
+
// invariant and break the `this: ModelClass<T>` bound on every static finder.
|
|
422
|
+
#rowTransformers = [];
|
|
410
423
|
/** Correlated subquery projections (withCount / withAggregate). */
|
|
411
424
|
#selectSubqueries = [];
|
|
425
|
+
/** Raw / subquery SELECT fragments carrying their own params (Lucid `select(raw)`, `select(subquery.as())`). */
|
|
426
|
+
#selectRaw = [];
|
|
427
|
+
/** Caller-facing statement timeout in ms (Lucid `timeout(ms)`), applied via a race in exec. */
|
|
428
|
+
#timeoutMs;
|
|
429
|
+
/** `timeout(ms, { cancel: true })` — also apply a SERVER-side statement timeout. */
|
|
430
|
+
#cancelTimeout = false;
|
|
431
|
+
/** Columns from a chainable `.returning(...)` on a lazy DML builder. */
|
|
432
|
+
#dmlReturning = [];
|
|
412
433
|
/** Alias stored by `.as()` — consumed when this query is used as a withCount/withAggregate sub-builder. */
|
|
413
434
|
#subqueryAlias;
|
|
414
435
|
/** Raw JOIN fragments — Story 29.4. */
|
|
@@ -421,6 +442,10 @@ export class ModelQuery {
|
|
|
421
442
|
#sideloaded = null;
|
|
422
443
|
/** Per-query debug flag — Story 29.11. */
|
|
423
444
|
#debugFlag = false;
|
|
445
|
+
/** Metadata attached to the db:query event (Adonis Lucid `reporterData`). */
|
|
446
|
+
#reporterData;
|
|
447
|
+
/** SQL comments prepended to the compiled query (Lucid/Knex `comment`). */
|
|
448
|
+
#comments = [];
|
|
424
449
|
/** Distinct flag — Story 29.5. */
|
|
425
450
|
#distinct = false;
|
|
426
451
|
#distinctOn = [];
|
|
@@ -434,6 +459,8 @@ export class ModelQuery {
|
|
|
434
459
|
#unions = [];
|
|
435
460
|
/** m2m pivot-table WHERE constraints — applied to the pivot lookup, not the related query. */
|
|
436
461
|
#pivotWheres = [];
|
|
462
|
+
/** Extra pivot columns requested at query time (Lucid `pivotColumns([...])`). */
|
|
463
|
+
#extraPivotColumns = [];
|
|
437
464
|
/**
|
|
438
465
|
* Deferred builder for a lazy m2m `related().query()` EXISTS predicate. Set by
|
|
439
466
|
* the relation proxy's scoped query; invoked at `#buildSpec()` time with the
|
|
@@ -469,6 +496,24 @@ export class ModelQuery {
|
|
|
469
496
|
this.#softScope = "only-trashed";
|
|
470
497
|
return this;
|
|
471
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Apply the first `[guard, callback]` whose guard is truthy (Adonis Lucid
|
|
501
|
+
* `match`); a trailing bare callback is the default, run when none matched.
|
|
502
|
+
*/
|
|
503
|
+
match(...blocks) {
|
|
504
|
+
for (const block of blocks) {
|
|
505
|
+
if (typeof block === "function") {
|
|
506
|
+
block(this);
|
|
507
|
+
return this;
|
|
508
|
+
}
|
|
509
|
+
const [guard, callback] = block;
|
|
510
|
+
if (guard) {
|
|
511
|
+
callback(this);
|
|
512
|
+
return this;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return this;
|
|
516
|
+
}
|
|
472
517
|
/**
|
|
473
518
|
* Eager-load a relation (AdonisJS-style).
|
|
474
519
|
* Relations are never loaded automatically — you must call .preload() explicitly.
|
|
@@ -481,14 +526,80 @@ export class ModelQuery {
|
|
|
481
526
|
this.#preloads.set(relationName, callback);
|
|
482
527
|
return this;
|
|
483
528
|
}
|
|
484
|
-
/**
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
529
|
+
/**
|
|
530
|
+
* Eager-load a relation only if it hasn't been registered yet (Adonis Lucid
|
|
531
|
+
* `preloadOnce`) — a later `preload`/`preloadOnce` for the same relation is
|
|
532
|
+
* ignored, so a shared scope can safely add a preload without clobbering one
|
|
533
|
+
* the caller already set.
|
|
534
|
+
*/
|
|
535
|
+
preloadOnce(relationName, callback) {
|
|
536
|
+
if (!this.#preloads.has(relationName)) {
|
|
537
|
+
this.#preloads.set(relationName, callback);
|
|
538
|
+
}
|
|
539
|
+
return this;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Register a callback run for every hydrated instance after loading (preloads
|
|
543
|
+
* included) but before the query resolves (Adonis Lucid `rowTransformer`). The
|
|
544
|
+
* callback mutates the instance in place — decorate rows with per-query
|
|
545
|
+
* computed values without a model hook/accessor. Multiple transformers run in
|
|
546
|
+
* registration order.
|
|
547
|
+
*/
|
|
548
|
+
rowTransformer(callback) {
|
|
549
|
+
this.#rowTransformers.push({ run: callback });
|
|
550
|
+
return this;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Select columns (default: `*`). The model query builder extends the database
|
|
554
|
+
* query builder's `select` surface (Lucid parity): bare names, multiple args,
|
|
555
|
+
* a comma-separated string, an array, an `{ alias: 'column' }` object map, a
|
|
556
|
+
* `db.raw(...)` fragment, and a named subquery — `select(subquery.as('x'))`.
|
|
557
|
+
* Plain columns are resolved to their DB column (honouring `@Column`).
|
|
558
|
+
*/
|
|
559
|
+
select(...columns) {
|
|
560
|
+
const plain = [];
|
|
561
|
+
for (const col of columns) {
|
|
562
|
+
if (typeof col === "string") {
|
|
563
|
+
plain.push(...col.split(",").map((c) => c.trim()));
|
|
564
|
+
}
|
|
565
|
+
else if (Array.isArray(col)) {
|
|
566
|
+
plain.push(...col);
|
|
567
|
+
}
|
|
568
|
+
else if (col instanceof RawSql) {
|
|
569
|
+
// Lucid `select(db.raw(sql, bindings))` — verbatim fragment + params.
|
|
570
|
+
this.#selectRaw.push({ sql: col.sql, params: [...col.params] });
|
|
571
|
+
}
|
|
572
|
+
else if (col instanceof _a) {
|
|
573
|
+
// Lucid `select(subquery.as('alias'))` — a correlated subquery column.
|
|
574
|
+
const alias = col.#subqueryAlias;
|
|
575
|
+
if (!alias) {
|
|
576
|
+
throw new Error("select(subquery) requires the subquery to be named with .as('alias')");
|
|
577
|
+
}
|
|
578
|
+
const { sql, params } = col.#compiledNative();
|
|
579
|
+
this.#selectRaw.push({
|
|
580
|
+
sql: `(${sql}) AS ${this.#quoteAliasName(alias)}`,
|
|
581
|
+
params,
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
for (const [alias, expr] of Object.entries(col)) {
|
|
586
|
+
plain.push(`${expr} AS ${alias}`);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (plain.length > 0) {
|
|
591
|
+
this.#select = plain.map((c) => this.#resolveSelect(c));
|
|
592
|
+
}
|
|
490
593
|
return this;
|
|
491
594
|
}
|
|
595
|
+
/** Validate + dialect-quote a bare alias identifier. */
|
|
596
|
+
#quoteAliasName(alias) {
|
|
597
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) {
|
|
598
|
+
throw new Error(`Invalid alias '${alias}' — expected a bare identifier.`);
|
|
599
|
+
}
|
|
600
|
+
const q = this.#dialect === "mysql" ? "`" : '"';
|
|
601
|
+
return `${q}${alias}${q}`;
|
|
602
|
+
}
|
|
492
603
|
/**
|
|
493
604
|
* Resolve a bare model-property select/returning target to its DB column
|
|
494
605
|
* (honouring `@Column({ columnName })`), leaving expressions / aliases /
|
|
@@ -588,33 +699,73 @@ export class ModelQuery {
|
|
|
588
699
|
andWhereColumn(left, operator, right) {
|
|
589
700
|
return this.whereColumn(left, operator, right);
|
|
590
701
|
}
|
|
591
|
-
|
|
592
|
-
|
|
702
|
+
whereNot(columnOrCbOrObj, operatorOrValue, value) {
|
|
703
|
+
if (typeof columnOrCbOrObj === "function") {
|
|
704
|
+
this.#wheres.push({
|
|
705
|
+
...this.#buildGroup("and", columnOrCbOrObj),
|
|
706
|
+
negated: true,
|
|
707
|
+
});
|
|
708
|
+
return this;
|
|
709
|
+
}
|
|
710
|
+
if (typeof columnOrCbOrObj === "object") {
|
|
711
|
+
for (const [col, val] of Object.entries(columnOrCbOrObj)) {
|
|
712
|
+
this.#wheres.push({
|
|
713
|
+
type: "and",
|
|
714
|
+
column: this.#resolveColumn(col),
|
|
715
|
+
operator: "!=",
|
|
716
|
+
value: this.#prep(col, val),
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
return this;
|
|
720
|
+
}
|
|
721
|
+
if (value === undefined) {
|
|
722
|
+
this.#wheres.push({
|
|
723
|
+
type: "and",
|
|
724
|
+
column: this.#resolveColumn(columnOrCbOrObj),
|
|
725
|
+
operator: "!=",
|
|
726
|
+
value: this.#prep(columnOrCbOrObj, operatorOrValue),
|
|
727
|
+
});
|
|
728
|
+
return this;
|
|
729
|
+
}
|
|
730
|
+
// (column, operator, value): NOT (col <op> value) via a negated group so any
|
|
731
|
+
// operator negates correctly without an operator-inversion table.
|
|
732
|
+
const col = columnOrCbOrObj;
|
|
733
|
+
const op = operatorOrValue;
|
|
593
734
|
this.#wheres.push({
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
operator: "!=",
|
|
597
|
-
value: this.#prep(column, value),
|
|
735
|
+
...this.#buildGroup("and", (q) => q.where(col, op, value)),
|
|
736
|
+
negated: true,
|
|
598
737
|
});
|
|
599
738
|
return this;
|
|
600
739
|
}
|
|
601
|
-
|
|
602
|
-
|
|
740
|
+
whereIn(columnOrColumns, source) {
|
|
741
|
+
// Tuple form (Lucid `whereIn(['id','email'], [[1,'a@b.com']])`).
|
|
742
|
+
if (Array.isArray(columnOrColumns)) {
|
|
743
|
+
const cols = columnOrColumns.map((c) => this.#resolveColumn(c));
|
|
744
|
+
const rows = (Array.isArray(source) ? source : []).map((r) => (Array.isArray(r) ? r : [r]).map((v, j) => this.#prep(cols[j] ?? "", v)));
|
|
745
|
+
this.#wheres.push({
|
|
746
|
+
type: "and",
|
|
747
|
+
kind: "inTuple",
|
|
748
|
+
columns: cols,
|
|
749
|
+
rows,
|
|
750
|
+
negated: false,
|
|
751
|
+
});
|
|
752
|
+
return this;
|
|
753
|
+
}
|
|
603
754
|
if (source instanceof _a) {
|
|
604
755
|
this.#wheres.push({
|
|
605
756
|
type: "and",
|
|
606
757
|
kind: "inSub",
|
|
607
758
|
negated: false,
|
|
608
|
-
column: this.#resolveColumn(
|
|
759
|
+
column: this.#resolveColumn(columnOrColumns),
|
|
609
760
|
subquery: source.#buildSpec(),
|
|
610
761
|
});
|
|
611
762
|
return this;
|
|
612
763
|
}
|
|
613
764
|
this.#wheres.push({
|
|
614
765
|
type: "and",
|
|
615
|
-
column: this.#resolveColumn(
|
|
766
|
+
column: this.#resolveColumn(columnOrColumns),
|
|
616
767
|
operator: "IN",
|
|
617
|
-
value: this.#prep(
|
|
768
|
+
value: this.#prep(columnOrColumns, [...source]),
|
|
618
769
|
});
|
|
619
770
|
return this;
|
|
620
771
|
}
|
|
@@ -976,28 +1127,48 @@ export class ModelQuery {
|
|
|
976
1127
|
orWhereNotExists(subquery) {
|
|
977
1128
|
return this.#pushExists("or", true, subquery);
|
|
978
1129
|
}
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1130
|
+
whereJsonPath(column, path, operatorOrValue, value) {
|
|
1131
|
+
return this.#pushJsonPathArgs("and", column, path, operatorOrValue, value);
|
|
1132
|
+
}
|
|
1133
|
+
andWhereJsonPath(column, path, operatorOrValue, value) {
|
|
1134
|
+
return this.#pushJsonPathArgs("and", column, path, operatorOrValue, value);
|
|
1135
|
+
}
|
|
1136
|
+
orWhereJsonPath(column, path, operatorOrValue, value) {
|
|
1137
|
+
return this.#pushJsonPathArgs("or", column, path, operatorOrValue, value);
|
|
1138
|
+
}
|
|
1139
|
+
/** Resolve the optional-operator JSONPath form, then push (default op `=`). */
|
|
1140
|
+
#pushJsonPathArgs(type, column, path, operatorOrValue, value) {
|
|
1141
|
+
const [operator, val] = value === undefined
|
|
1142
|
+
? ["=", operatorOrValue]
|
|
1143
|
+
: [String(operatorOrValue), value];
|
|
1144
|
+
return this.#pushJson(type, false, "path", column, val, path, String(operator));
|
|
1145
|
+
}
|
|
985
1146
|
/**
|
|
986
|
-
*
|
|
987
|
-
* (
|
|
988
|
-
*
|
|
989
|
-
* query.whereJsonPath('data', '$.address.city', '=', 'Paris')
|
|
1147
|
+
* Structural JSON match (Lucid `whereJson`) — the column's JSON must equal
|
|
1148
|
+
* `value` (canonical comparison on Postgres/MySQL). AND is the default.
|
|
990
1149
|
*/
|
|
991
|
-
|
|
992
|
-
return this.#pushJson("and", false, "
|
|
1150
|
+
whereJson(column, value) {
|
|
1151
|
+
return this.#pushJson("and", false, "equals", column, value);
|
|
1152
|
+
}
|
|
1153
|
+
/** Alias of {@link whereJson} (Lucid `andWhereJson`). */
|
|
1154
|
+
andWhereJson(column, value) {
|
|
1155
|
+
return this.#pushJson("and", false, "equals", column, value);
|
|
1156
|
+
}
|
|
1157
|
+
/** OR form of {@link whereJson} (Lucid `orWhereJson`). */
|
|
1158
|
+
orWhereJson(column, value) {
|
|
1159
|
+
return this.#pushJson("or", false, "equals", column, value);
|
|
993
1160
|
}
|
|
994
|
-
/**
|
|
995
|
-
|
|
996
|
-
return this.#pushJson("and",
|
|
1161
|
+
/** WHERE NOT structural JSON match (Lucid `whereNotJson`). */
|
|
1162
|
+
whereNotJson(column, value) {
|
|
1163
|
+
return this.#pushJson("and", true, "equals", column, value);
|
|
997
1164
|
}
|
|
998
|
-
/**
|
|
999
|
-
|
|
1000
|
-
return this.#pushJson("
|
|
1165
|
+
/** Alias of {@link whereNotJson} (Lucid `andWhereNotJson`). */
|
|
1166
|
+
andWhereNotJson(column, value) {
|
|
1167
|
+
return this.#pushJson("and", true, "equals", column, value);
|
|
1168
|
+
}
|
|
1169
|
+
/** OR NOT structural JSON match (Lucid `orWhereNotJson`). */
|
|
1170
|
+
orWhereNotJson(column, value) {
|
|
1171
|
+
return this.#pushJson("or", true, "equals", column, value);
|
|
1001
1172
|
}
|
|
1002
1173
|
/**
|
|
1003
1174
|
* `WHERE <col> @> ?` — the JSON column contains `value` (Lucid/Knex
|
|
@@ -1048,6 +1219,47 @@ export class ModelQuery {
|
|
|
1048
1219
|
orWhereNotJsonSubsetOf(column, value) {
|
|
1049
1220
|
return this.#pushJson("or", true, "subset", column, value);
|
|
1050
1221
|
}
|
|
1222
|
+
// Lucid's canonical containment names drop the `Of` suffix — kept as aliases.
|
|
1223
|
+
/** Lucid `whereJsonSuperset` (alias of {@link whereJsonSupersetOf}). */
|
|
1224
|
+
whereJsonSuperset(column, value) {
|
|
1225
|
+
return this.#pushJson("and", false, "superset", column, value);
|
|
1226
|
+
}
|
|
1227
|
+
/** Lucid `andWhereJsonSuperset`. */
|
|
1228
|
+
andWhereJsonSuperset(column, value) {
|
|
1229
|
+
return this.#pushJson("and", false, "superset", column, value);
|
|
1230
|
+
}
|
|
1231
|
+
/** Lucid `orWhereJsonSuperset`. */
|
|
1232
|
+
orWhereJsonSuperset(column, value) {
|
|
1233
|
+
return this.#pushJson("or", false, "superset", column, value);
|
|
1234
|
+
}
|
|
1235
|
+
/** Lucid `whereNotJsonSuperset`. */
|
|
1236
|
+
whereNotJsonSuperset(column, value) {
|
|
1237
|
+
return this.#pushJson("and", true, "superset", column, value);
|
|
1238
|
+
}
|
|
1239
|
+
/** Lucid `orWhereNotJsonSuperset`. */
|
|
1240
|
+
orWhereNotJsonSuperset(column, value) {
|
|
1241
|
+
return this.#pushJson("or", true, "superset", column, value);
|
|
1242
|
+
}
|
|
1243
|
+
/** Lucid `whereJsonSubset` (alias of {@link whereJsonSubsetOf}). */
|
|
1244
|
+
whereJsonSubset(column, value) {
|
|
1245
|
+
return this.#pushJson("and", false, "subset", column, value);
|
|
1246
|
+
}
|
|
1247
|
+
/** Lucid `andWhereJsonSubset`. */
|
|
1248
|
+
andWhereJsonSubset(column, value) {
|
|
1249
|
+
return this.#pushJson("and", false, "subset", column, value);
|
|
1250
|
+
}
|
|
1251
|
+
/** Lucid `orWhereJsonSubset`. */
|
|
1252
|
+
orWhereJsonSubset(column, value) {
|
|
1253
|
+
return this.#pushJson("or", false, "subset", column, value);
|
|
1254
|
+
}
|
|
1255
|
+
/** Lucid `whereNotJsonSubset`. */
|
|
1256
|
+
whereNotJsonSubset(column, value) {
|
|
1257
|
+
return this.#pushJson("and", true, "subset", column, value);
|
|
1258
|
+
}
|
|
1259
|
+
/** Lucid `orWhereNotJsonSubset`. */
|
|
1260
|
+
orWhereNotJsonSubset(column, value) {
|
|
1261
|
+
return this.#pushJson("or", true, "subset", column, value);
|
|
1262
|
+
}
|
|
1051
1263
|
#pushJson(type, negated, jsonOp, column, value, path, operator) {
|
|
1052
1264
|
// A JSONPath is bound, not interpolated, so injection is not the concern
|
|
1053
1265
|
// here — a clear early error for a malformed path is. Lucid/Knex paths
|
|
@@ -1308,8 +1520,29 @@ export class ModelQuery {
|
|
|
1308
1520
|
this.#selectSubqueries.push(this.#buildRelationSubquery(relationName, callback, "aggregate", relationName));
|
|
1309
1521
|
return this;
|
|
1310
1522
|
}
|
|
1311
|
-
orderBy(
|
|
1312
|
-
|
|
1523
|
+
orderBy(columnOrTerms, direction = "asc") {
|
|
1524
|
+
// A raw fragment (e.g. `db.ref('posts.created_at')`) orders verbatim.
|
|
1525
|
+
if (columnOrTerms instanceof RawSql) {
|
|
1526
|
+
const dir = direction === "desc" ? "DESC" : "ASC";
|
|
1527
|
+
this.#orderBys.push({ raw: `${columnOrTerms.sql} ${dir}` });
|
|
1528
|
+
return this;
|
|
1529
|
+
}
|
|
1530
|
+
if (Array.isArray(columnOrTerms)) {
|
|
1531
|
+
for (const t of columnOrTerms) {
|
|
1532
|
+
const [col, dir] = typeof t === "string"
|
|
1533
|
+
? [t, "asc"]
|
|
1534
|
+
: [t.column, t.order ?? "asc"];
|
|
1535
|
+
this.#orderBys.push({
|
|
1536
|
+
column: this.#resolveColumn(col),
|
|
1537
|
+
direction: dir,
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
return this;
|
|
1541
|
+
}
|
|
1542
|
+
this.#orderBys.push({
|
|
1543
|
+
column: this.#resolveColumn(columnOrTerms),
|
|
1544
|
+
direction,
|
|
1545
|
+
});
|
|
1313
1546
|
return this;
|
|
1314
1547
|
}
|
|
1315
1548
|
/**
|
|
@@ -1373,6 +1606,40 @@ export class ModelQuery {
|
|
|
1373
1606
|
});
|
|
1374
1607
|
return this;
|
|
1375
1608
|
}
|
|
1609
|
+
/** HAVING col IS NULL (Lucid/Knex `havingNull`). */
|
|
1610
|
+
havingNull(column) {
|
|
1611
|
+
return this.#pushHaving(column, "IS NULL", null);
|
|
1612
|
+
}
|
|
1613
|
+
/** HAVING col IS NOT NULL (Lucid/Knex `havingNotNull`). */
|
|
1614
|
+
havingNotNull(column) {
|
|
1615
|
+
return this.#pushHaving(column, "IS NOT NULL", null);
|
|
1616
|
+
}
|
|
1617
|
+
/** HAVING col IN (...) (Lucid/Knex `havingIn`). */
|
|
1618
|
+
havingIn(column, values) {
|
|
1619
|
+
return this.#pushHaving(column, "IN", [...values]);
|
|
1620
|
+
}
|
|
1621
|
+
/** HAVING col NOT IN (...) (Lucid/Knex `havingNotIn`). */
|
|
1622
|
+
havingNotIn(column, values) {
|
|
1623
|
+
return this.#pushHaving(column, "NOT IN", [...values]);
|
|
1624
|
+
}
|
|
1625
|
+
/** HAVING col BETWEEN ? AND ? (Lucid/Knex `havingBetween`). */
|
|
1626
|
+
havingBetween(column, range) {
|
|
1627
|
+
return this.#pushHaving(column, "BETWEEN", [...range]);
|
|
1628
|
+
}
|
|
1629
|
+
/** HAVING col NOT BETWEEN ? AND ? (Lucid/Knex `havingNotBetween`). */
|
|
1630
|
+
havingNotBetween(column, range) {
|
|
1631
|
+
return this.#pushHaving(column, "NOT BETWEEN", [...range]);
|
|
1632
|
+
}
|
|
1633
|
+
/** Push a HAVING entry with a raw (non-`#prep`'d) value — for IN/BETWEEN/NULL. */
|
|
1634
|
+
#pushHaving(column, operator, value) {
|
|
1635
|
+
this.#having.push({
|
|
1636
|
+
column: this.#resolveHavingCol(column),
|
|
1637
|
+
operator,
|
|
1638
|
+
value,
|
|
1639
|
+
type: "and",
|
|
1640
|
+
});
|
|
1641
|
+
return this;
|
|
1642
|
+
}
|
|
1376
1643
|
/**
|
|
1377
1644
|
* Resolve a HAVING column: a bare model property maps to its DB column
|
|
1378
1645
|
* (honouring `@Column({ columnName })`), but an aggregate expression
|
|
@@ -1428,17 +1695,21 @@ export class ModelQuery {
|
|
|
1428
1695
|
* the outer parameter list.
|
|
1429
1696
|
*/
|
|
1430
1697
|
union(query) {
|
|
1431
|
-
this.#unions.push({ query, all: false });
|
|
1698
|
+
this.#unions.push({ query: this.#resolveUnion(query), all: false });
|
|
1432
1699
|
return this;
|
|
1433
1700
|
}
|
|
1434
1701
|
/** `UNION ALL (<query>)` — duplicate-preserving {@link union}. */
|
|
1435
1702
|
unionAll(query) {
|
|
1436
|
-
this.#unions.push({ query, all: true });
|
|
1703
|
+
this.#unions.push({ query: this.#resolveUnion(query), all: true });
|
|
1437
1704
|
return this;
|
|
1438
1705
|
}
|
|
1439
1706
|
/** `INTERSECT (<query>)` — rows present in both (Lucid/Knex `intersect`). */
|
|
1440
1707
|
intersect(query) {
|
|
1441
|
-
this.#unions.push({
|
|
1708
|
+
this.#unions.push({
|
|
1709
|
+
query: this.#resolveUnion(query),
|
|
1710
|
+
all: false,
|
|
1711
|
+
op: "intersect",
|
|
1712
|
+
});
|
|
1442
1713
|
return this;
|
|
1443
1714
|
}
|
|
1444
1715
|
/**
|
|
@@ -1449,19 +1720,43 @@ export class ModelQuery {
|
|
|
1449
1720
|
* `E_UNSUPPORTED` there rather than emitting a syntax error.
|
|
1450
1721
|
*/
|
|
1451
1722
|
intersectAll(query) {
|
|
1452
|
-
this.#unions.push({
|
|
1723
|
+
this.#unions.push({
|
|
1724
|
+
query: this.#resolveUnion(query),
|
|
1725
|
+
all: true,
|
|
1726
|
+
op: "intersect",
|
|
1727
|
+
});
|
|
1453
1728
|
return this;
|
|
1454
1729
|
}
|
|
1455
1730
|
/** `EXCEPT (<query>)` — rows in this query but not the other (Lucid/Knex `except`). */
|
|
1456
1731
|
except(query) {
|
|
1457
|
-
this.#unions.push({
|
|
1732
|
+
this.#unions.push({
|
|
1733
|
+
query: this.#resolveUnion(query),
|
|
1734
|
+
all: false,
|
|
1735
|
+
op: "except",
|
|
1736
|
+
});
|
|
1458
1737
|
return this;
|
|
1459
1738
|
}
|
|
1460
1739
|
/** `EXCEPT ALL (<query>)` — duplicate-preserving {@link except}. Not on SQLite; see {@link intersectAll}. */
|
|
1461
1740
|
exceptAll(query) {
|
|
1462
|
-
this.#unions.push({
|
|
1741
|
+
this.#unions.push({
|
|
1742
|
+
query: this.#resolveUnion(query),
|
|
1743
|
+
all: true,
|
|
1744
|
+
op: "except",
|
|
1745
|
+
});
|
|
1463
1746
|
return this;
|
|
1464
1747
|
}
|
|
1748
|
+
/** A compound-query arg — an explicit ModelQuery OR a callback building one on this model. */
|
|
1749
|
+
#resolveUnion(query) {
|
|
1750
|
+
if (typeof query !== "function")
|
|
1751
|
+
return query;
|
|
1752
|
+
const sub = this.#freshQuery();
|
|
1753
|
+
query(sub);
|
|
1754
|
+
return sub;
|
|
1755
|
+
}
|
|
1756
|
+
/** A fresh, empty query on the SAME model + connection (for compound callbacks). */
|
|
1757
|
+
#freshQuery() {
|
|
1758
|
+
return new _a(this.#tableName, this.#db, this.#hydrateFn, this.#entityClass, this.#resolveColumn, this.#softDeletes, this.#dialect, this.#prepareValue, this.#onDomainEvents);
|
|
1759
|
+
}
|
|
1465
1760
|
/**
|
|
1466
1761
|
* `WITH <name> AS (<query>)` — register a Common Table Expression
|
|
1467
1762
|
* (AdonisJS/Lucid `with`). The CTE name is validated as an identifier; the
|
|
@@ -1481,8 +1776,11 @@ export class ModelQuery {
|
|
|
1481
1776
|
* The recursive term itself is a `UNION`/`UNION ALL` inside `query`, e.g.
|
|
1482
1777
|
* an anchor `SELECT` unioned with a select that references `<name>`.
|
|
1483
1778
|
*/
|
|
1484
|
-
withRecursive(name, query) {
|
|
1485
|
-
return this.#pushCte("withRecursive", name, query, {
|
|
1779
|
+
withRecursive(name, query, columns) {
|
|
1780
|
+
return this.#pushCte("withRecursive", name, query, {
|
|
1781
|
+
recursive: true,
|
|
1782
|
+
columns,
|
|
1783
|
+
});
|
|
1486
1784
|
}
|
|
1487
1785
|
/**
|
|
1488
1786
|
* `WITH <name> AS MATERIALIZED (<query>)` — force the CTE to be evaluated
|
|
@@ -1506,7 +1804,18 @@ export class ModelQuery {
|
|
|
1506
1804
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
1507
1805
|
throw new Error(`${method}(): CTE name '${name}' is not a valid identifier`);
|
|
1508
1806
|
}
|
|
1509
|
-
this.#ctes.push({ name, query, ...options });
|
|
1807
|
+
this.#ctes.push({ name, query: this.#resolveUnion(query), ...options });
|
|
1808
|
+
return this;
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* `@ManyToMany` only — project EXTRA pivot columns at query time (AdonisJS/Lucid
|
|
1812
|
+
* `pivotColumns([...])`), on top of any declared in the relation decorator. Read
|
|
1813
|
+
* off each loaded relation as `$extras.pivot_<col>`. Inert on non-m2m relations.
|
|
1814
|
+
*
|
|
1815
|
+
* userRepo.query().preload('skills', q => q.pivotColumns(['notes']))
|
|
1816
|
+
*/
|
|
1817
|
+
pivotColumns(columns) {
|
|
1818
|
+
this.#extraPivotColumns.push(...columns);
|
|
1510
1819
|
return this;
|
|
1511
1820
|
}
|
|
1512
1821
|
wherePivot(column, operatorOrValue, value) {
|
|
@@ -1534,17 +1843,21 @@ export class ModelQuery {
|
|
|
1534
1843
|
wherePivotIn(column, values) {
|
|
1535
1844
|
return this.whereInPivot(column, values);
|
|
1536
1845
|
}
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1846
|
+
whereNotPivot(column, operatorOrValue, value) {
|
|
1847
|
+
if (value === undefined) {
|
|
1848
|
+
return this.#pushPivotOp("and", column, "!=", operatorOrValue);
|
|
1849
|
+
}
|
|
1850
|
+
return this.#pushPivotOp("and", column, negateOperator(String(operatorOrValue)), value);
|
|
1540
1851
|
}
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1852
|
+
andWhereNotPivot(column, operatorOrValue, value) {
|
|
1853
|
+
return value === undefined
|
|
1854
|
+
? this.#pushPivotOp("and", column, "!=", operatorOrValue)
|
|
1855
|
+
: this.#pushPivotOp("and", column, negateOperator(String(operatorOrValue)), value);
|
|
1544
1856
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1857
|
+
orWhereNotPivot(column, operatorOrValue, value) {
|
|
1858
|
+
return value === undefined
|
|
1859
|
+
? this.#pushPivotOp("or", column, "!=", operatorOrValue)
|
|
1860
|
+
: this.#pushPivotOp("or", column, negateOperator(String(operatorOrValue)), value);
|
|
1548
1861
|
}
|
|
1549
1862
|
/** `@ManyToMany` only — `WHERE <pivotCol> NOT IN (...)` on the pivot table (AdonisJS Lucid `whereNotInPivot`). */
|
|
1550
1863
|
whereNotInPivot(column, values) {
|
|
@@ -1596,6 +1909,10 @@ export class ModelQuery {
|
|
|
1596
1909
|
get pivotConstraints() {
|
|
1597
1910
|
return this.#pivotWheres;
|
|
1598
1911
|
}
|
|
1912
|
+
/** Read-only accessor for query-time pivot columns — consumed by the m2m preload resolver. */
|
|
1913
|
+
get pivotColumnRequests() {
|
|
1914
|
+
return this.#extraPivotColumns;
|
|
1915
|
+
}
|
|
1599
1916
|
limit(n) {
|
|
1600
1917
|
// Guard here with a clear message — the Rust spec types limit as
|
|
1601
1918
|
// u64, so a negative/non-integer otherwise surfaces as a cryptic
|
|
@@ -1607,6 +1924,24 @@ export class ModelQuery {
|
|
|
1607
1924
|
this.#limit = n;
|
|
1608
1925
|
return this;
|
|
1609
1926
|
}
|
|
1927
|
+
/**
|
|
1928
|
+
* In a has-many preload callback, cap the related rows PER PARENT (Lucid
|
|
1929
|
+
* `groupLimit`) — a plain `.limit()` caps the whole result set across parents.
|
|
1930
|
+
* Compiled with a `ROW_NUMBER() OVER (PARTITION BY <fk> …)` window. Pair with
|
|
1931
|
+
* {@link groupOrderBy} to pick which N per parent.
|
|
1932
|
+
*/
|
|
1933
|
+
groupLimit(n) {
|
|
1934
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
1935
|
+
throw new Error(`groupLimit must be a non-negative integer, got ${n}`);
|
|
1936
|
+
}
|
|
1937
|
+
this.#groupLimit = n;
|
|
1938
|
+
return this;
|
|
1939
|
+
}
|
|
1940
|
+
/** ORDER BY for the {@link groupLimit} per-parent window (Lucid `groupOrderBy`). */
|
|
1941
|
+
groupOrderBy(column, direction = "asc") {
|
|
1942
|
+
this.#groupOrderBy = { column, direction };
|
|
1943
|
+
return this;
|
|
1944
|
+
}
|
|
1610
1945
|
offset(n) {
|
|
1611
1946
|
if (!Number.isInteger(n) || n < 0) {
|
|
1612
1947
|
throw new Error(`offset must be a non-negative integer, got ${n}`);
|
|
@@ -1754,6 +2089,7 @@ export class ModelQuery {
|
|
|
1754
2089
|
table: this.#tableName,
|
|
1755
2090
|
select: selectCols,
|
|
1756
2091
|
selectSubqueries: this.#selectSubqueries,
|
|
2092
|
+
selectRaw: this.#selectRaw,
|
|
1757
2093
|
wheres,
|
|
1758
2094
|
orderBy: this.#orderBys,
|
|
1759
2095
|
groupBy: this.#groupBy,
|
|
@@ -1762,18 +2098,9 @@ export class ModelQuery {
|
|
|
1762
2098
|
offset: this.#offset ?? null,
|
|
1763
2099
|
distinct: this.#distinct,
|
|
1764
2100
|
distinctOn: this.#distinctOn,
|
|
1765
|
-
ctes: this.#
|
|
1766
|
-
const { sql, params } = c.query.toSQL();
|
|
1767
|
-
return {
|
|
1768
|
-
name: c.name,
|
|
1769
|
-
sql,
|
|
1770
|
-
params,
|
|
1771
|
-
recursive: c.recursive ?? false,
|
|
1772
|
-
materialized: c.materialized ?? null,
|
|
1773
|
-
};
|
|
1774
|
-
}),
|
|
2101
|
+
ctes: this.#compiledCtes(),
|
|
1775
2102
|
unions: this.#unions.map((u) => {
|
|
1776
|
-
const { sql, params } = u.query
|
|
2103
|
+
const { sql, params } = u.query.#compiledNative();
|
|
1777
2104
|
return { sql, params, all: u.all, op: u.op ?? null };
|
|
1778
2105
|
}),
|
|
1779
2106
|
joins: this.#joins,
|
|
@@ -1784,10 +2111,106 @@ export class ModelQuery {
|
|
|
1784
2111
|
: null,
|
|
1785
2112
|
};
|
|
1786
2113
|
}
|
|
1787
|
-
/**
|
|
1788
|
-
|
|
2114
|
+
/**
|
|
2115
|
+
* Native compiled `{ sql, params }` (Postgres `$N`) — the form atlas executes
|
|
2116
|
+
* and embeds inside parent queries. Public `toSQL()` normalizes to Knex `?`.
|
|
2117
|
+
*/
|
|
2118
|
+
#compiledNative() {
|
|
1789
2119
|
const compiled = compileStatementNative(this.#buildSpec(), this.#dialect);
|
|
1790
|
-
|
|
2120
|
+
const sql = this.#commentPrefix() + compiled.statements[0];
|
|
2121
|
+
return { sql, params: compiled.params };
|
|
2122
|
+
}
|
|
2123
|
+
/**
|
|
2124
|
+
* Build SQL via the Rust query compiler (Lucid `toSQL`). `.sql` uses `?`
|
|
2125
|
+
* placeholders (Knex-normalized, like Lucid); `.toNative()` yields the native
|
|
2126
|
+
* form. Returns `bindings` (Lucid's name) and `params` (atlas's) — same array.
|
|
2127
|
+
*/
|
|
2128
|
+
toSQL() {
|
|
2129
|
+
const { sql, params } = this.#compiledNative();
|
|
2130
|
+
return compiledStatement(sql, params);
|
|
2131
|
+
}
|
|
2132
|
+
/** `{ sql, bindings }` — the compiled native query (Lucid/Knex `toNative`). */
|
|
2133
|
+
toNative() {
|
|
2134
|
+
const { sql, params } = this.#compiledNative();
|
|
2135
|
+
return { sql, bindings: params };
|
|
2136
|
+
}
|
|
2137
|
+
/** The model class this query targets (Adonis Lucid `query.model`). */
|
|
2138
|
+
get model() {
|
|
2139
|
+
return this.#entityClass;
|
|
2140
|
+
}
|
|
2141
|
+
/** Apply `cb` only on the given dialect(s) (Lucid `ifDialect`; Lucid names accepted). */
|
|
2142
|
+
ifDialect(dialect, cb) {
|
|
2143
|
+
const set = (Array.isArray(dialect) ? dialect : [dialect]).map(normalizeDialect);
|
|
2144
|
+
if (set.includes(this.#dialect))
|
|
2145
|
+
cb(this);
|
|
2146
|
+
return this;
|
|
2147
|
+
}
|
|
2148
|
+
/** Apply `cb` on every dialect EXCEPT the given one(s) (Lucid `unlessDialect`). */
|
|
2149
|
+
unlessDialect(dialect, cb) {
|
|
2150
|
+
const set = (Array.isArray(dialect) ? dialect : [dialect]).map(normalizeDialect);
|
|
2151
|
+
if (!set.includes(this.#dialect))
|
|
2152
|
+
cb(this);
|
|
2153
|
+
return this;
|
|
2154
|
+
}
|
|
2155
|
+
/** Prepend a `/* … */` SQL comment to the compiled query (Lucid/Knex `comment`). */
|
|
2156
|
+
comment(text) {
|
|
2157
|
+
if (text.includes("*/")) {
|
|
2158
|
+
throw new Error("comment() text may not contain '*/'");
|
|
2159
|
+
}
|
|
2160
|
+
this.#comments.push(text);
|
|
2161
|
+
return this;
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Set a caller-facing statement timeout in milliseconds (Lucid `timeout(ms)`).
|
|
2165
|
+
* The awaiting promise rejects after `ms` on the primary result fetch. With
|
|
2166
|
+
* `{ cancel: true }` a SERVER-side statement timeout is also applied — Postgres
|
|
2167
|
+
* `statement_timeout`, MySQL `MAX_EXECUTION_TIME` (SELECT) — so the server
|
|
2168
|
+
* aborts the query, not just the client. (SQLite has no server timeout; the
|
|
2169
|
+
* client race applies.) Called with no argument it clears the timeout.
|
|
2170
|
+
*/
|
|
2171
|
+
timeout(ms, options) {
|
|
2172
|
+
// `{ cancel: true }` also applies a SERVER-side statement timeout (Postgres
|
|
2173
|
+
// statement_timeout / MySQL MAX_EXECUTION_TIME for SELECT); SQLite has none,
|
|
2174
|
+
// so the client race applies there.
|
|
2175
|
+
this.#timeoutMs = ms;
|
|
2176
|
+
this.#cancelTimeout = options?.cancel === true;
|
|
2177
|
+
return this;
|
|
2178
|
+
}
|
|
2179
|
+
/**
|
|
2180
|
+
* Race `work` against the configured `.timeout(ms)`. Rejects the awaiter after
|
|
2181
|
+
* `ms`; the losing DB promise is swallowed so a post-timeout driver error never
|
|
2182
|
+
* surfaces as an unhandled rejection. No timeout set → returns `work` as-is.
|
|
2183
|
+
*/
|
|
2184
|
+
#raceTimeout(work) {
|
|
2185
|
+
const ms = this.#timeoutMs;
|
|
2186
|
+
if (!ms || ms <= 0)
|
|
2187
|
+
return work;
|
|
2188
|
+
let timer;
|
|
2189
|
+
const guard = new Promise((_, reject) => {
|
|
2190
|
+
timer = setTimeout(() => reject(new Error(`Query timed out after ${ms}ms`)), ms);
|
|
2191
|
+
});
|
|
2192
|
+
work.catch(() => { });
|
|
2193
|
+
return Promise.race([work, guard]).finally(() => clearTimeout(timer));
|
|
2194
|
+
}
|
|
2195
|
+
/** The `/* … */` prefix for the compiled SQL, or empty when no comments. */
|
|
2196
|
+
/** Compile the registered CTEs to the wire shape — shared by SELECT and DML. */
|
|
2197
|
+
#compiledCtes() {
|
|
2198
|
+
return this.#ctes.map((c) => {
|
|
2199
|
+
const { sql, params } = c.query.#compiledNative();
|
|
2200
|
+
return {
|
|
2201
|
+
name: c.name,
|
|
2202
|
+
sql,
|
|
2203
|
+
params,
|
|
2204
|
+
recursive: c.recursive ?? false,
|
|
2205
|
+
materialized: c.materialized ?? null,
|
|
2206
|
+
columns: c.columns ?? [],
|
|
2207
|
+
};
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
#commentPrefix() {
|
|
2211
|
+
return this.#comments.length > 0
|
|
2212
|
+
? `${this.#comments.map((c) => `/* ${c} */`).join(" ")} `
|
|
2213
|
+
: "";
|
|
1791
2214
|
}
|
|
1792
2215
|
/**
|
|
1793
2216
|
* Cached exec result. Memoizing the promise makes the builder a one-shot
|
|
@@ -1811,8 +2234,8 @@ export class ModelQuery {
|
|
|
1811
2234
|
return results;
|
|
1812
2235
|
}
|
|
1813
2236
|
async #doExec() {
|
|
1814
|
-
const { sql, params } = this
|
|
1815
|
-
const rawRows = await this.#db.query(sql, params, this.#meta("exec"));
|
|
2237
|
+
const { sql, params } = this.#compiledNative();
|
|
2238
|
+
const rawRows = await this.#raceTimeout(this.#db.query(sql, params, this.#meta("exec")));
|
|
1816
2239
|
// Peel withCount / withAggregate alias columns off the raw row into $extras
|
|
1817
2240
|
// BEFORE hydration, so the hydrator doesn't try to interpret them as columns.
|
|
1818
2241
|
const extraKeys = this.#selectSubqueries.map((s) => s.alias);
|
|
@@ -1836,6 +2259,14 @@ export class ModelQuery {
|
|
|
1836
2259
|
if (this.#preloads.size > 0 && this.#entityClass && entities.length > 0) {
|
|
1837
2260
|
await this.#resolvePreloads(entities);
|
|
1838
2261
|
}
|
|
2262
|
+
// rowTransformer callbacks run per instance AFTER preloads, before resolve
|
|
2263
|
+
// (Lucid), so a transformer can read the eager-loaded relations.
|
|
2264
|
+
if (this.#rowTransformers.length > 0) {
|
|
2265
|
+
for (const entity of entities) {
|
|
2266
|
+
for (const transform of this.#rowTransformers)
|
|
2267
|
+
transform.run(entity);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
1839
2270
|
return entities;
|
|
1840
2271
|
}
|
|
1841
2272
|
/**
|
|
@@ -1845,16 +2276,20 @@ export class ModelQuery {
|
|
|
1845
2276
|
* instances aren't needed.
|
|
1846
2277
|
*/
|
|
1847
2278
|
async pojo() {
|
|
1848
|
-
const { sql, params } = this
|
|
2279
|
+
const { sql, params } = this.#compiledNative();
|
|
1849
2280
|
return this.#db.query(sql, params);
|
|
1850
2281
|
}
|
|
1851
2282
|
/**
|
|
1852
2283
|
* Thread arbitrary context onto every instance this query hydrates, exposed as
|
|
1853
2284
|
* `entity.$sideloaded` (AdonisJS Lucid `sideload`) — e.g. the current tenant or
|
|
1854
|
-
* user, so hooks/computed can read it.
|
|
2285
|
+
* user, so hooks/computed can read it. REPLACES the current sideloaded data by
|
|
2286
|
+
* default (Lucid); pass `merge = true` to merge instead. The data also
|
|
2287
|
+
* propagates to preloaded relation queries. Chainable.
|
|
1855
2288
|
*/
|
|
1856
|
-
sideload(values) {
|
|
1857
|
-
this.#sideloaded =
|
|
2289
|
+
sideload(values, merge = false) {
|
|
2290
|
+
this.#sideloaded = merge
|
|
2291
|
+
? { ...this.#sideloaded, ...values }
|
|
2292
|
+
: { ...values };
|
|
1858
2293
|
return this;
|
|
1859
2294
|
}
|
|
1860
2295
|
/** Resolve preloaded relations via batched subqueries (no N+1). */
|
|
@@ -1870,6 +2305,14 @@ export class ModelQuery {
|
|
|
1870
2305
|
if (!ctx)
|
|
1871
2306
|
continue;
|
|
1872
2307
|
const allRelated = await this.#resolveOneRelation(entities, relationName, relation.type, ctx);
|
|
2308
|
+
// Propagate the parent query's sideloaded context onto the preloaded
|
|
2309
|
+
// relation instances (Adonis Lucid). The related row's own sideloaded
|
|
2310
|
+
// data (if any) wins over the inherited context.
|
|
2311
|
+
if (this.#sideloaded) {
|
|
2312
|
+
for (const related of allRelated) {
|
|
2313
|
+
related.$sideloaded = { ...this.#sideloaded, ...related.$sideloaded };
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
1873
2316
|
await this.#applyNestedPreloads(allRelated, ctx);
|
|
1874
2317
|
}
|
|
1875
2318
|
}
|
|
@@ -1885,7 +2328,13 @@ export class ModelQuery {
|
|
|
1885
2328
|
// entities using Adonis' `declare field: T` pattern have no own-properties
|
|
1886
2329
|
// on a freshly constructed instance, so `key in entity` is always false and
|
|
1887
2330
|
// every column would be silently dropped. Mirrors `BaseRepository.#hydrate`.
|
|
1888
|
-
|
|
2331
|
+
// For m2m the pivot's otherKey references `relatedKey` (default the related
|
|
2332
|
+
// PK); the load must filter/index rows by THAT column, not always the PK.
|
|
2333
|
+
const relatedPkName = (relation.type === "manyToMany"
|
|
2334
|
+
? relation.pivot?.relatedKey
|
|
2335
|
+
: undefined) ??
|
|
2336
|
+
getPrimaryKey(relatedClass) ??
|
|
2337
|
+
"id";
|
|
1889
2338
|
const validColumns = new Set();
|
|
1890
2339
|
// Reverse map (db column → property) so an explicit `@Column({ columnName })`
|
|
1891
2340
|
// on the related entity hydrates correctly — mirrors `BaseRepository.#hydrate`.
|
|
@@ -2148,9 +2597,13 @@ export class ModelQuery {
|
|
|
2148
2597
|
// also runs (again) inside runRelationQuery against the related table; both
|
|
2149
2598
|
// runs are pure builder mutations, and pivot constraints are inert there.
|
|
2150
2599
|
const pivotWheres = [];
|
|
2600
|
+
// Query-time pivotColumns([...]) requested in the preload callback (replayed
|
|
2601
|
+
// on the scratch builder below) OR directly on this query (related().query()).
|
|
2602
|
+
const extraPivotCols = [...this.#extraPivotColumns];
|
|
2151
2603
|
if (ctx.nestedCallback) {
|
|
2152
2604
|
const scratch = new _a(ctx.relatedTable, this.#db, (r) => r, ctx.relatedClass, buildColumnResolver(ctx.relatedClass), false, this.#dialect, buildValuePreparer(ctx.relatedClass));
|
|
2153
2605
|
ctx.nestedCallback(scratch);
|
|
2606
|
+
extraPivotCols.push(...scratch.pivotColumnRequests);
|
|
2154
2607
|
// Apply the pivot column adapters' `prepare` to wherePivot values, so a
|
|
2155
2608
|
// filter like wherePivot('amount', new Money(1)) matches what attach()/
|
|
2156
2609
|
// sync() stored (they prepare the same extras on write).
|
|
@@ -2193,7 +2646,11 @@ export class ModelQuery {
|
|
|
2193
2646
|
];
|
|
2194
2647
|
// Step 2 — load all related entities in one query
|
|
2195
2648
|
const relRows = await ctx.runRelationQuery(ctx.relatedPk, otherIds);
|
|
2196
|
-
|
|
2649
|
+
// Declared pivot columns (decorator) + any requested at query time
|
|
2650
|
+
// (`pivotColumns([...])`, from the callback or this query), de-duplicated.
|
|
2651
|
+
const pivotCols = [
|
|
2652
|
+
...new Set([...(pivot.pivotColumns ?? []), ...extraPivotCols]),
|
|
2653
|
+
];
|
|
2197
2654
|
const pivotAdapters = pivot.pivotColumnAdapters ?? {};
|
|
2198
2655
|
// When pivot extras are projected, each (parent, related) edge gets its OWN
|
|
2199
2656
|
// hydrated instance so per-edge `$extras.pivot_<col>` values never clobber
|
|
@@ -2328,9 +2785,52 @@ export class ModelQuery {
|
|
|
2328
2785
|
relation.onQuery(sub);
|
|
2329
2786
|
if (userCallback)
|
|
2330
2787
|
userCallback(sub);
|
|
2331
|
-
|
|
2788
|
+
// Top-N per parent (Lucid groupLimit) → a ROW_NUMBER() window over the fk.
|
|
2789
|
+
if (sub.#groupLimit != null) {
|
|
2790
|
+
return this.#runGroupLimited(sub, column, sub.#groupLimit);
|
|
2791
|
+
}
|
|
2792
|
+
const { sql, params } = sub.#compiledNative();
|
|
2332
2793
|
return this.#db.query(sql, params);
|
|
2333
2794
|
}
|
|
2795
|
+
/**
|
|
2796
|
+
* Run a has-many preload query capped to `n` rows PER PARENT (Lucid
|
|
2797
|
+
* `groupLimit`). Wraps the inner query with a `ROW_NUMBER() OVER (PARTITION BY
|
|
2798
|
+
* <fk> ORDER BY <groupOrderBy | pk>)` window, then keeps `rn <= n`. Supported on
|
|
2799
|
+
* Postgres, MySQL 8+, and SQLite 3.25+ (all have window functions).
|
|
2800
|
+
*/
|
|
2801
|
+
async #runGroupLimited(sub, fkColumn, n) {
|
|
2802
|
+
const quoteSeg = (name) => {
|
|
2803
|
+
const qc = this.#dialect === "mysql" ? "`" : '"';
|
|
2804
|
+
return name
|
|
2805
|
+
.split(".")
|
|
2806
|
+
.map((s) => {
|
|
2807
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) {
|
|
2808
|
+
throw new Error(`groupLimit: unsafe identifier '${name}'`);
|
|
2809
|
+
}
|
|
2810
|
+
return `${qc}${s}${qc}`;
|
|
2811
|
+
})
|
|
2812
|
+
.join(".");
|
|
2813
|
+
};
|
|
2814
|
+
const orderCol = sub.#groupOrderBy
|
|
2815
|
+
? sub.#resolveColumn(sub.#groupOrderBy.column)
|
|
2816
|
+
: (getPrimaryKey(sub.#entityClass) ?? "id");
|
|
2817
|
+
const dir = sub.#groupOrderBy?.direction === "desc" ? "DESC" : "ASC";
|
|
2818
|
+
sub.#selectRaw.push({
|
|
2819
|
+
sql: `ROW_NUMBER() OVER (PARTITION BY ${quoteSeg(fkColumn)} ORDER BY ${quoteSeg(orderCol)} ${dir}) AS __atlas_rn`,
|
|
2820
|
+
params: [],
|
|
2821
|
+
});
|
|
2822
|
+
const inner = sub.#compiledNative();
|
|
2823
|
+
const ph = this.#dialect === "postgres" ? `$${inner.params.length + 1}` : "?";
|
|
2824
|
+
const outerSql = `SELECT * FROM (${inner.sql}) AS __atlas_grp WHERE __atlas_rn <= ${ph}`;
|
|
2825
|
+
const rows = await this.#db.query(outerSql, [
|
|
2826
|
+
...inner.params,
|
|
2827
|
+
n,
|
|
2828
|
+
]);
|
|
2829
|
+
// Strip the window helper column so it doesn't leak into hydration.
|
|
2830
|
+
for (const row of rows)
|
|
2831
|
+
delete row.__atlas_rn;
|
|
2832
|
+
return rows;
|
|
2833
|
+
}
|
|
2334
2834
|
/**
|
|
2335
2835
|
* Build a correlated subquery over a relation. Returns `SubqueryProjection`
|
|
2336
2836
|
* used by withCount / withAggregate. Default select is `COUNT(*)` for `'count'`
|
|
@@ -2448,14 +2948,26 @@ export class ModelQuery {
|
|
|
2448
2948
|
}
|
|
2449
2949
|
return sub;
|
|
2450
2950
|
}
|
|
2451
|
-
|
|
2452
|
-
return this.#pushJoin("INNER", table, leftOrBuild, right);
|
|
2951
|
+
join(table, leftOrBuild, operatorOrRight, right) {
|
|
2952
|
+
return this.#pushJoin("INNER", table, leftOrBuild, operatorOrRight, right);
|
|
2953
|
+
}
|
|
2954
|
+
innerJoin(table, leftOrBuild, operatorOrRight, right) {
|
|
2955
|
+
return this.#pushJoin("INNER", table, leftOrBuild, operatorOrRight, right);
|
|
2956
|
+
}
|
|
2957
|
+
leftJoin(table, leftOrBuild, operatorOrRight, right) {
|
|
2958
|
+
return this.#pushJoin("LEFT", table, leftOrBuild, operatorOrRight, right);
|
|
2959
|
+
}
|
|
2960
|
+
leftOuterJoin(table, leftOrBuild, operatorOrRight, right) {
|
|
2961
|
+
return this.#pushJoin("LEFT", table, leftOrBuild, operatorOrRight, right);
|
|
2453
2962
|
}
|
|
2454
|
-
|
|
2455
|
-
return this.#pushJoin("
|
|
2963
|
+
rightJoin(table, leftOrBuild, operatorOrRight, right) {
|
|
2964
|
+
return this.#pushJoin("RIGHT", table, leftOrBuild, operatorOrRight, right);
|
|
2456
2965
|
}
|
|
2457
|
-
|
|
2458
|
-
return this.#pushJoin("RIGHT", table, leftOrBuild, right);
|
|
2966
|
+
rightOuterJoin(table, leftOrBuild, operatorOrRight, right) {
|
|
2967
|
+
return this.#pushJoin("RIGHT", table, leftOrBuild, operatorOrRight, right);
|
|
2968
|
+
}
|
|
2969
|
+
fullOuterJoin(table, leftOrBuild, operatorOrRight, right) {
|
|
2970
|
+
return this.#pushJoin("FULL OUTER", table, leftOrBuild, operatorOrRight, right);
|
|
2459
2971
|
}
|
|
2460
2972
|
crossJoin(table) {
|
|
2461
2973
|
const tq = this.#quoteCol(table);
|
|
@@ -2501,8 +3013,15 @@ export class ModelQuery {
|
|
|
2501
3013
|
return this.innerJoin(table, left, right);
|
|
2502
3014
|
}
|
|
2503
3015
|
// === Story 29.5 — aggregates / exists / pluck =====================================================
|
|
2504
|
-
|
|
3016
|
+
/**
|
|
3017
|
+
* SELECT DISTINCT (Lucid/Knex `distinct`). With columns, those replace the
|
|
3018
|
+
* projection too — `distinct('a', 'b')` ≈ `SELECT DISTINCT a, b`.
|
|
3019
|
+
*/
|
|
3020
|
+
distinct(...columns) {
|
|
2505
3021
|
this.#distinct = true;
|
|
3022
|
+
if (columns.length > 0) {
|
|
3023
|
+
this.#select = columns.map((c) => this.#resolveSelect(c));
|
|
3024
|
+
}
|
|
2506
3025
|
return this;
|
|
2507
3026
|
}
|
|
2508
3027
|
/**
|
|
@@ -2542,7 +3061,7 @@ export class ModelQuery {
|
|
|
2542
3061
|
const clone = this.clone();
|
|
2543
3062
|
clone.#select = ["1"];
|
|
2544
3063
|
clone.#limit = 1;
|
|
2545
|
-
const { sql, params } = clone
|
|
3064
|
+
const { sql, params } = clone.#compiledNative();
|
|
2546
3065
|
const rows = await this.#db.query(sql, params, this.#meta("exists"));
|
|
2547
3066
|
return rows.length > 0;
|
|
2548
3067
|
}
|
|
@@ -2554,7 +3073,7 @@ export class ModelQuery {
|
|
|
2554
3073
|
const col = this.#resolveColumn(column);
|
|
2555
3074
|
const clone = this.clone();
|
|
2556
3075
|
clone.#select = [col];
|
|
2557
|
-
const { sql, params } = clone
|
|
3076
|
+
const { sql, params } = clone.#compiledNative();
|
|
2558
3077
|
const rows = await this.#db.query(sql, params);
|
|
2559
3078
|
return rows.map((row) => {
|
|
2560
3079
|
const v = row[col];
|
|
@@ -2599,9 +3118,11 @@ export class ModelQuery {
|
|
|
2599
3118
|
elseFn(this);
|
|
2600
3119
|
return this;
|
|
2601
3120
|
}
|
|
2602
|
-
unless(condition, fn) {
|
|
3121
|
+
unless(condition, fn, elseFn) {
|
|
2603
3122
|
if (!condition)
|
|
2604
3123
|
fn(this);
|
|
3124
|
+
else if (elseFn)
|
|
3125
|
+
elseFn(this);
|
|
2605
3126
|
return this;
|
|
2606
3127
|
}
|
|
2607
3128
|
// === Story 29.10 — pagination =====================================================================
|
|
@@ -2609,11 +3130,17 @@ export class ModelQuery {
|
|
|
2609
3130
|
async paginate(page, perPage) {
|
|
2610
3131
|
const p = Math.max(1, Math.floor(page));
|
|
2611
3132
|
const pp = Math.max(1, Math.floor(perPage));
|
|
2612
|
-
//
|
|
2613
|
-
//
|
|
2614
|
-
|
|
2615
|
-
// COUNT
|
|
3133
|
+
// Adonis Lucid hook order:
|
|
3134
|
+
// beforePaginate([countQuery, query]) → beforeFetch(query)
|
|
3135
|
+
// → (count + data queries) → afterPaginate(paginator) → afterFetch(rows)
|
|
3136
|
+
// The COUNT and data builders are separate clones so a beforePaginate hook
|
|
3137
|
+
// can constrain BOTH (keeping totals in sync), exactly as documented.
|
|
2616
3138
|
const countQ = this.clone();
|
|
3139
|
+
const dataQ = this.clone();
|
|
3140
|
+
await fireHooks(this.#entityClass, "beforePaginate", [countQ, dataQ]);
|
|
3141
|
+
// beforeFetch fires on the main (data) query before either query runs.
|
|
3142
|
+
await fireHooks(this.#entityClass, "beforeFetch", dataQ);
|
|
3143
|
+
// COUNT(*) — strip pagination/order noise from the count clone.
|
|
2617
3144
|
countQ.#limit = undefined;
|
|
2618
3145
|
countQ.#offset = undefined;
|
|
2619
3146
|
countQ.#orderBys = [];
|
|
@@ -2624,29 +3151,32 @@ export class ModelQuery {
|
|
|
2624
3151
|
// group's own size), so `rows[0].count` would be the first group's size, not
|
|
2625
3152
|
// the number of pages. Lucid counts via a subquery: wrap the grouped query
|
|
2626
3153
|
// (select + groupBy + having preserved) and count its rows = group count.
|
|
2627
|
-
const inner = countQ
|
|
3154
|
+
const inner = countQ.#compiledNative();
|
|
2628
3155
|
cSql = `SELECT COUNT(*) AS count FROM (${inner.sql}) AS __paginate_count`;
|
|
2629
3156
|
cParams = inner.params;
|
|
2630
3157
|
}
|
|
2631
3158
|
else {
|
|
2632
3159
|
countQ.#select = ["COUNT(*) AS count"];
|
|
2633
|
-
const flat = countQ
|
|
3160
|
+
const flat = countQ.#compiledNative();
|
|
2634
3161
|
cSql = flat.sql;
|
|
2635
3162
|
cParams = flat.params;
|
|
2636
3163
|
}
|
|
2637
3164
|
const cRows = await this.#db.query(cSql, cParams, this.#meta("paginate"));
|
|
2638
3165
|
const total = Number(cRows[0]?.count ?? 0);
|
|
2639
|
-
const dataQ = this.clone();
|
|
2640
3166
|
dataQ.#limit = pp;
|
|
2641
3167
|
dataQ.#offset = (p - 1) * pp;
|
|
2642
|
-
// `#doExec`
|
|
2643
|
-
//
|
|
3168
|
+
// `#doExec` runs the raw fetch + preloads. beforeFetch already fired above;
|
|
3169
|
+
// afterFetch fires AFTER afterPaginate (Lucid order), so #doExec must not
|
|
3170
|
+
// fire either itself — hence #doExec, not exec().
|
|
2644
3171
|
const items = await dataQ.#doExec();
|
|
2645
|
-
await fireHooks(this.#entityClass, "afterPaginate", items);
|
|
2646
3172
|
const metaKeys = this.#entityClass
|
|
2647
3173
|
? getNamingStrategy(this.#entityClass).paginationMetaKeys?.()
|
|
2648
3174
|
: undefined;
|
|
2649
|
-
|
|
3175
|
+
const paginator = new Paginator(items, { total, perPage: pp, currentPage: p }, metaKeys);
|
|
3176
|
+
// Lucid: afterPaginate(paginator) then afterFetch(rows).
|
|
3177
|
+
await fireHooks(this.#entityClass, "afterPaginate", paginator);
|
|
3178
|
+
await fireHooks(this.#entityClass, "afterFetch", items);
|
|
3179
|
+
return paginator;
|
|
2650
3180
|
}
|
|
2651
3181
|
/**
|
|
2652
3182
|
* Cursor-based pagination — base64 opaque keyset, multi-column aware.
|
|
@@ -2729,6 +3259,17 @@ export class ModelQuery {
|
|
|
2729
3259
|
this.#debugFlag = flag;
|
|
2730
3260
|
return this;
|
|
2731
3261
|
}
|
|
3262
|
+
/**
|
|
3263
|
+
* Attach arbitrary metadata to the `db:query` event this query emits (Adonis
|
|
3264
|
+
* Lucid `reporterData`) — request id, user id, feature flag, … A listener
|
|
3265
|
+
* reads it off `event.reporterData`. Repeated calls merge. Setting it also
|
|
3266
|
+
* forces emission (like {@link debug}), so the data actually reaches a listener.
|
|
3267
|
+
*/
|
|
3268
|
+
reporterData(data) {
|
|
3269
|
+
this.#reporterData = { ...this.#reporterData, ...data };
|
|
3270
|
+
this.#debugFlag = true;
|
|
3271
|
+
return this;
|
|
3272
|
+
}
|
|
2732
3273
|
/**
|
|
2733
3274
|
* Context attached to each statement this query runs, so a `db:query`
|
|
2734
3275
|
* listener can say which model and which call produced it — and so
|
|
@@ -2742,16 +3283,16 @@ export class ModelQuery {
|
|
|
2742
3283
|
model: this.#entityClass.name,
|
|
2743
3284
|
method,
|
|
2744
3285
|
debug: this.#debugFlag,
|
|
3286
|
+
reporterData: this.#reporterData,
|
|
3287
|
+
serverTimeoutMs: this.#cancelTimeout && this.#timeoutMs != null
|
|
3288
|
+
? this.#timeoutMs
|
|
3289
|
+
: undefined,
|
|
2745
3290
|
};
|
|
2746
3291
|
}
|
|
2747
3292
|
/** Returns the compiled SQL with bindings interpolated as dialect-safe literals. */
|
|
2748
3293
|
toQuery() {
|
|
2749
|
-
const { sql, params } = this
|
|
2750
|
-
|
|
2751
|
-
return sql.replace(/\?|\$\d+/g, () => {
|
|
2752
|
-
const v = params[i++];
|
|
2753
|
-
return this.#literalEscape(v);
|
|
2754
|
-
});
|
|
3294
|
+
const { sql, params } = this.#compiledNative();
|
|
3295
|
+
return interpolateQuery(sql, params);
|
|
2755
3296
|
}
|
|
2756
3297
|
/** Deep clone of this query — mutations on the clone never affect the original. */
|
|
2757
3298
|
clone() {
|
|
@@ -2761,9 +3302,20 @@ export class ModelQuery {
|
|
|
2761
3302
|
c.#orderBys = [...this.#orderBys];
|
|
2762
3303
|
c.#select = [...this.#select];
|
|
2763
3304
|
c.#limit = this.#limit;
|
|
3305
|
+
c.#groupLimit = this.#groupLimit;
|
|
3306
|
+
c.#groupOrderBy = this.#groupOrderBy
|
|
3307
|
+
? { ...this.#groupOrderBy }
|
|
3308
|
+
: undefined;
|
|
2764
3309
|
c.#offset = this.#offset;
|
|
2765
3310
|
c.#preloads = new Map(this.#preloads);
|
|
3311
|
+
c.#rowTransformers = [...this.#rowTransformers];
|
|
2766
3312
|
c.#selectSubqueries = structuredClone(this.#selectSubqueries);
|
|
3313
|
+
c.#selectRaw = this.#selectRaw.map((s) => ({
|
|
3314
|
+
...s,
|
|
3315
|
+
params: [...s.params],
|
|
3316
|
+
}));
|
|
3317
|
+
c.#timeoutMs = this.#timeoutMs;
|
|
3318
|
+
c.#cancelTimeout = this.#cancelTimeout;
|
|
2767
3319
|
c.#joins = this.#joins.map((j) => ({ sql: j.sql, params: [...j.params] }));
|
|
2768
3320
|
c.#lockMode = this.#lockMode;
|
|
2769
3321
|
c.#lockModifier = this.#lockModifier;
|
|
@@ -2777,6 +3329,7 @@ export class ModelQuery {
|
|
|
2777
3329
|
query: e.query.clone(),
|
|
2778
3330
|
recursive: e.recursive,
|
|
2779
3331
|
materialized: e.materialized,
|
|
3332
|
+
columns: e.columns ? [...e.columns] : undefined,
|
|
2780
3333
|
}));
|
|
2781
3334
|
c.#unions = this.#unions.map((u) => ({
|
|
2782
3335
|
query: u.query.clone(),
|
|
@@ -2784,34 +3337,41 @@ export class ModelQuery {
|
|
|
2784
3337
|
op: u.op,
|
|
2785
3338
|
}));
|
|
2786
3339
|
c.#pivotWheres = structuredCloneSafe(this.#pivotWheres);
|
|
3340
|
+
c.#extraPivotColumns = [...this.#extraPivotColumns];
|
|
2787
3341
|
// Pure closure over pivot metadata — safe to share by reference; it reads the
|
|
2788
3342
|
// clone's own #pivotWheres at build time (passed in), holding no query state.
|
|
2789
3343
|
c.#pivotExists = this.#pivotExists;
|
|
2790
3344
|
c.#debugFlag = this.#debugFlag;
|
|
3345
|
+
c.#reporterData = this.#reporterData
|
|
3346
|
+
? { ...this.#reporterData }
|
|
3347
|
+
: undefined;
|
|
3348
|
+
c.#comments = [...this.#comments];
|
|
2791
3349
|
return c;
|
|
2792
3350
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
3351
|
+
update(patchOrColumn, valueOrReturning) {
|
|
3352
|
+
const patch = typeof patchOrColumn === "string"
|
|
3353
|
+
? { [patchOrColumn]: valueOrReturning }
|
|
3354
|
+
: patchOrColumn;
|
|
3355
|
+
const returning = typeof patchOrColumn === "string"
|
|
3356
|
+
? undefined
|
|
3357
|
+
: valueOrReturning;
|
|
2796
3358
|
if (!patch || Object.keys(patch).length === 0) {
|
|
2797
3359
|
throw new Error("update() requires a non-empty payload");
|
|
2798
3360
|
}
|
|
2799
3361
|
// Lower each value through prepare (DateTime → ISO, @Column adapters) exactly
|
|
2800
3362
|
// like BaseRepository's write paths — the fluent update() must not bypass it.
|
|
2801
|
-
|
|
2802
|
-
const
|
|
3363
|
+
// A `db.raw(...)` value is emitted as a raw SET expression instead.
|
|
3364
|
+
const setPairs = Object.entries(patch).map(([k, v]) => v instanceof RawSql
|
|
3365
|
+
? [this.#resolveColumn(k), { raw: v.sql, rawParams: [...v.params] }]
|
|
3366
|
+
: [this.#resolveColumn(k), this.#prepareValue(k, v)]);
|
|
3367
|
+
return this.#makeDml((r) => ({
|
|
2803
3368
|
kind: "update",
|
|
2804
3369
|
table: this.#tableName,
|
|
2805
3370
|
set: setPairs,
|
|
2806
3371
|
wheres: this.#wheresForDml(),
|
|
2807
|
-
returning:
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
if (returning && returning.length > 0) {
|
|
2811
|
-
return this.#db.query(compiled.statements[0], compiled.params);
|
|
2812
|
-
}
|
|
2813
|
-
const r = await this.#db.execute(compiled.statements[0], compiled.params);
|
|
2814
|
-
return r.rowsAffected ?? 0;
|
|
3372
|
+
returning: r,
|
|
3373
|
+
ctes: this.#compiledCtes(),
|
|
3374
|
+
}), returning);
|
|
2815
3375
|
}
|
|
2816
3376
|
/**
|
|
2817
3377
|
* Execute a fluent DELETE. For a `@SoftDeletes` model this SOFT-deletes the
|
|
@@ -2820,28 +3380,29 @@ export class ModelQuery {
|
|
|
2820
3380
|
* model it issues a hard `DELETE`. Returns affected rows (or rows when
|
|
2821
3381
|
* `returning` is set).
|
|
2822
3382
|
*/
|
|
2823
|
-
|
|
3383
|
+
delete(returning) {
|
|
2824
3384
|
if (this.#softDeletes) {
|
|
2825
|
-
const
|
|
3385
|
+
const stampedAt = new Date().toISOString();
|
|
3386
|
+
return this.#makeDml((r) => ({
|
|
2826
3387
|
kind: "update",
|
|
2827
3388
|
table: this.#tableName,
|
|
2828
|
-
set: [[this.#deletedAtColumn(),
|
|
3389
|
+
set: [[this.#deletedAtColumn(), stampedAt]],
|
|
2829
3390
|
wheres: this.#wheresForDml(),
|
|
2830
|
-
returning:
|
|
2831
|
-
|
|
2832
|
-
|
|
3391
|
+
returning: r,
|
|
3392
|
+
ctes: this.#compiledCtes(),
|
|
3393
|
+
}), returning);
|
|
2833
3394
|
}
|
|
2834
3395
|
return this.forceDelete(returning);
|
|
2835
3396
|
}
|
|
2836
3397
|
/** Hard `DELETE` of the scoped rows, bypassing `@SoftDeletes` (AdonisJS/Lucid `forceDelete`). */
|
|
2837
|
-
|
|
2838
|
-
|
|
3398
|
+
forceDelete(returning) {
|
|
3399
|
+
return this.#makeDml((r) => ({
|
|
2839
3400
|
kind: "delete",
|
|
2840
3401
|
table: this.#tableName,
|
|
2841
3402
|
wheres: this.#wheresForDml(),
|
|
2842
|
-
returning:
|
|
2843
|
-
|
|
2844
|
-
|
|
3403
|
+
returning: r,
|
|
3404
|
+
ctes: this.#compiledCtes(),
|
|
3405
|
+
}), returning);
|
|
2845
3406
|
}
|
|
2846
3407
|
/**
|
|
2847
3408
|
* Bulk restore: clear `deleted_at` on the trashed rows matching the user's
|
|
@@ -2849,24 +3410,27 @@ export class ModelQuery {
|
|
|
2849
3410
|
* on a non-soft-delete model. Independent of the current soft-scope — it always
|
|
2850
3411
|
* targets trashed rows (`deleted_at IS NOT NULL`).
|
|
2851
3412
|
*/
|
|
2852
|
-
|
|
2853
|
-
if (!this.#softDeletes)
|
|
2854
|
-
return 0;
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
3413
|
+
restore(returning) {
|
|
3414
|
+
if (!this.#softDeletes) {
|
|
3415
|
+
return new DmlBuilder(() => Promise.resolve(0), () => ({ sql: "", bindings: [], params: [] }), this.#dmlHooks());
|
|
3416
|
+
}
|
|
3417
|
+
return this.#makeDml((r) => {
|
|
3418
|
+
const wheres = this.#userWheresForDml();
|
|
3419
|
+
wheres.push({
|
|
3420
|
+
column: this.#deletedAtColumn(),
|
|
3421
|
+
operator: "IS NOT NULL",
|
|
3422
|
+
value: null,
|
|
3423
|
+
type: "and",
|
|
3424
|
+
});
|
|
3425
|
+
return {
|
|
3426
|
+
kind: "update",
|
|
3427
|
+
table: this.#tableName,
|
|
3428
|
+
set: [[this.#deletedAtColumn(), null]],
|
|
3429
|
+
wheres,
|
|
3430
|
+
returning: r,
|
|
3431
|
+
ctes: this.#compiledCtes(),
|
|
3432
|
+
};
|
|
3433
|
+
}, returning);
|
|
2870
3434
|
}
|
|
2871
3435
|
increment(colOrPatch, amount = 1) {
|
|
2872
3436
|
return this.#runIncDec("increment", colOrPatch, amount);
|
|
@@ -2958,77 +3522,172 @@ export class ModelQuery {
|
|
|
2958
3522
|
.map((seg) => this.#quote(seg))
|
|
2959
3523
|
.join(".");
|
|
2960
3524
|
}
|
|
2961
|
-
#pushJoin(kind, table, leftOrBuild, right) {
|
|
3525
|
+
#pushJoin(kind, table, leftOrBuild, operatorOrRight, right) {
|
|
2962
3526
|
const tq = this.#quoteCol(table);
|
|
2963
3527
|
if (typeof leftOrBuild === "function") {
|
|
3528
|
+
const parts = [];
|
|
2964
3529
|
const jb = {
|
|
2965
|
-
parts
|
|
2966
|
-
on(l, r) {
|
|
2967
|
-
|
|
2968
|
-
|
|
3530
|
+
parts,
|
|
3531
|
+
on: (l, opOrR, r) => {
|
|
3532
|
+
parts.push(r === undefined
|
|
3533
|
+
? { kind: "and", left: l, right: opOrR }
|
|
3534
|
+
: { kind: "and", left: l, operator: opOrR, right: r });
|
|
3535
|
+
return jb;
|
|
3536
|
+
},
|
|
3537
|
+
andOn: (l, opOrR, r) => {
|
|
3538
|
+
parts.push(r === undefined
|
|
3539
|
+
? { kind: "and", left: l, right: opOrR }
|
|
3540
|
+
: { kind: "and", left: l, operator: opOrR, right: r });
|
|
3541
|
+
return jb;
|
|
3542
|
+
},
|
|
3543
|
+
orOn: (l, opOrR, r) => {
|
|
3544
|
+
parts.push(r === undefined
|
|
3545
|
+
? { kind: "or", left: l, right: opOrR }
|
|
3546
|
+
: { kind: "or", left: l, operator: opOrR, right: r });
|
|
3547
|
+
return jb;
|
|
3548
|
+
},
|
|
3549
|
+
onVal: (l, v) => {
|
|
3550
|
+
parts.push({ kind: "and", left: l, value: { v } });
|
|
3551
|
+
return jb;
|
|
3552
|
+
},
|
|
3553
|
+
andOnVal: (l, v) => {
|
|
3554
|
+
parts.push({ kind: "and", left: l, value: { v } });
|
|
3555
|
+
return jb;
|
|
2969
3556
|
},
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
return
|
|
3557
|
+
orOnVal: (l, v) => {
|
|
3558
|
+
parts.push({ kind: "or", left: l, value: { v } });
|
|
3559
|
+
return jb;
|
|
2973
3560
|
},
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
return
|
|
3561
|
+
onIn: (l, values) => {
|
|
3562
|
+
parts.push({ kind: "and", left: l, values: [...values] });
|
|
3563
|
+
return jb;
|
|
2977
3564
|
},
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
3565
|
+
onNotIn: (l, values) => {
|
|
3566
|
+
parts.push({
|
|
3567
|
+
kind: "and",
|
|
3568
|
+
left: l,
|
|
3569
|
+
values: [...values],
|
|
3570
|
+
notIn: true,
|
|
3571
|
+
});
|
|
3572
|
+
return jb;
|
|
3573
|
+
},
|
|
3574
|
+
onNull: (l) => {
|
|
3575
|
+
parts.push({ kind: "and", left: l, nullOp: "IS NULL" });
|
|
3576
|
+
return jb;
|
|
3577
|
+
},
|
|
3578
|
+
onNotNull: (l) => {
|
|
3579
|
+
parts.push({ kind: "and", left: l, nullOp: "IS NOT NULL" });
|
|
3580
|
+
return jb;
|
|
2981
3581
|
},
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
return
|
|
3582
|
+
onBetween: (l, range) => {
|
|
3583
|
+
parts.push({ kind: "and", left: l, between: [range[0], range[1]] });
|
|
3584
|
+
return jb;
|
|
3585
|
+
},
|
|
3586
|
+
onNotBetween: (l, range) => {
|
|
3587
|
+
parts.push({
|
|
3588
|
+
kind: "and",
|
|
3589
|
+
left: l,
|
|
3590
|
+
between: [range[0], range[1]],
|
|
3591
|
+
notBetween: true,
|
|
3592
|
+
});
|
|
3593
|
+
return jb;
|
|
2985
3594
|
},
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3595
|
+
onExists: (sub) => {
|
|
3596
|
+
const { sql, params } = this.#resolveUnion(sub).#compiledNative();
|
|
3597
|
+
parts.push({ kind: "and", exists: { sql, params, not: false } });
|
|
3598
|
+
return jb;
|
|
3599
|
+
},
|
|
3600
|
+
onNotExists: (sub) => {
|
|
3601
|
+
const { sql, params } = this.#resolveUnion(sub).#compiledNative();
|
|
3602
|
+
parts.push({ kind: "and", exists: { sql, params, not: true } });
|
|
3603
|
+
return jb;
|
|
2989
3604
|
},
|
|
2990
3605
|
};
|
|
2991
3606
|
leftOrBuild(jb);
|
|
2992
|
-
//
|
|
3607
|
+
// A BASE-table column runs the full model prepare (DateTime→ISO + @Column
|
|
3608
|
+
// adapters/casts), keyed by its property. A FOREIGN join column must NOT
|
|
3609
|
+
// borrow the root model's adapter for a same-named column on another
|
|
3610
|
+
// table — apply only universal type-lowering, matching Knex's
|
|
3611
|
+
// model-agnostic join binding.
|
|
3612
|
+
const prep = (col, v) => {
|
|
3613
|
+
const dot = col.lastIndexOf(".");
|
|
3614
|
+
const tablePrefix = dot >= 0 ? col.slice(0, dot) : "";
|
|
3615
|
+
const leaf = dot >= 0 ? col.slice(dot + 1) : col;
|
|
3616
|
+
const isBaseColumn = tablePrefix === "" || sameTableRef(tablePrefix, this.#tableName);
|
|
3617
|
+
return isBaseColumn ? this.#prepareValue(leaf, v) : lowerJoinValue(v);
|
|
3618
|
+
};
|
|
2993
3619
|
const params = [];
|
|
2994
|
-
const on =
|
|
3620
|
+
const on = parts
|
|
2995
3621
|
.map((p, i) => {
|
|
2996
3622
|
const prefix = i === 0 ? "ON" : p.kind === "or" ? "OR" : "AND";
|
|
3623
|
+
if (p.exists) {
|
|
3624
|
+
params.push(...p.exists.params);
|
|
3625
|
+
return `${prefix} ${p.exists.not ? "NOT EXISTS" : "EXISTS"} (${p.exists.sql})`;
|
|
3626
|
+
}
|
|
3627
|
+
const col = this.#quoteCol(p.left ?? "");
|
|
3628
|
+
if (p.nullOp) {
|
|
3629
|
+
return `${prefix} ${col} ${p.nullOp}`;
|
|
3630
|
+
}
|
|
3631
|
+
if (p.between) {
|
|
3632
|
+
params.push(prep(p.left ?? "", p.between[0]), prep(p.left ?? "", p.between[1]));
|
|
3633
|
+
return `${prefix} ${col} ${p.notBetween ? "NOT BETWEEN" : "BETWEEN"} ? AND ?`;
|
|
3634
|
+
}
|
|
3635
|
+
if (p.values) {
|
|
3636
|
+
const placeholders = p.values.map(() => "?").join(", ");
|
|
3637
|
+
for (const v of p.values)
|
|
3638
|
+
params.push(prep(p.left ?? "", v));
|
|
3639
|
+
return `${prefix} ${col} ${p.notIn ? "NOT IN" : "IN"} (${placeholders})`;
|
|
3640
|
+
}
|
|
2997
3641
|
if (p.value) {
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
// column must NOT borrow the root model's adapter for a same-named
|
|
3001
|
-
// column on another table — apply only universal type-lowering
|
|
3002
|
-
// (Date/DateTime→ISO), matching Knex's model-agnostic join binding.
|
|
3003
|
-
const dot = p.left.lastIndexOf(".");
|
|
3004
|
-
const tablePrefix = dot >= 0 ? p.left.slice(0, dot) : "";
|
|
3005
|
-
const leaf = dot >= 0 ? p.left.slice(dot + 1) : p.left;
|
|
3006
|
-
const isBaseColumn = tablePrefix === "" || sameTableRef(tablePrefix, this.#tableName);
|
|
3007
|
-
params.push(isBaseColumn
|
|
3008
|
-
? this.#prepareValue(leaf, p.value.v)
|
|
3009
|
-
: lowerJoinValue(p.value.v));
|
|
3010
|
-
return `${prefix} ${this.#quoteCol(p.left)} = ?`;
|
|
3642
|
+
params.push(prep(p.left ?? "", p.value.v));
|
|
3643
|
+
return `${prefix} ${col} ${this.#validateJoinOp(p.operator ?? "=")} ?`;
|
|
3011
3644
|
}
|
|
3012
|
-
return `${prefix} ${this.#
|
|
3645
|
+
return `${prefix} ${col} ${this.#validateJoinOp(p.operator ?? "=")} ${this.#quoteCol(p.right ?? "")}`;
|
|
3013
3646
|
})
|
|
3014
3647
|
.join(" ");
|
|
3015
3648
|
this.#joins.push({ sql: `${kind} JOIN ${tq} ${on}`, params });
|
|
3016
3649
|
return this;
|
|
3017
3650
|
}
|
|
3018
|
-
|
|
3651
|
+
// String form: 3-arg `(left, right)` or 4-arg `(left, operator, right)`.
|
|
3652
|
+
const left = leftOrBuild;
|
|
3653
|
+
const operator = right === undefined ? "=" : (operatorOrRight ?? "=");
|
|
3654
|
+
const rightCol = right === undefined ? operatorOrRight : right;
|
|
3655
|
+
if (rightCol === undefined)
|
|
3019
3656
|
throw new Error("join() with string form requires both left and right operands");
|
|
3020
3657
|
this.#joins.push({
|
|
3021
|
-
sql: `${kind} JOIN ${tq} ON ${this.#quoteCol(
|
|
3658
|
+
sql: `${kind} JOIN ${tq} ON ${this.#quoteCol(left)} ${this.#validateJoinOp(operator)} ${this.#quoteCol(rightCol)}`,
|
|
3022
3659
|
params: [],
|
|
3023
3660
|
});
|
|
3024
3661
|
return this;
|
|
3025
3662
|
}
|
|
3663
|
+
/** Allowlist the comparison operator embedded verbatim into a JOIN's ON SQL. */
|
|
3664
|
+
#validateJoinOp(op) {
|
|
3665
|
+
const t = op.trim();
|
|
3666
|
+
const up = t.toUpperCase();
|
|
3667
|
+
const allowed = new Set([
|
|
3668
|
+
"=",
|
|
3669
|
+
"<>",
|
|
3670
|
+
"!=",
|
|
3671
|
+
"<",
|
|
3672
|
+
"<=",
|
|
3673
|
+
">",
|
|
3674
|
+
">=",
|
|
3675
|
+
"LIKE",
|
|
3676
|
+
"NOT LIKE",
|
|
3677
|
+
"ILIKE",
|
|
3678
|
+
]);
|
|
3679
|
+
if (allowed.has(t))
|
|
3680
|
+
return t;
|
|
3681
|
+
if (allowed.has(up))
|
|
3682
|
+
return up;
|
|
3683
|
+
throw new Error(`Unsupported join operator '${op}'.`);
|
|
3684
|
+
}
|
|
3026
3685
|
async #runScalar(expr) {
|
|
3027
3686
|
const clone = this.clone();
|
|
3028
3687
|
clone.#select = [`${expr} AS __scalar__`];
|
|
3029
3688
|
clone.#orderBys = [];
|
|
3030
|
-
const { sql, params } = clone
|
|
3031
|
-
const rows = await this.#db.query(sql, params);
|
|
3689
|
+
const { sql, params } = clone.#compiledNative();
|
|
3690
|
+
const rows = await this.#raceTimeout(this.#db.query(sql, params));
|
|
3032
3691
|
const row = rows[0];
|
|
3033
3692
|
return row ? row.__scalar__ : null;
|
|
3034
3693
|
}
|
|
@@ -3041,42 +3700,21 @@ export class ModelQuery {
|
|
|
3041
3700
|
set: setPairs,
|
|
3042
3701
|
wheres: this.#wheresForDml(),
|
|
3043
3702
|
returning: [],
|
|
3703
|
+
ctes: this.#compiledCtes(),
|
|
3044
3704
|
};
|
|
3045
3705
|
const compiled = compileStatementNative(spec, this.#dialect);
|
|
3046
|
-
const r = await this.#db.execute(compiled.statements[0], compiled.params);
|
|
3706
|
+
const r = await this.#raceTimeout(this.#db.execute(this.#commentPrefix() + compiled.statements[0], compiled.params, this.#meta("dml")));
|
|
3047
3707
|
return r.rowsAffected ?? 0;
|
|
3048
3708
|
}
|
|
3049
3709
|
/**
|
|
3050
|
-
*
|
|
3051
|
-
*
|
|
3052
|
-
*
|
|
3053
|
-
*
|
|
3710
|
+
* The user's own WHERE predicates for DML (no soft-delete scope). The DML
|
|
3711
|
+
* compiler now REUSES the SELECT compiler's WHERE lowering, so every predicate
|
|
3712
|
+
* the read builder accepts — standard, `whereRaw`, `whereExists`, sub-queries,
|
|
3713
|
+
* and nested groups — is valid for update/delete too. Passed through unchanged
|
|
3714
|
+
* (same as `#buildSpec` does for reads).
|
|
3054
3715
|
*/
|
|
3055
|
-
/** The user's own WHERE predicates mapped for DML (no soft-delete scope). */
|
|
3056
3716
|
#userWheresForDml() {
|
|
3057
|
-
|
|
3058
|
-
for (const w of this.#wheres) {
|
|
3059
|
-
if ("kind" in w) {
|
|
3060
|
-
if (w.kind === "raw") {
|
|
3061
|
-
out.push({
|
|
3062
|
-
kind: "raw",
|
|
3063
|
-
sql: w.sql,
|
|
3064
|
-
bindings: w.bindings,
|
|
3065
|
-
type: w.type,
|
|
3066
|
-
});
|
|
3067
|
-
continue;
|
|
3068
|
-
}
|
|
3069
|
-
throw new Error(`update/delete do not support '${w.kind}' WHERE clauses. ` +
|
|
3070
|
-
`Supported: plain predicates and whereRaw. Use a raw UPDATE/DELETE for complex criteria.`);
|
|
3071
|
-
}
|
|
3072
|
-
out.push({
|
|
3073
|
-
column: w.column,
|
|
3074
|
-
operator: w.operator,
|
|
3075
|
-
value: w.value,
|
|
3076
|
-
type: w.type,
|
|
3077
|
-
});
|
|
3078
|
-
}
|
|
3079
|
-
return out;
|
|
3717
|
+
return [...this.#wheres];
|
|
3080
3718
|
}
|
|
3081
3719
|
#wheresForDml() {
|
|
3082
3720
|
const out = this.#userWheresForDml();
|
|
@@ -3106,13 +3744,55 @@ export class ModelQuery {
|
|
|
3106
3744
|
}
|
|
3107
3745
|
return out;
|
|
3108
3746
|
}
|
|
3747
|
+
/** Compile a DML spec with the comment prefix — for a lazy builder's `.toSQL()`. */
|
|
3748
|
+
#compileDmlSpec(spec) {
|
|
3749
|
+
const compiled = compileStatementNative(spec, this.#dialect);
|
|
3750
|
+
const sql = this.#commentPrefix() + compiled.statements[0];
|
|
3751
|
+
return { sql, bindings: compiled.params, params: compiled.params };
|
|
3752
|
+
}
|
|
3753
|
+
/** Hooks a lazy DML builder delegates back to (upsert clauses are N/A on a model query). */
|
|
3754
|
+
#dmlHooks() {
|
|
3755
|
+
return {
|
|
3756
|
+
onConflict: () => { },
|
|
3757
|
+
merge: () => { },
|
|
3758
|
+
ignore: () => { },
|
|
3759
|
+
returning: (...cols) => {
|
|
3760
|
+
for (const c of cols) {
|
|
3761
|
+
if (Array.isArray(c))
|
|
3762
|
+
this.#dmlReturning.push(...c);
|
|
3763
|
+
else
|
|
3764
|
+
this.#dmlReturning.push(c);
|
|
3765
|
+
}
|
|
3766
|
+
},
|
|
3767
|
+
timeout: (ms) => {
|
|
3768
|
+
this.#timeoutMs = ms;
|
|
3769
|
+
},
|
|
3770
|
+
comment: (t) => {
|
|
3771
|
+
this.comment(t);
|
|
3772
|
+
},
|
|
3773
|
+
debug: (enabled) => {
|
|
3774
|
+
this.debug(enabled);
|
|
3775
|
+
},
|
|
3776
|
+
reporterData: (data) => {
|
|
3777
|
+
this.reporterData(data);
|
|
3778
|
+
},
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
/** Wrap a DML spec-builder as a lazy, chainable, inspectable {@link DmlBuilder}. */
|
|
3782
|
+
#makeDml(build, paramReturning) {
|
|
3783
|
+
const resolved = () => [...(paramReturning ?? []), ...this.#dmlReturning].map((c) => this.#resolveSelect(c));
|
|
3784
|
+
return new DmlBuilder(() => {
|
|
3785
|
+
const r = resolved();
|
|
3786
|
+
return this.#runDml(build(r), r);
|
|
3787
|
+
}, () => this.#compileDmlSpec(build(resolved())), this.#dmlHooks());
|
|
3788
|
+
}
|
|
3109
3789
|
/** Compile + run a DML spec: returns affected-row count, or rows when `returning` is set. */
|
|
3110
3790
|
async #runDml(spec, returning) {
|
|
3111
3791
|
const compiled = compileStatementNative(spec, this.#dialect);
|
|
3112
3792
|
if (returning && returning.length > 0) {
|
|
3113
|
-
return this.#db.query(compiled.statements[0], compiled.params);
|
|
3793
|
+
return this.#raceTimeout(this.#db.query(this.#commentPrefix() + compiled.statements[0], compiled.params, this.#meta("dml")));
|
|
3114
3794
|
}
|
|
3115
|
-
const r = await this.#db.execute(compiled.statements[0], compiled.params);
|
|
3795
|
+
const r = await this.#raceTimeout(this.#db.execute(this.#commentPrefix() + compiled.statements[0], compiled.params, this.#meta("dml")));
|
|
3116
3796
|
return r.rowsAffected ?? 0;
|
|
3117
3797
|
}
|
|
3118
3798
|
/**
|
|
@@ -3128,18 +3808,6 @@ export class ModelQuery {
|
|
|
3128
3808
|
* parameters via the Rust compiler — this escaper is never on the hot path.
|
|
3129
3809
|
* If you are tempted to feed `.toQuery()` output into `db.prepare()`, STOP.
|
|
3130
3810
|
*/
|
|
3131
|
-
#literalEscape(v) {
|
|
3132
|
-
if (v === null || v === undefined)
|
|
3133
|
-
return "NULL";
|
|
3134
|
-
if (typeof v === "number")
|
|
3135
|
-
return String(v);
|
|
3136
|
-
if (typeof v === "boolean")
|
|
3137
|
-
return v ? "1" : "0";
|
|
3138
|
-
if (v instanceof Date)
|
|
3139
|
-
return `'${v.toISOString()}'`;
|
|
3140
|
-
// Strings — escape single quotes per SQL. NOT injection-safe against `\'`.
|
|
3141
|
-
return `'${String(v).replace(/'/g, "''")}'`;
|
|
3142
|
-
}
|
|
3143
3811
|
/**
|
|
3144
3812
|
* Build a parenthesised WHERE group from a callback. A throwaway ModelQuery
|
|
3145
3813
|
* on the SAME table is used as the scratch builder so the callback can call
|