@c9up/atlas 0.2.0 → 0.2.1
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 +48 -0
- package/dist/testing/DbAssertions.d.ts.map +1 -0
- package/dist/testing/DbAssertions.js +80 -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 +127 -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/src/ModelQuery.ts
CHANGED
|
@@ -29,11 +29,21 @@ import {
|
|
|
29
29
|
} from "./decorators/entity.js";
|
|
30
30
|
import { fireHooks } from "./decorators/hooks.js";
|
|
31
31
|
import { getNamingStrategy } from "./naming/NamingStrategy.js";
|
|
32
|
+
import { DmlBuilder, type DmlChainHooks } from "./query/DmlBuilder.js";
|
|
33
|
+
import {
|
|
34
|
+
type CompiledStatement,
|
|
35
|
+
compiledStatement,
|
|
36
|
+
interpolateQuery,
|
|
37
|
+
} from "./query/interpolate.js";
|
|
32
38
|
import {
|
|
33
39
|
type AtlasDialect,
|
|
34
40
|
compileStatementNative,
|
|
41
|
+
type DialectName,
|
|
35
42
|
getAtlasDialect,
|
|
43
|
+
normalizeDialect,
|
|
36
44
|
} from "./query/native.js";
|
|
45
|
+
import { negateOperator } from "./query/operators.js";
|
|
46
|
+
import { RawSql } from "./query/QueryBuilder.js";
|
|
37
47
|
import { camelToSnake, snakeToCamel } from "./utils/casing.js";
|
|
38
48
|
|
|
39
49
|
/**
|
|
@@ -317,7 +327,7 @@ interface RawWhere {
|
|
|
317
327
|
interface JsonWhere {
|
|
318
328
|
type: "and" | "or";
|
|
319
329
|
kind: "json";
|
|
320
|
-
jsonOp: "path" | "superset" | "subset";
|
|
330
|
+
jsonOp: "path" | "superset" | "subset" | "equals";
|
|
321
331
|
column: string;
|
|
322
332
|
negated: boolean;
|
|
323
333
|
path?: string;
|
|
@@ -333,11 +343,13 @@ interface ExistsWhere {
|
|
|
333
343
|
subquery: SelectSpec;
|
|
334
344
|
}
|
|
335
345
|
|
|
336
|
-
/** Parenthesised group of WHERE conditions — built via `where(cb)`. */
|
|
346
|
+
/** Parenthesised group of WHERE conditions — built via `where(cb)` / `whereNot(cb)`. */
|
|
337
347
|
interface GroupWhere {
|
|
338
348
|
type: "and" | "or";
|
|
339
349
|
kind: "group";
|
|
340
350
|
conditions: WhereClause[];
|
|
351
|
+
/** `whereNot(cb)` wraps the group in `NOT (…)` (honoured by the Rust compiler). */
|
|
352
|
+
negated?: boolean;
|
|
341
353
|
}
|
|
342
354
|
|
|
343
355
|
/** `col IN (SELECT ...)` / `col NOT IN (SELECT ...)` — built via `whereIn(col, subQ)`. */
|
|
@@ -433,20 +445,35 @@ interface SelectSpec {
|
|
|
433
445
|
unions: UnionSpec[];
|
|
434
446
|
/** JOIN fragments; each carries its own `?`-style bound params (e.g. `onVal`). */
|
|
435
447
|
joins: Array<{ sql: string; params: unknown[] }>;
|
|
448
|
+
/** Verbatim SELECT fragments with their own params — `select(raw)` / `select(subquery.as())`. */
|
|
449
|
+
selectRaw?: Array<{ sql: string; params: unknown[] }>;
|
|
436
450
|
/** Composite lock clause, e.g. `FOR UPDATE`, `FOR NO KEY UPDATE SKIP LOCKED`. */
|
|
437
451
|
lockMode: string | null;
|
|
438
452
|
}
|
|
439
453
|
|
|
454
|
+
/** `(col1, col2) IN ((v11, v12), …)` / NOT IN — built via `whereIn([cols], [rows])`. */
|
|
455
|
+
interface InTupleWhere {
|
|
456
|
+
type: "and" | "or";
|
|
457
|
+
kind: "inTuple";
|
|
458
|
+
columns: string[];
|
|
459
|
+
rows: unknown[][];
|
|
460
|
+
negated: boolean;
|
|
461
|
+
}
|
|
462
|
+
|
|
440
463
|
type WhereClause =
|
|
441
464
|
| StandardWhere
|
|
442
465
|
| RawWhere
|
|
443
466
|
| ExistsWhere
|
|
444
467
|
| GroupWhere
|
|
445
468
|
| InSubWhere
|
|
469
|
+
| InTupleWhere
|
|
446
470
|
| JsonWhere;
|
|
447
471
|
|
|
448
472
|
type WhereCallback = (q: ModelQuery<BaseEntity>) => void;
|
|
449
473
|
|
|
474
|
+
/** A compound-query (union/intersect/except) arg — a query OR a callback building one. */
|
|
475
|
+
type UnionArg = ModelQuery<BaseEntity> | ((q: ModelQuery<BaseEntity>) => void);
|
|
476
|
+
|
|
450
477
|
/**
|
|
451
478
|
* Process-wide strict mode flag. When enabled, `whereRaw()`, `joinRaw()`,
|
|
452
479
|
* `havingRaw()` and the repository's `raw()` throw unconditionally — forcing every
|
|
@@ -505,20 +532,48 @@ function isInternalBypass(): boolean {
|
|
|
505
532
|
* to a bound VALUE (AdonisJS/Knex parity) — the value flows through the join-params
|
|
506
533
|
* channel into the compiled parameter list.
|
|
507
534
|
*/
|
|
535
|
+
interface JoinPartMQ {
|
|
536
|
+
kind: "and" | "or";
|
|
537
|
+
left?: string;
|
|
538
|
+
operator?: string;
|
|
539
|
+
right?: string;
|
|
540
|
+
value?: { v: unknown };
|
|
541
|
+
values?: unknown[];
|
|
542
|
+
notIn?: boolean;
|
|
543
|
+
between?: [unknown, unknown];
|
|
544
|
+
notBetween?: boolean;
|
|
545
|
+
nullOp?: "IS NULL" | "IS NOT NULL";
|
|
546
|
+
exists?: { sql: string; params: unknown[]; not: boolean };
|
|
547
|
+
}
|
|
548
|
+
|
|
508
549
|
interface JoinBuilder {
|
|
509
|
-
/**
|
|
510
|
-
parts:
|
|
511
|
-
kind: "and" | "or";
|
|
512
|
-
left: string;
|
|
513
|
-
right?: string;
|
|
514
|
-
value?: { v: unknown };
|
|
515
|
-
}>;
|
|
550
|
+
/** Accumulated `ON` parts — column-to-column, column-to-value, IN, BETWEEN, NULL, EXISTS. */
|
|
551
|
+
parts: JoinPartMQ[];
|
|
516
552
|
on(left: string, right: string): JoinBuilder;
|
|
553
|
+
on(left: string, operator: string, right: string): JoinBuilder;
|
|
517
554
|
andOn(left: string, right: string): JoinBuilder;
|
|
555
|
+
andOn(left: string, operator: string, right: string): JoinBuilder;
|
|
518
556
|
orOn(left: string, right: string): JoinBuilder;
|
|
557
|
+
orOn(left: string, operator: string, right: string): JoinBuilder;
|
|
519
558
|
onVal(left: string, value: unknown): JoinBuilder;
|
|
520
559
|
andOnVal(left: string, value: unknown): JoinBuilder;
|
|
521
560
|
orOnVal(left: string, value: unknown): JoinBuilder;
|
|
561
|
+
/** `ON col IN (?, ?)` (Lucid/Knex `onIn`). */
|
|
562
|
+
onIn(left: string, values: unknown[]): JoinBuilder;
|
|
563
|
+
/** `ON col NOT IN (?, ?)` (Lucid/Knex `onNotIn`). */
|
|
564
|
+
onNotIn(left: string, values: unknown[]): JoinBuilder;
|
|
565
|
+
/** `ON col IS NULL` (Lucid/Knex `onNull`). */
|
|
566
|
+
onNull(left: string): JoinBuilder;
|
|
567
|
+
/** `ON col IS NOT NULL` (Lucid/Knex `onNotNull`). */
|
|
568
|
+
onNotNull(left: string): JoinBuilder;
|
|
569
|
+
/** `ON col BETWEEN ? AND ?` — inclusive (Lucid/Knex `onBetween`). */
|
|
570
|
+
onBetween(left: string, range: readonly [unknown, unknown]): JoinBuilder;
|
|
571
|
+
/** `ON col NOT BETWEEN ? AND ?` (Lucid/Knex `onNotBetween`). */
|
|
572
|
+
onNotBetween(left: string, range: readonly [unknown, unknown]): JoinBuilder;
|
|
573
|
+
/** `ON EXISTS (subquery)` — a builder or a callback (Lucid/Knex `onExists`). */
|
|
574
|
+
onExists(subquery: UnionArg): JoinBuilder;
|
|
575
|
+
/** `ON NOT EXISTS (subquery)` (Lucid/Knex `onNotExists`). */
|
|
576
|
+
onNotExists(subquery: UnionArg): JoinBuilder;
|
|
522
577
|
}
|
|
523
578
|
|
|
524
579
|
/** Offset-based paginator (Story 29.10). */
|
|
@@ -698,10 +753,27 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
698
753
|
#orderBys: OrderByEntry[] = [];
|
|
699
754
|
#select: string[] = ["*"];
|
|
700
755
|
#limit?: number;
|
|
756
|
+
/** Top-N-per-parent limit for a has-many preload (Lucid `groupLimit`). */
|
|
757
|
+
#groupLimit?: number;
|
|
758
|
+
/** ORDER BY for the per-parent window (Lucid `groupOrderBy`). */
|
|
759
|
+
#groupOrderBy?: { column: string; direction: "asc" | "desc" };
|
|
701
760
|
#offset?: number;
|
|
702
761
|
#preloads = new Map<string, PreloadCallback | undefined>();
|
|
762
|
+
// Per-query row transformers (Adonis Lucid `rowTransformer`). Stored via a
|
|
763
|
+
// method-signature wrapper (not a bare `(row: T) => void` property) so T stays
|
|
764
|
+
// in a bivariant position — a function-typed property would force ModelQuery<T>
|
|
765
|
+
// invariant and break the `this: ModelClass<T>` bound on every static finder.
|
|
766
|
+
#rowTransformers: Array<{ run(row: T): void }> = [];
|
|
703
767
|
/** Correlated subquery projections (withCount / withAggregate). */
|
|
704
768
|
#selectSubqueries: SubqueryProjection[] = [];
|
|
769
|
+
/** Raw / subquery SELECT fragments carrying their own params (Lucid `select(raw)`, `select(subquery.as())`). */
|
|
770
|
+
#selectRaw: Array<{ sql: string; params: unknown[] }> = [];
|
|
771
|
+
/** Caller-facing statement timeout in ms (Lucid `timeout(ms)`), applied via a race in exec. */
|
|
772
|
+
#timeoutMs?: number;
|
|
773
|
+
/** `timeout(ms, { cancel: true })` — also apply a SERVER-side statement timeout. */
|
|
774
|
+
#cancelTimeout = false;
|
|
775
|
+
/** Columns from a chainable `.returning(...)` on a lazy DML builder. */
|
|
776
|
+
#dmlReturning: string[] = [];
|
|
705
777
|
/** Alias stored by `.as()` — consumed when this query is used as a withCount/withAggregate sub-builder. */
|
|
706
778
|
#subqueryAlias?: string;
|
|
707
779
|
/** Raw JOIN fragments — Story 29.4. */
|
|
@@ -719,6 +791,10 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
719
791
|
#sideloaded: Record<string, unknown> | null = null;
|
|
720
792
|
/** Per-query debug flag — Story 29.11. */
|
|
721
793
|
#debugFlag = false;
|
|
794
|
+
/** Metadata attached to the db:query event (Adonis Lucid `reporterData`). */
|
|
795
|
+
#reporterData?: Record<string, unknown>;
|
|
796
|
+
/** SQL comments prepended to the compiled query (Lucid/Knex `comment`). */
|
|
797
|
+
#comments: string[] = [];
|
|
722
798
|
/** Distinct flag — Story 29.5. */
|
|
723
799
|
#distinct = false;
|
|
724
800
|
#distinctOn: string[] = [];
|
|
@@ -732,6 +808,7 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
732
808
|
query: ModelQuery<BaseEntity>;
|
|
733
809
|
recursive?: boolean;
|
|
734
810
|
materialized?: boolean;
|
|
811
|
+
columns?: string[];
|
|
735
812
|
}> = [];
|
|
736
813
|
/** UNION / UNION ALL branches (Lucid parity). */
|
|
737
814
|
#unions: Array<{
|
|
@@ -747,6 +824,8 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
747
824
|
/** AND/OR within the parenthesised pivot-filter group — see `#runInQuery`. */
|
|
748
825
|
type: "and" | "or";
|
|
749
826
|
}> = [];
|
|
827
|
+
/** Extra pivot columns requested at query time (Lucid `pivotColumns([...])`). */
|
|
828
|
+
#extraPivotColumns: string[] = [];
|
|
750
829
|
/**
|
|
751
830
|
* Deferred builder for a lazy m2m `related().query()` EXISTS predicate. Set by
|
|
752
831
|
* the relation proxy's scoped query; invoked at `#buildSpec()` time with the
|
|
@@ -803,6 +882,27 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
803
882
|
return this;
|
|
804
883
|
}
|
|
805
884
|
|
|
885
|
+
/**
|
|
886
|
+
* Apply the first `[guard, callback]` whose guard is truthy (Adonis Lucid
|
|
887
|
+
* `match`); a trailing bare callback is the default, run when none matched.
|
|
888
|
+
*/
|
|
889
|
+
match(
|
|
890
|
+
...blocks: Array<[unknown, (query: this) => void] | ((query: this) => void)>
|
|
891
|
+
): this {
|
|
892
|
+
for (const block of blocks) {
|
|
893
|
+
if (typeof block === "function") {
|
|
894
|
+
block(this);
|
|
895
|
+
return this;
|
|
896
|
+
}
|
|
897
|
+
const [guard, callback] = block;
|
|
898
|
+
if (guard) {
|
|
899
|
+
callback(this);
|
|
900
|
+
return this;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return this;
|
|
904
|
+
}
|
|
905
|
+
|
|
806
906
|
/**
|
|
807
907
|
* Eager-load a relation (AdonisJS-style).
|
|
808
908
|
* Relations are never loaded automatically — you must call .preload() explicitly.
|
|
@@ -816,15 +916,90 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
816
916
|
return this;
|
|
817
917
|
}
|
|
818
918
|
|
|
819
|
-
/**
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
919
|
+
/**
|
|
920
|
+
* Eager-load a relation only if it hasn't been registered yet (Adonis Lucid
|
|
921
|
+
* `preloadOnce`) — a later `preload`/`preloadOnce` for the same relation is
|
|
922
|
+
* ignored, so a shared scope can safely add a preload without clobbering one
|
|
923
|
+
* the caller already set.
|
|
924
|
+
*/
|
|
925
|
+
preloadOnce(relationName: string, callback?: PreloadCallback): this {
|
|
926
|
+
if (!this.#preloads.has(relationName)) {
|
|
927
|
+
this.#preloads.set(relationName, callback);
|
|
928
|
+
}
|
|
825
929
|
return this;
|
|
826
930
|
}
|
|
827
931
|
|
|
932
|
+
/**
|
|
933
|
+
* Register a callback run for every hydrated instance after loading (preloads
|
|
934
|
+
* included) but before the query resolves (Adonis Lucid `rowTransformer`). The
|
|
935
|
+
* callback mutates the instance in place — decorate rows with per-query
|
|
936
|
+
* computed values without a model hook/accessor. Multiple transformers run in
|
|
937
|
+
* registration order.
|
|
938
|
+
*/
|
|
939
|
+
rowTransformer(callback: (row: T) => void): this {
|
|
940
|
+
this.#rowTransformers.push({ run: callback });
|
|
941
|
+
return this;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
/**
|
|
945
|
+
* Select columns (default: `*`). The model query builder extends the database
|
|
946
|
+
* query builder's `select` surface (Lucid parity): bare names, multiple args,
|
|
947
|
+
* a comma-separated string, an array, an `{ alias: 'column' }` object map, a
|
|
948
|
+
* `db.raw(...)` fragment, and a named subquery — `select(subquery.as('x'))`.
|
|
949
|
+
* Plain columns are resolved to their DB column (honouring `@Column`).
|
|
950
|
+
*/
|
|
951
|
+
select(
|
|
952
|
+
...columns: Array<
|
|
953
|
+
| string
|
|
954
|
+
| string[]
|
|
955
|
+
| Record<string, string>
|
|
956
|
+
| RawSql
|
|
957
|
+
| ModelQuery<BaseEntity>
|
|
958
|
+
>
|
|
959
|
+
): this {
|
|
960
|
+
const plain: string[] = [];
|
|
961
|
+
for (const col of columns) {
|
|
962
|
+
if (typeof col === "string") {
|
|
963
|
+
plain.push(...col.split(",").map((c) => c.trim()));
|
|
964
|
+
} else if (Array.isArray(col)) {
|
|
965
|
+
plain.push(...col);
|
|
966
|
+
} else if (col instanceof RawSql) {
|
|
967
|
+
// Lucid `select(db.raw(sql, bindings))` — verbatim fragment + params.
|
|
968
|
+
this.#selectRaw.push({ sql: col.sql, params: [...col.params] });
|
|
969
|
+
} else if (col instanceof ModelQuery) {
|
|
970
|
+
// Lucid `select(subquery.as('alias'))` — a correlated subquery column.
|
|
971
|
+
const alias = col.#subqueryAlias;
|
|
972
|
+
if (!alias) {
|
|
973
|
+
throw new Error(
|
|
974
|
+
"select(subquery) requires the subquery to be named with .as('alias')",
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const { sql, params } = col.#compiledNative();
|
|
978
|
+
this.#selectRaw.push({
|
|
979
|
+
sql: `(${sql}) AS ${this.#quoteAliasName(alias)}`,
|
|
980
|
+
params,
|
|
981
|
+
});
|
|
982
|
+
} else {
|
|
983
|
+
for (const [alias, expr] of Object.entries(col)) {
|
|
984
|
+
plain.push(`${expr} AS ${alias}`);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
if (plain.length > 0) {
|
|
989
|
+
this.#select = plain.map((c) => this.#resolveSelect(c));
|
|
990
|
+
}
|
|
991
|
+
return this;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/** Validate + dialect-quote a bare alias identifier. */
|
|
995
|
+
#quoteAliasName(alias: string): string {
|
|
996
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(alias)) {
|
|
997
|
+
throw new Error(`Invalid alias '${alias}' — expected a bare identifier.`);
|
|
998
|
+
}
|
|
999
|
+
const q = this.#dialect === "mysql" ? "`" : '"';
|
|
1000
|
+
return `${q}${alias}${q}`;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
828
1003
|
/**
|
|
829
1004
|
* Resolve a bare model-property select/returning target to its DB column
|
|
830
1005
|
* (honouring `@Column({ columnName })`), leaving expressions / aliases /
|
|
@@ -968,37 +1143,100 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
968
1143
|
return this.whereColumn(left, operator, right);
|
|
969
1144
|
}
|
|
970
1145
|
|
|
971
|
-
/**
|
|
972
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* Negated WHERE (Lucid/Knex `whereNot`) — the same forms as {@link where}: a
|
|
1148
|
+
* `(column, [operator,] value)` comparison, an object (`whereNot({ a: 1 })` →
|
|
1149
|
+
* `a != 1`), or a callback group (`whereNot((q) => …)` → `NOT (…)`).
|
|
1150
|
+
*/
|
|
1151
|
+
whereNot(callback: WhereCallback): this;
|
|
1152
|
+
whereNot(conditions: Record<string, unknown>): this;
|
|
1153
|
+
whereNot(column: string, value: unknown): this;
|
|
1154
|
+
whereNot(column: string, operator: string, value: unknown): this;
|
|
1155
|
+
whereNot(
|
|
1156
|
+
columnOrCbOrObj: string | WhereCallback | Record<string, unknown>,
|
|
1157
|
+
operatorOrValue?: unknown,
|
|
1158
|
+
value?: unknown,
|
|
1159
|
+
): this {
|
|
1160
|
+
if (typeof columnOrCbOrObj === "function") {
|
|
1161
|
+
this.#wheres.push({
|
|
1162
|
+
...this.#buildGroup("and", columnOrCbOrObj),
|
|
1163
|
+
negated: true,
|
|
1164
|
+
});
|
|
1165
|
+
return this;
|
|
1166
|
+
}
|
|
1167
|
+
if (typeof columnOrCbOrObj === "object") {
|
|
1168
|
+
for (const [col, val] of Object.entries(columnOrCbOrObj)) {
|
|
1169
|
+
this.#wheres.push({
|
|
1170
|
+
type: "and",
|
|
1171
|
+
column: this.#resolveColumn(col),
|
|
1172
|
+
operator: "!=",
|
|
1173
|
+
value: this.#prep(col, val),
|
|
1174
|
+
});
|
|
1175
|
+
}
|
|
1176
|
+
return this;
|
|
1177
|
+
}
|
|
1178
|
+
if (value === undefined) {
|
|
1179
|
+
this.#wheres.push({
|
|
1180
|
+
type: "and",
|
|
1181
|
+
column: this.#resolveColumn(columnOrCbOrObj),
|
|
1182
|
+
operator: "!=",
|
|
1183
|
+
value: this.#prep(columnOrCbOrObj, operatorOrValue),
|
|
1184
|
+
});
|
|
1185
|
+
return this;
|
|
1186
|
+
}
|
|
1187
|
+
// (column, operator, value): NOT (col <op> value) via a negated group so any
|
|
1188
|
+
// operator negates correctly without an operator-inversion table.
|
|
1189
|
+
const col = columnOrCbOrObj;
|
|
1190
|
+
const op = operatorOrValue;
|
|
973
1191
|
this.#wheres.push({
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
operator: "!=",
|
|
977
|
-
value: this.#prep(column, value),
|
|
1192
|
+
...this.#buildGroup("and", (q) => q.where(col, op as string, value)),
|
|
1193
|
+
negated: true,
|
|
978
1194
|
});
|
|
979
1195
|
return this;
|
|
980
1196
|
}
|
|
981
1197
|
|
|
982
1198
|
/** `WHERE col IN (...)` — accepts an array of values OR a `ModelQuery` subquery source. */
|
|
1199
|
+
whereIn(columns: string[], rows: unknown[][]): this;
|
|
983
1200
|
whereIn(
|
|
984
1201
|
column: string,
|
|
985
1202
|
source: readonly unknown[] | ModelQuery<BaseEntity>,
|
|
1203
|
+
): this;
|
|
1204
|
+
whereIn(
|
|
1205
|
+
columnOrColumns: string | string[],
|
|
1206
|
+
source: readonly unknown[] | unknown[][] | ModelQuery<BaseEntity>,
|
|
986
1207
|
): this {
|
|
1208
|
+
// Tuple form (Lucid `whereIn(['id','email'], [[1,'a@b.com']])`).
|
|
1209
|
+
if (Array.isArray(columnOrColumns)) {
|
|
1210
|
+
const cols = columnOrColumns.map((c) => this.#resolveColumn(c));
|
|
1211
|
+
const rows = (Array.isArray(source) ? source : []).map((r) =>
|
|
1212
|
+
(Array.isArray(r) ? r : [r]).map((v, j) =>
|
|
1213
|
+
this.#prep(cols[j] ?? "", v),
|
|
1214
|
+
),
|
|
1215
|
+
);
|
|
1216
|
+
this.#wheres.push({
|
|
1217
|
+
type: "and",
|
|
1218
|
+
kind: "inTuple",
|
|
1219
|
+
columns: cols,
|
|
1220
|
+
rows,
|
|
1221
|
+
negated: false,
|
|
1222
|
+
});
|
|
1223
|
+
return this;
|
|
1224
|
+
}
|
|
987
1225
|
if (source instanceof ModelQuery) {
|
|
988
1226
|
this.#wheres.push({
|
|
989
1227
|
type: "and",
|
|
990
1228
|
kind: "inSub",
|
|
991
1229
|
negated: false,
|
|
992
|
-
column: this.#resolveColumn(
|
|
1230
|
+
column: this.#resolveColumn(columnOrColumns),
|
|
993
1231
|
subquery: source.#buildSpec(),
|
|
994
1232
|
});
|
|
995
1233
|
return this;
|
|
996
1234
|
}
|
|
997
1235
|
this.#wheres.push({
|
|
998
1236
|
type: "and",
|
|
999
|
-
column: this.#resolveColumn(
|
|
1237
|
+
column: this.#resolveColumn(columnOrColumns),
|
|
1000
1238
|
operator: "IN",
|
|
1001
|
-
value: this.#prep(
|
|
1239
|
+
value: this.#prep(columnOrColumns, [...source]),
|
|
1002
1240
|
});
|
|
1003
1241
|
return this;
|
|
1004
1242
|
}
|
|
@@ -1464,33 +1702,105 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
1464
1702
|
*
|
|
1465
1703
|
* query.whereJsonPath('data', '$.address.city', '=', 'Paris')
|
|
1466
1704
|
*/
|
|
1705
|
+
whereJsonPath(column: string, path: string, value: unknown): this;
|
|
1467
1706
|
whereJsonPath(
|
|
1468
1707
|
column: string,
|
|
1469
1708
|
path: string,
|
|
1470
1709
|
operator: string,
|
|
1471
1710
|
value: unknown,
|
|
1711
|
+
): this;
|
|
1712
|
+
whereJsonPath(
|
|
1713
|
+
column: string,
|
|
1714
|
+
path: string,
|
|
1715
|
+
operatorOrValue: unknown,
|
|
1716
|
+
value?: unknown,
|
|
1472
1717
|
): this {
|
|
1473
|
-
return this.#
|
|
1718
|
+
return this.#pushJsonPathArgs("and", column, path, operatorOrValue, value);
|
|
1474
1719
|
}
|
|
1475
1720
|
|
|
1476
|
-
/** Alias of {@link whereJsonPath} (Lucid parity). */
|
|
1721
|
+
/** Alias of {@link whereJsonPath} (Lucid parity). Operator defaults to `=`. */
|
|
1722
|
+
andWhereJsonPath(column: string, path: string, value: unknown): this;
|
|
1477
1723
|
andWhereJsonPath(
|
|
1478
1724
|
column: string,
|
|
1479
1725
|
path: string,
|
|
1480
1726
|
operator: string,
|
|
1481
1727
|
value: unknown,
|
|
1728
|
+
): this;
|
|
1729
|
+
andWhereJsonPath(
|
|
1730
|
+
column: string,
|
|
1731
|
+
path: string,
|
|
1732
|
+
operatorOrValue: unknown,
|
|
1733
|
+
value?: unknown,
|
|
1482
1734
|
): this {
|
|
1483
|
-
return this.#
|
|
1735
|
+
return this.#pushJsonPathArgs("and", column, path, operatorOrValue, value);
|
|
1484
1736
|
}
|
|
1485
1737
|
|
|
1486
|
-
/** `OR <col at path> <op> ?` (Lucid parity). */
|
|
1738
|
+
/** `OR <col at path> <op> ?` (Lucid parity). Operator defaults to `=`. */
|
|
1739
|
+
orWhereJsonPath(column: string, path: string, value: unknown): this;
|
|
1487
1740
|
orWhereJsonPath(
|
|
1488
1741
|
column: string,
|
|
1489
1742
|
path: string,
|
|
1490
1743
|
operator: string,
|
|
1491
1744
|
value: unknown,
|
|
1745
|
+
): this;
|
|
1746
|
+
orWhereJsonPath(
|
|
1747
|
+
column: string,
|
|
1748
|
+
path: string,
|
|
1749
|
+
operatorOrValue: unknown,
|
|
1750
|
+
value?: unknown,
|
|
1751
|
+
): this {
|
|
1752
|
+
return this.#pushJsonPathArgs("or", column, path, operatorOrValue, value);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
/** Resolve the optional-operator JSONPath form, then push (default op `=`). */
|
|
1756
|
+
#pushJsonPathArgs(
|
|
1757
|
+
type: "and" | "or",
|
|
1758
|
+
column: string,
|
|
1759
|
+
path: string,
|
|
1760
|
+
operatorOrValue: unknown,
|
|
1761
|
+
value?: unknown,
|
|
1492
1762
|
): this {
|
|
1493
|
-
|
|
1763
|
+
const [operator, val] =
|
|
1764
|
+
value === undefined
|
|
1765
|
+
? ["=", operatorOrValue]
|
|
1766
|
+
: [String(operatorOrValue), value];
|
|
1767
|
+
return this.#pushJson(
|
|
1768
|
+
type,
|
|
1769
|
+
false,
|
|
1770
|
+
"path",
|
|
1771
|
+
column,
|
|
1772
|
+
val,
|
|
1773
|
+
path,
|
|
1774
|
+
String(operator),
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
/**
|
|
1779
|
+
* Structural JSON match (Lucid `whereJson`) — the column's JSON must equal
|
|
1780
|
+
* `value` (canonical comparison on Postgres/MySQL). AND is the default.
|
|
1781
|
+
*/
|
|
1782
|
+
whereJson(column: string, value: unknown): this {
|
|
1783
|
+
return this.#pushJson("and", false, "equals", column, value);
|
|
1784
|
+
}
|
|
1785
|
+
/** Alias of {@link whereJson} (Lucid `andWhereJson`). */
|
|
1786
|
+
andWhereJson(column: string, value: unknown): this {
|
|
1787
|
+
return this.#pushJson("and", false, "equals", column, value);
|
|
1788
|
+
}
|
|
1789
|
+
/** OR form of {@link whereJson} (Lucid `orWhereJson`). */
|
|
1790
|
+
orWhereJson(column: string, value: unknown): this {
|
|
1791
|
+
return this.#pushJson("or", false, "equals", column, value);
|
|
1792
|
+
}
|
|
1793
|
+
/** WHERE NOT structural JSON match (Lucid `whereNotJson`). */
|
|
1794
|
+
whereNotJson(column: string, value: unknown): this {
|
|
1795
|
+
return this.#pushJson("and", true, "equals", column, value);
|
|
1796
|
+
}
|
|
1797
|
+
/** Alias of {@link whereNotJson} (Lucid `andWhereNotJson`). */
|
|
1798
|
+
andWhereNotJson(column: string, value: unknown): this {
|
|
1799
|
+
return this.#pushJson("and", true, "equals", column, value);
|
|
1800
|
+
}
|
|
1801
|
+
/** OR NOT structural JSON match (Lucid `orWhereNotJson`). */
|
|
1802
|
+
orWhereNotJson(column: string, value: unknown): this {
|
|
1803
|
+
return this.#pushJson("or", true, "equals", column, value);
|
|
1494
1804
|
}
|
|
1495
1805
|
|
|
1496
1806
|
/**
|
|
@@ -1552,10 +1862,52 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
1552
1862
|
return this.#pushJson("or", true, "subset", column, value);
|
|
1553
1863
|
}
|
|
1554
1864
|
|
|
1865
|
+
// Lucid's canonical containment names drop the `Of` suffix — kept as aliases.
|
|
1866
|
+
/** Lucid `whereJsonSuperset` (alias of {@link whereJsonSupersetOf}). */
|
|
1867
|
+
whereJsonSuperset(column: string, value: unknown): this {
|
|
1868
|
+
return this.#pushJson("and", false, "superset", column, value);
|
|
1869
|
+
}
|
|
1870
|
+
/** Lucid `andWhereJsonSuperset`. */
|
|
1871
|
+
andWhereJsonSuperset(column: string, value: unknown): this {
|
|
1872
|
+
return this.#pushJson("and", false, "superset", column, value);
|
|
1873
|
+
}
|
|
1874
|
+
/** Lucid `orWhereJsonSuperset`. */
|
|
1875
|
+
orWhereJsonSuperset(column: string, value: unknown): this {
|
|
1876
|
+
return this.#pushJson("or", false, "superset", column, value);
|
|
1877
|
+
}
|
|
1878
|
+
/** Lucid `whereNotJsonSuperset`. */
|
|
1879
|
+
whereNotJsonSuperset(column: string, value: unknown): this {
|
|
1880
|
+
return this.#pushJson("and", true, "superset", column, value);
|
|
1881
|
+
}
|
|
1882
|
+
/** Lucid `orWhereNotJsonSuperset`. */
|
|
1883
|
+
orWhereNotJsonSuperset(column: string, value: unknown): this {
|
|
1884
|
+
return this.#pushJson("or", true, "superset", column, value);
|
|
1885
|
+
}
|
|
1886
|
+
/** Lucid `whereJsonSubset` (alias of {@link whereJsonSubsetOf}). */
|
|
1887
|
+
whereJsonSubset(column: string, value: unknown): this {
|
|
1888
|
+
return this.#pushJson("and", false, "subset", column, value);
|
|
1889
|
+
}
|
|
1890
|
+
/** Lucid `andWhereJsonSubset`. */
|
|
1891
|
+
andWhereJsonSubset(column: string, value: unknown): this {
|
|
1892
|
+
return this.#pushJson("and", false, "subset", column, value);
|
|
1893
|
+
}
|
|
1894
|
+
/** Lucid `orWhereJsonSubset`. */
|
|
1895
|
+
orWhereJsonSubset(column: string, value: unknown): this {
|
|
1896
|
+
return this.#pushJson("or", false, "subset", column, value);
|
|
1897
|
+
}
|
|
1898
|
+
/** Lucid `whereNotJsonSubset`. */
|
|
1899
|
+
whereNotJsonSubset(column: string, value: unknown): this {
|
|
1900
|
+
return this.#pushJson("and", true, "subset", column, value);
|
|
1901
|
+
}
|
|
1902
|
+
/** Lucid `orWhereNotJsonSubset`. */
|
|
1903
|
+
orWhereNotJsonSubset(column: string, value: unknown): this {
|
|
1904
|
+
return this.#pushJson("or", true, "subset", column, value);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1555
1907
|
#pushJson(
|
|
1556
1908
|
type: "and" | "or",
|
|
1557
1909
|
negated: boolean,
|
|
1558
|
-
jsonOp: "path" | "superset" | "subset",
|
|
1910
|
+
jsonOp: "path" | "superset" | "subset" | "equals",
|
|
1559
1911
|
column: string,
|
|
1560
1912
|
value: unknown,
|
|
1561
1913
|
path?: string,
|
|
@@ -1953,8 +2305,41 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
1953
2305
|
return this;
|
|
1954
2306
|
}
|
|
1955
2307
|
|
|
1956
|
-
|
|
1957
|
-
|
|
2308
|
+
/** ORDER BY a column, a `db.ref()` fragment, or an array of terms (Lucid/Knex `orderBy`). */
|
|
2309
|
+
orderBy(column: string | RawSql, direction?: "asc" | "desc"): this;
|
|
2310
|
+
orderBy(
|
|
2311
|
+
terms: Array<string | { column: string; order?: "asc" | "desc" }>,
|
|
2312
|
+
): this;
|
|
2313
|
+
orderBy(
|
|
2314
|
+
columnOrTerms:
|
|
2315
|
+
| string
|
|
2316
|
+
| RawSql
|
|
2317
|
+
| Array<string | { column: string; order?: "asc" | "desc" }>,
|
|
2318
|
+
direction: "asc" | "desc" = "asc",
|
|
2319
|
+
): this {
|
|
2320
|
+
// A raw fragment (e.g. `db.ref('posts.created_at')`) orders verbatim.
|
|
2321
|
+
if (columnOrTerms instanceof RawSql) {
|
|
2322
|
+
const dir = direction === "desc" ? "DESC" : "ASC";
|
|
2323
|
+
this.#orderBys.push({ raw: `${columnOrTerms.sql} ${dir}` });
|
|
2324
|
+
return this;
|
|
2325
|
+
}
|
|
2326
|
+
if (Array.isArray(columnOrTerms)) {
|
|
2327
|
+
for (const t of columnOrTerms) {
|
|
2328
|
+
const [col, dir] =
|
|
2329
|
+
typeof t === "string"
|
|
2330
|
+
? [t, "asc" as const]
|
|
2331
|
+
: [t.column, t.order ?? "asc"];
|
|
2332
|
+
this.#orderBys.push({
|
|
2333
|
+
column: this.#resolveColumn(col),
|
|
2334
|
+
direction: dir,
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
return this;
|
|
2338
|
+
}
|
|
2339
|
+
this.#orderBys.push({
|
|
2340
|
+
column: this.#resolveColumn(columnOrTerms),
|
|
2341
|
+
direction,
|
|
2342
|
+
});
|
|
1958
2343
|
return this;
|
|
1959
2344
|
}
|
|
1960
2345
|
|
|
@@ -2022,6 +2407,47 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2022
2407
|
return this;
|
|
2023
2408
|
}
|
|
2024
2409
|
|
|
2410
|
+
/** HAVING col IS NULL (Lucid/Knex `havingNull`). */
|
|
2411
|
+
havingNull(column: string): this {
|
|
2412
|
+
return this.#pushHaving(column, "IS NULL", null);
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
/** HAVING col IS NOT NULL (Lucid/Knex `havingNotNull`). */
|
|
2416
|
+
havingNotNull(column: string): this {
|
|
2417
|
+
return this.#pushHaving(column, "IS NOT NULL", null);
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
/** HAVING col IN (...) (Lucid/Knex `havingIn`). */
|
|
2421
|
+
havingIn(column: string, values: unknown[]): this {
|
|
2422
|
+
return this.#pushHaving(column, "IN", [...values]);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
/** HAVING col NOT IN (...) (Lucid/Knex `havingNotIn`). */
|
|
2426
|
+
havingNotIn(column: string, values: unknown[]): this {
|
|
2427
|
+
return this.#pushHaving(column, "NOT IN", [...values]);
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
/** HAVING col BETWEEN ? AND ? (Lucid/Knex `havingBetween`). */
|
|
2431
|
+
havingBetween(column: string, range: readonly [unknown, unknown]): this {
|
|
2432
|
+
return this.#pushHaving(column, "BETWEEN", [...range]);
|
|
2433
|
+
}
|
|
2434
|
+
|
|
2435
|
+
/** HAVING col NOT BETWEEN ? AND ? (Lucid/Knex `havingNotBetween`). */
|
|
2436
|
+
havingNotBetween(column: string, range: readonly [unknown, unknown]): this {
|
|
2437
|
+
return this.#pushHaving(column, "NOT BETWEEN", [...range]);
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
/** Push a HAVING entry with a raw (non-`#prep`'d) value — for IN/BETWEEN/NULL. */
|
|
2441
|
+
#pushHaving(column: string, operator: string, value: unknown): this {
|
|
2442
|
+
this.#having.push({
|
|
2443
|
+
column: this.#resolveHavingCol(column),
|
|
2444
|
+
operator,
|
|
2445
|
+
value,
|
|
2446
|
+
type: "and",
|
|
2447
|
+
});
|
|
2448
|
+
return this;
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2025
2451
|
/**
|
|
2026
2452
|
* Resolve a HAVING column: a bare model property maps to its DB column
|
|
2027
2453
|
* (honouring `@Column({ columnName })`), but an aggregate expression
|
|
@@ -2079,20 +2505,24 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2079
2505
|
* appended as a parenthesised UNION branch; its bindings are re-indexed into
|
|
2080
2506
|
* the outer parameter list.
|
|
2081
2507
|
*/
|
|
2082
|
-
union(query:
|
|
2083
|
-
this.#unions.push({ query, all: false });
|
|
2508
|
+
union(query: UnionArg): this {
|
|
2509
|
+
this.#unions.push({ query: this.#resolveUnion(query), all: false });
|
|
2084
2510
|
return this;
|
|
2085
2511
|
}
|
|
2086
2512
|
|
|
2087
2513
|
/** `UNION ALL (<query>)` — duplicate-preserving {@link union}. */
|
|
2088
|
-
unionAll(query:
|
|
2089
|
-
this.#unions.push({ query, all: true });
|
|
2514
|
+
unionAll(query: UnionArg): this {
|
|
2515
|
+
this.#unions.push({ query: this.#resolveUnion(query), all: true });
|
|
2090
2516
|
return this;
|
|
2091
2517
|
}
|
|
2092
2518
|
|
|
2093
2519
|
/** `INTERSECT (<query>)` — rows present in both (Lucid/Knex `intersect`). */
|
|
2094
|
-
intersect(query:
|
|
2095
|
-
this.#unions.push({
|
|
2520
|
+
intersect(query: UnionArg): this {
|
|
2521
|
+
this.#unions.push({
|
|
2522
|
+
query: this.#resolveUnion(query),
|
|
2523
|
+
all: false,
|
|
2524
|
+
op: "intersect",
|
|
2525
|
+
});
|
|
2096
2526
|
return this;
|
|
2097
2527
|
}
|
|
2098
2528
|
|
|
@@ -2103,29 +2533,64 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2103
2533
|
* INTERSECT and EXCEPT — there is no INTERSECT ALL — so the compiler raises
|
|
2104
2534
|
* `E_UNSUPPORTED` there rather than emitting a syntax error.
|
|
2105
2535
|
*/
|
|
2106
|
-
intersectAll(query:
|
|
2107
|
-
this.#unions.push({
|
|
2536
|
+
intersectAll(query: UnionArg): this {
|
|
2537
|
+
this.#unions.push({
|
|
2538
|
+
query: this.#resolveUnion(query),
|
|
2539
|
+
all: true,
|
|
2540
|
+
op: "intersect",
|
|
2541
|
+
});
|
|
2108
2542
|
return this;
|
|
2109
2543
|
}
|
|
2110
2544
|
|
|
2111
2545
|
/** `EXCEPT (<query>)` — rows in this query but not the other (Lucid/Knex `except`). */
|
|
2112
|
-
except(query:
|
|
2113
|
-
this.#unions.push({
|
|
2546
|
+
except(query: UnionArg): this {
|
|
2547
|
+
this.#unions.push({
|
|
2548
|
+
query: this.#resolveUnion(query),
|
|
2549
|
+
all: false,
|
|
2550
|
+
op: "except",
|
|
2551
|
+
});
|
|
2114
2552
|
return this;
|
|
2115
2553
|
}
|
|
2116
2554
|
|
|
2117
2555
|
/** `EXCEPT ALL (<query>)` — duplicate-preserving {@link except}. Not on SQLite; see {@link intersectAll}. */
|
|
2118
|
-
exceptAll(query:
|
|
2119
|
-
this.#unions.push({
|
|
2556
|
+
exceptAll(query: UnionArg): this {
|
|
2557
|
+
this.#unions.push({
|
|
2558
|
+
query: this.#resolveUnion(query),
|
|
2559
|
+
all: true,
|
|
2560
|
+
op: "except",
|
|
2561
|
+
});
|
|
2120
2562
|
return this;
|
|
2121
2563
|
}
|
|
2122
2564
|
|
|
2565
|
+
/** A compound-query arg — an explicit ModelQuery OR a callback building one on this model. */
|
|
2566
|
+
#resolveUnion(query: UnionArg): ModelQuery<BaseEntity> {
|
|
2567
|
+
if (typeof query !== "function") return query;
|
|
2568
|
+
const sub = this.#freshQuery();
|
|
2569
|
+
query(sub);
|
|
2570
|
+
return sub;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
/** A fresh, empty query on the SAME model + connection (for compound callbacks). */
|
|
2574
|
+
#freshQuery(): ModelQuery<BaseEntity> {
|
|
2575
|
+
return new ModelQuery<BaseEntity>(
|
|
2576
|
+
this.#tableName,
|
|
2577
|
+
this.#db,
|
|
2578
|
+
this.#hydrateFn,
|
|
2579
|
+
this.#entityClass,
|
|
2580
|
+
this.#resolveColumn,
|
|
2581
|
+
this.#softDeletes,
|
|
2582
|
+
this.#dialect,
|
|
2583
|
+
this.#prepareValue,
|
|
2584
|
+
this.#onDomainEvents,
|
|
2585
|
+
);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2123
2588
|
/**
|
|
2124
2589
|
* `WITH <name> AS (<query>)` — register a Common Table Expression
|
|
2125
2590
|
* (AdonisJS/Lucid `with`). The CTE name is validated as an identifier; the
|
|
2126
2591
|
* sub-query is compiled and its bindings are re-indexed into the outer list.
|
|
2127
2592
|
*/
|
|
2128
|
-
with(name: string, query:
|
|
2593
|
+
with(name: string, query: UnionArg): this {
|
|
2129
2594
|
return this.#pushCte("with", name, query, {});
|
|
2130
2595
|
}
|
|
2131
2596
|
|
|
@@ -2140,8 +2605,11 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2140
2605
|
* The recursive term itself is a `UNION`/`UNION ALL` inside `query`, e.g.
|
|
2141
2606
|
* an anchor `SELECT` unioned with a select that references `<name>`.
|
|
2142
2607
|
*/
|
|
2143
|
-
withRecursive(name: string, query:
|
|
2144
|
-
return this.#pushCte("withRecursive", name, query, {
|
|
2608
|
+
withRecursive(name: string, query: UnionArg, columns?: string[]): this {
|
|
2609
|
+
return this.#pushCte("withRecursive", name, query, {
|
|
2610
|
+
recursive: true,
|
|
2611
|
+
columns,
|
|
2612
|
+
});
|
|
2145
2613
|
}
|
|
2146
2614
|
|
|
2147
2615
|
/**
|
|
@@ -2151,14 +2619,14 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2151
2619
|
* Postgres 12+ and SQLite 3.35+ only; MySQL has no such hint and the
|
|
2152
2620
|
* compiler raises `E_UNSUPPORTED` rather than emitting a syntax error.
|
|
2153
2621
|
*/
|
|
2154
|
-
withMaterialized(name: string, query:
|
|
2622
|
+
withMaterialized(name: string, query: UnionArg): this {
|
|
2155
2623
|
return this.#pushCte("withMaterialized", name, query, {
|
|
2156
2624
|
materialized: true,
|
|
2157
2625
|
});
|
|
2158
2626
|
}
|
|
2159
2627
|
|
|
2160
2628
|
/** `WITH <name> AS NOT MATERIALIZED (<query>)` — let it be inlined (Lucid/Knex `withNotMaterialized`). See {@link withMaterialized}. */
|
|
2161
|
-
withNotMaterialized(name: string, query:
|
|
2629
|
+
withNotMaterialized(name: string, query: UnionArg): this {
|
|
2162
2630
|
return this.#pushCte("withNotMaterialized", name, query, {
|
|
2163
2631
|
materialized: false,
|
|
2164
2632
|
});
|
|
@@ -2167,15 +2635,31 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2167
2635
|
#pushCte(
|
|
2168
2636
|
method: string,
|
|
2169
2637
|
name: string,
|
|
2170
|
-
query:
|
|
2171
|
-
options: {
|
|
2638
|
+
query: UnionArg,
|
|
2639
|
+
options: {
|
|
2640
|
+
recursive?: boolean;
|
|
2641
|
+
materialized?: boolean;
|
|
2642
|
+
columns?: string[];
|
|
2643
|
+
},
|
|
2172
2644
|
): this {
|
|
2173
2645
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
2174
2646
|
throw new Error(
|
|
2175
2647
|
`${method}(): CTE name '${name}' is not a valid identifier`,
|
|
2176
2648
|
);
|
|
2177
2649
|
}
|
|
2178
|
-
this.#ctes.push({ name, query, ...options });
|
|
2650
|
+
this.#ctes.push({ name, query: this.#resolveUnion(query), ...options });
|
|
2651
|
+
return this;
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
/**
|
|
2655
|
+
* `@ManyToMany` only — project EXTRA pivot columns at query time (AdonisJS/Lucid
|
|
2656
|
+
* `pivotColumns([...])`), on top of any declared in the relation decorator. Read
|
|
2657
|
+
* off each loaded relation as `$extras.pivot_<col>`. Inert on non-m2m relations.
|
|
2658
|
+
*
|
|
2659
|
+
* userRepo.query().preload('skills', q => q.pivotColumns(['notes']))
|
|
2660
|
+
*/
|
|
2661
|
+
pivotColumns(columns: string[]): this {
|
|
2662
|
+
this.#extraPivotColumns.push(...columns);
|
|
2179
2663
|
return this;
|
|
2180
2664
|
}
|
|
2181
2665
|
|
|
@@ -2241,19 +2725,63 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2241
2725
|
return this.whereInPivot(column, values);
|
|
2242
2726
|
}
|
|
2243
2727
|
|
|
2244
|
-
/**
|
|
2245
|
-
|
|
2246
|
-
|
|
2728
|
+
/**
|
|
2729
|
+
* `@ManyToMany` only — negated pivot filter (AdonisJS Lucid `whereNotPivot`).
|
|
2730
|
+
* `whereNotPivot(col, value)` → `col != value`; `whereNotPivot(col, op, value)`
|
|
2731
|
+
* → the negated comparison (`whereNotPivot('proficiency', '>=', 4)` → `< 4`).
|
|
2732
|
+
*/
|
|
2733
|
+
whereNotPivot(column: string, value: unknown): this;
|
|
2734
|
+
whereNotPivot(column: string, operator: string, value: unknown): this;
|
|
2735
|
+
whereNotPivot(
|
|
2736
|
+
column: string,
|
|
2737
|
+
operatorOrValue: unknown,
|
|
2738
|
+
value?: unknown,
|
|
2739
|
+
): this {
|
|
2740
|
+
if (value === undefined) {
|
|
2741
|
+
return this.#pushPivotOp("and", column, "!=", operatorOrValue);
|
|
2742
|
+
}
|
|
2743
|
+
return this.#pushPivotOp(
|
|
2744
|
+
"and",
|
|
2745
|
+
column,
|
|
2746
|
+
negateOperator(String(operatorOrValue)),
|
|
2747
|
+
value,
|
|
2748
|
+
);
|
|
2247
2749
|
}
|
|
2248
2750
|
|
|
2249
|
-
/** Alias of {@link whereNotPivot} (Lucid parity). */
|
|
2250
|
-
andWhereNotPivot(column: string, value: unknown): this
|
|
2251
|
-
|
|
2751
|
+
/** Alias of {@link whereNotPivot} (Lucid parity). Operator form supported too. */
|
|
2752
|
+
andWhereNotPivot(column: string, value: unknown): this;
|
|
2753
|
+
andWhereNotPivot(column: string, operator: string, value: unknown): this;
|
|
2754
|
+
andWhereNotPivot(
|
|
2755
|
+
column: string,
|
|
2756
|
+
operatorOrValue: unknown,
|
|
2757
|
+
value?: unknown,
|
|
2758
|
+
): this {
|
|
2759
|
+
return value === undefined
|
|
2760
|
+
? this.#pushPivotOp("and", column, "!=", operatorOrValue)
|
|
2761
|
+
: this.#pushPivotOp(
|
|
2762
|
+
"and",
|
|
2763
|
+
column,
|
|
2764
|
+
negateOperator(String(operatorOrValue)),
|
|
2765
|
+
value,
|
|
2766
|
+
);
|
|
2252
2767
|
}
|
|
2253
2768
|
|
|
2254
2769
|
/** `@ManyToMany` only — OR form of {@link whereNotPivot} (Lucid parity). */
|
|
2255
|
-
orWhereNotPivot(column: string, value: unknown): this
|
|
2256
|
-
|
|
2770
|
+
orWhereNotPivot(column: string, value: unknown): this;
|
|
2771
|
+
orWhereNotPivot(column: string, operator: string, value: unknown): this;
|
|
2772
|
+
orWhereNotPivot(
|
|
2773
|
+
column: string,
|
|
2774
|
+
operatorOrValue: unknown,
|
|
2775
|
+
value?: unknown,
|
|
2776
|
+
): this {
|
|
2777
|
+
return value === undefined
|
|
2778
|
+
? this.#pushPivotOp("or", column, "!=", operatorOrValue)
|
|
2779
|
+
: this.#pushPivotOp(
|
|
2780
|
+
"or",
|
|
2781
|
+
column,
|
|
2782
|
+
negateOperator(String(operatorOrValue)),
|
|
2783
|
+
value,
|
|
2784
|
+
);
|
|
2257
2785
|
}
|
|
2258
2786
|
|
|
2259
2787
|
/** `@ManyToMany` only — `WHERE <pivotCol> NOT IN (...)` on the pivot table (AdonisJS Lucid `whereNotInPivot`). */
|
|
@@ -2333,6 +2861,11 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2333
2861
|
return this.#pivotWheres;
|
|
2334
2862
|
}
|
|
2335
2863
|
|
|
2864
|
+
/** Read-only accessor for query-time pivot columns — consumed by the m2m preload resolver. */
|
|
2865
|
+
get pivotColumnRequests(): readonly string[] {
|
|
2866
|
+
return this.#extraPivotColumns;
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2336
2869
|
limit(n: number): this {
|
|
2337
2870
|
// Guard here with a clear message — the Rust spec types limit as
|
|
2338
2871
|
// u64, so a negative/non-integer otherwise surfaces as a cryptic
|
|
@@ -2345,6 +2878,26 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2345
2878
|
return this;
|
|
2346
2879
|
}
|
|
2347
2880
|
|
|
2881
|
+
/**
|
|
2882
|
+
* In a has-many preload callback, cap the related rows PER PARENT (Lucid
|
|
2883
|
+
* `groupLimit`) — a plain `.limit()` caps the whole result set across parents.
|
|
2884
|
+
* Compiled with a `ROW_NUMBER() OVER (PARTITION BY <fk> …)` window. Pair with
|
|
2885
|
+
* {@link groupOrderBy} to pick which N per parent.
|
|
2886
|
+
*/
|
|
2887
|
+
groupLimit(n: number): this {
|
|
2888
|
+
if (!Number.isInteger(n) || n < 0) {
|
|
2889
|
+
throw new Error(`groupLimit must be a non-negative integer, got ${n}`);
|
|
2890
|
+
}
|
|
2891
|
+
this.#groupLimit = n;
|
|
2892
|
+
return this;
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
/** ORDER BY for the {@link groupLimit} per-parent window (Lucid `groupOrderBy`). */
|
|
2896
|
+
groupOrderBy(column: string, direction: "asc" | "desc" = "asc"): this {
|
|
2897
|
+
this.#groupOrderBy = { column, direction };
|
|
2898
|
+
return this;
|
|
2899
|
+
}
|
|
2900
|
+
|
|
2348
2901
|
offset(n: number): this {
|
|
2349
2902
|
if (!Number.isInteger(n) || n < 0) {
|
|
2350
2903
|
throw new Error(`offset must be a non-negative integer, got ${n}`);
|
|
@@ -2520,6 +3073,7 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2520
3073
|
table: this.#tableName,
|
|
2521
3074
|
select: selectCols,
|
|
2522
3075
|
selectSubqueries: this.#selectSubqueries,
|
|
3076
|
+
selectRaw: this.#selectRaw,
|
|
2523
3077
|
wheres,
|
|
2524
3078
|
orderBy: this.#orderBys,
|
|
2525
3079
|
groupBy: this.#groupBy,
|
|
@@ -2528,18 +3082,9 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2528
3082
|
offset: this.#offset ?? null,
|
|
2529
3083
|
distinct: this.#distinct,
|
|
2530
3084
|
distinctOn: this.#distinctOn,
|
|
2531
|
-
ctes: this.#
|
|
2532
|
-
const { sql, params } = c.query.toSQL();
|
|
2533
|
-
return {
|
|
2534
|
-
name: c.name,
|
|
2535
|
-
sql,
|
|
2536
|
-
params,
|
|
2537
|
-
recursive: c.recursive ?? false,
|
|
2538
|
-
materialized: c.materialized ?? null,
|
|
2539
|
-
};
|
|
2540
|
-
}),
|
|
3085
|
+
ctes: this.#compiledCtes(),
|
|
2541
3086
|
unions: this.#unions.map((u) => {
|
|
2542
|
-
const { sql, params } = u.query
|
|
3087
|
+
const { sql, params } = u.query.#compiledNative();
|
|
2543
3088
|
return { sql, params, all: u.all, op: u.op ?? null };
|
|
2544
3089
|
}),
|
|
2545
3090
|
joins: this.#joins,
|
|
@@ -2551,10 +3096,133 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2551
3096
|
};
|
|
2552
3097
|
}
|
|
2553
3098
|
|
|
2554
|
-
/**
|
|
2555
|
-
|
|
3099
|
+
/**
|
|
3100
|
+
* Native compiled `{ sql, params }` (Postgres `$N`) — the form atlas executes
|
|
3101
|
+
* and embeds inside parent queries. Public `toSQL()` normalizes to Knex `?`.
|
|
3102
|
+
*/
|
|
3103
|
+
#compiledNative(): { sql: string; params: unknown[] } {
|
|
2556
3104
|
const compiled = compileStatementNative(this.#buildSpec(), this.#dialect);
|
|
2557
|
-
|
|
3105
|
+
const sql = this.#commentPrefix() + compiled.statements[0];
|
|
3106
|
+
return { sql, params: compiled.params };
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
/**
|
|
3110
|
+
* Build SQL via the Rust query compiler (Lucid `toSQL`). `.sql` uses `?`
|
|
3111
|
+
* placeholders (Knex-normalized, like Lucid); `.toNative()` yields the native
|
|
3112
|
+
* form. Returns `bindings` (Lucid's name) and `params` (atlas's) — same array.
|
|
3113
|
+
*/
|
|
3114
|
+
toSQL(): CompiledStatement {
|
|
3115
|
+
const { sql, params } = this.#compiledNative();
|
|
3116
|
+
return compiledStatement(sql, params);
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/** `{ sql, bindings }` — the compiled native query (Lucid/Knex `toNative`). */
|
|
3120
|
+
toNative(): { sql: string; bindings: unknown[] } {
|
|
3121
|
+
const { sql, params } = this.#compiledNative();
|
|
3122
|
+
return { sql, bindings: params };
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
/** The model class this query targets (Adonis Lucid `query.model`). */
|
|
3126
|
+
get model(): new () => T {
|
|
3127
|
+
return this.#entityClass;
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
/** Apply `cb` only on the given dialect(s) (Lucid `ifDialect`; Lucid names accepted). */
|
|
3131
|
+
ifDialect(
|
|
3132
|
+
dialect: DialectName | DialectName[],
|
|
3133
|
+
cb: (query: this) => void,
|
|
3134
|
+
): this {
|
|
3135
|
+
const set = (Array.isArray(dialect) ? dialect : [dialect]).map(
|
|
3136
|
+
normalizeDialect,
|
|
3137
|
+
);
|
|
3138
|
+
if (set.includes(this.#dialect)) cb(this);
|
|
3139
|
+
return this;
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
/** Apply `cb` on every dialect EXCEPT the given one(s) (Lucid `unlessDialect`). */
|
|
3143
|
+
unlessDialect(
|
|
3144
|
+
dialect: DialectName | DialectName[],
|
|
3145
|
+
cb: (query: this) => void,
|
|
3146
|
+
): this {
|
|
3147
|
+
const set = (Array.isArray(dialect) ? dialect : [dialect]).map(
|
|
3148
|
+
normalizeDialect,
|
|
3149
|
+
);
|
|
3150
|
+
if (!set.includes(this.#dialect)) cb(this);
|
|
3151
|
+
return this;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
/** Prepend a `/* … */` SQL comment to the compiled query (Lucid/Knex `comment`). */
|
|
3155
|
+
comment(text: string): this {
|
|
3156
|
+
if (text.includes("*/")) {
|
|
3157
|
+
throw new Error("comment() text may not contain '*/'");
|
|
3158
|
+
}
|
|
3159
|
+
this.#comments.push(text);
|
|
3160
|
+
return this;
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
/**
|
|
3164
|
+
* Set a caller-facing statement timeout in milliseconds (Lucid `timeout(ms)`).
|
|
3165
|
+
* The awaiting promise rejects after `ms` on the primary result fetch. With
|
|
3166
|
+
* `{ cancel: true }` a SERVER-side statement timeout is also applied — Postgres
|
|
3167
|
+
* `statement_timeout`, MySQL `MAX_EXECUTION_TIME` (SELECT) — so the server
|
|
3168
|
+
* aborts the query, not just the client. (SQLite has no server timeout; the
|
|
3169
|
+
* client race applies.) Called with no argument it clears the timeout.
|
|
3170
|
+
*/
|
|
3171
|
+
timeout(ms?: number, options?: { cancel?: boolean }): this {
|
|
3172
|
+
// `{ cancel: true }` also applies a SERVER-side statement timeout (Postgres
|
|
3173
|
+
// statement_timeout / MySQL MAX_EXECUTION_TIME for SELECT); SQLite has none,
|
|
3174
|
+
// so the client race applies there.
|
|
3175
|
+
this.#timeoutMs = ms;
|
|
3176
|
+
this.#cancelTimeout = options?.cancel === true;
|
|
3177
|
+
return this;
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
/**
|
|
3181
|
+
* Race `work` against the configured `.timeout(ms)`. Rejects the awaiter after
|
|
3182
|
+
* `ms`; the losing DB promise is swallowed so a post-timeout driver error never
|
|
3183
|
+
* surfaces as an unhandled rejection. No timeout set → returns `work` as-is.
|
|
3184
|
+
*/
|
|
3185
|
+
#raceTimeout<R>(work: Promise<R>): Promise<R> {
|
|
3186
|
+
const ms = this.#timeoutMs;
|
|
3187
|
+
if (!ms || ms <= 0) return work;
|
|
3188
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
3189
|
+
const guard = new Promise<never>((_, reject) => {
|
|
3190
|
+
timer = setTimeout(
|
|
3191
|
+
() => reject(new Error(`Query timed out after ${ms}ms`)),
|
|
3192
|
+
ms,
|
|
3193
|
+
);
|
|
3194
|
+
});
|
|
3195
|
+
work.catch(() => {});
|
|
3196
|
+
return Promise.race([work, guard]).finally(() => clearTimeout(timer));
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
/** The `/* … */` prefix for the compiled SQL, or empty when no comments. */
|
|
3200
|
+
/** Compile the registered CTEs to the wire shape — shared by SELECT and DML. */
|
|
3201
|
+
#compiledCtes(): Array<{
|
|
3202
|
+
name: string;
|
|
3203
|
+
sql: string;
|
|
3204
|
+
params: unknown[];
|
|
3205
|
+
recursive: boolean;
|
|
3206
|
+
materialized: boolean | null;
|
|
3207
|
+
columns: string[];
|
|
3208
|
+
}> {
|
|
3209
|
+
return this.#ctes.map((c) => {
|
|
3210
|
+
const { sql, params } = c.query.#compiledNative();
|
|
3211
|
+
return {
|
|
3212
|
+
name: c.name,
|
|
3213
|
+
sql,
|
|
3214
|
+
params,
|
|
3215
|
+
recursive: c.recursive ?? false,
|
|
3216
|
+
materialized: c.materialized ?? null,
|
|
3217
|
+
columns: c.columns ?? [],
|
|
3218
|
+
};
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
|
|
3222
|
+
#commentPrefix(): string {
|
|
3223
|
+
return this.#comments.length > 0
|
|
3224
|
+
? `${this.#comments.map((c) => `/* ${c} */`).join(" ")} `
|
|
3225
|
+
: "";
|
|
2558
3226
|
}
|
|
2559
3227
|
|
|
2560
3228
|
/**
|
|
@@ -2582,11 +3250,9 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2582
3250
|
}
|
|
2583
3251
|
|
|
2584
3252
|
async #doExec(): Promise<T[]> {
|
|
2585
|
-
const { sql, params } = this
|
|
2586
|
-
const rawRows = await this.#
|
|
2587
|
-
sql,
|
|
2588
|
-
params,
|
|
2589
|
-
this.#meta("exec"),
|
|
3253
|
+
const { sql, params } = this.#compiledNative();
|
|
3254
|
+
const rawRows = await this.#raceTimeout(
|
|
3255
|
+
this.#db.query<Record<string, unknown>>(sql, params, this.#meta("exec")),
|
|
2590
3256
|
);
|
|
2591
3257
|
// Peel withCount / withAggregate alias columns off the raw row into $extras
|
|
2592
3258
|
// BEFORE hydration, so the hydrator doesn't try to interpret them as columns.
|
|
@@ -2611,6 +3277,14 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2611
3277
|
await this.#resolvePreloads(entities);
|
|
2612
3278
|
}
|
|
2613
3279
|
|
|
3280
|
+
// rowTransformer callbacks run per instance AFTER preloads, before resolve
|
|
3281
|
+
// (Lucid), so a transformer can read the eager-loaded relations.
|
|
3282
|
+
if (this.#rowTransformers.length > 0) {
|
|
3283
|
+
for (const entity of entities) {
|
|
3284
|
+
for (const transform of this.#rowTransformers) transform.run(entity);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
|
|
2614
3288
|
return entities;
|
|
2615
3289
|
}
|
|
2616
3290
|
|
|
@@ -2621,17 +3295,21 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2621
3295
|
* instances aren't needed.
|
|
2622
3296
|
*/
|
|
2623
3297
|
async pojo<R = Record<string, unknown>>(): Promise<R[]> {
|
|
2624
|
-
const { sql, params } = this
|
|
3298
|
+
const { sql, params } = this.#compiledNative();
|
|
2625
3299
|
return this.#db.query<R>(sql, params);
|
|
2626
3300
|
}
|
|
2627
3301
|
|
|
2628
3302
|
/**
|
|
2629
3303
|
* Thread arbitrary context onto every instance this query hydrates, exposed as
|
|
2630
3304
|
* `entity.$sideloaded` (AdonisJS Lucid `sideload`) — e.g. the current tenant or
|
|
2631
|
-
* user, so hooks/computed can read it.
|
|
3305
|
+
* user, so hooks/computed can read it. REPLACES the current sideloaded data by
|
|
3306
|
+
* default (Lucid); pass `merge = true` to merge instead. The data also
|
|
3307
|
+
* propagates to preloaded relation queries. Chainable.
|
|
2632
3308
|
*/
|
|
2633
|
-
sideload(values: Record<string, unknown
|
|
2634
|
-
this.#sideloaded =
|
|
3309
|
+
sideload(values: Record<string, unknown>, merge = false): this {
|
|
3310
|
+
this.#sideloaded = merge
|
|
3311
|
+
? { ...this.#sideloaded, ...values }
|
|
3312
|
+
: { ...values };
|
|
2635
3313
|
return this;
|
|
2636
3314
|
}
|
|
2637
3315
|
|
|
@@ -2653,6 +3331,14 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2653
3331
|
relation.type,
|
|
2654
3332
|
ctx,
|
|
2655
3333
|
);
|
|
3334
|
+
// Propagate the parent query's sideloaded context onto the preloaded
|
|
3335
|
+
// relation instances (Adonis Lucid). The related row's own sideloaded
|
|
3336
|
+
// data (if any) wins over the inherited context.
|
|
3337
|
+
if (this.#sideloaded) {
|
|
3338
|
+
for (const related of allRelated) {
|
|
3339
|
+
related.$sideloaded = { ...this.#sideloaded, ...related.$sideloaded };
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
2656
3342
|
await this.#applyNestedPreloads(allRelated, ctx);
|
|
2657
3343
|
}
|
|
2658
3344
|
}
|
|
@@ -2673,7 +3359,14 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
2673
3359
|
// entities using Adonis' `declare field: T` pattern have no own-properties
|
|
2674
3360
|
// on a freshly constructed instance, so `key in entity` is always false and
|
|
2675
3361
|
// every column would be silently dropped. Mirrors `BaseRepository.#hydrate`.
|
|
2676
|
-
|
|
3362
|
+
// For m2m the pivot's otherKey references `relatedKey` (default the related
|
|
3363
|
+
// PK); the load must filter/index rows by THAT column, not always the PK.
|
|
3364
|
+
const relatedPkName =
|
|
3365
|
+
(relation.type === "manyToMany"
|
|
3366
|
+
? relation.pivot?.relatedKey
|
|
3367
|
+
: undefined) ??
|
|
3368
|
+
getPrimaryKey(relatedClass) ??
|
|
3369
|
+
"id";
|
|
2677
3370
|
const validColumns = new Set<string>();
|
|
2678
3371
|
// Reverse map (db column → property) so an explicit `@Column({ columnName })`
|
|
2679
3372
|
// on the related entity hydrates correctly — mirrors `BaseRepository.#hydrate`.
|
|
@@ -3022,6 +3715,9 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3022
3715
|
operator: string;
|
|
3023
3716
|
value: unknown;
|
|
3024
3717
|
}> = [];
|
|
3718
|
+
// Query-time pivotColumns([...]) requested in the preload callback (replayed
|
|
3719
|
+
// on the scratch builder below) OR directly on this query (related().query()).
|
|
3720
|
+
const extraPivotCols: string[] = [...this.#extraPivotColumns];
|
|
3025
3721
|
if (ctx.nestedCallback) {
|
|
3026
3722
|
const scratch = new ModelQuery<BaseEntity>(
|
|
3027
3723
|
ctx.relatedTable,
|
|
@@ -3034,6 +3730,7 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3034
3730
|
buildValuePreparer(ctx.relatedClass),
|
|
3035
3731
|
);
|
|
3036
3732
|
ctx.nestedCallback(scratch);
|
|
3733
|
+
extraPivotCols.push(...scratch.pivotColumnRequests);
|
|
3037
3734
|
// Apply the pivot column adapters' `prepare` to wherePivot values, so a
|
|
3038
3735
|
// filter like wherePivot('amount', new Money(1)) matches what attach()/
|
|
3039
3736
|
// sync() stored (they prepare the same extras on write).
|
|
@@ -3080,7 +3777,11 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3080
3777
|
|
|
3081
3778
|
// Step 2 — load all related entities in one query
|
|
3082
3779
|
const relRows = await ctx.runRelationQuery(ctx.relatedPk, otherIds);
|
|
3083
|
-
|
|
3780
|
+
// Declared pivot columns (decorator) + any requested at query time
|
|
3781
|
+
// (`pivotColumns([...])`, from the callback or this query), de-duplicated.
|
|
3782
|
+
const pivotCols = [
|
|
3783
|
+
...new Set([...(pivot.pivotColumns ?? []), ...extraPivotCols]),
|
|
3784
|
+
];
|
|
3084
3785
|
const pivotAdapters = pivot.pivotColumnAdapters ?? {};
|
|
3085
3786
|
// When pivot extras are projected, each (parent, related) edge gets its OWN
|
|
3086
3787
|
// hydrated instance so per-edge `$extras.pivot_<col>` values never clobber
|
|
@@ -3254,10 +3955,58 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3254
3955
|
sub.whereIn(column, values);
|
|
3255
3956
|
if (relation.onQuery) relation.onQuery(sub as unknown);
|
|
3256
3957
|
if (userCallback) userCallback(sub);
|
|
3257
|
-
|
|
3958
|
+
// Top-N per parent (Lucid groupLimit) → a ROW_NUMBER() window over the fk.
|
|
3959
|
+
if (sub.#groupLimit != null) {
|
|
3960
|
+
return this.#runGroupLimited(sub, column, sub.#groupLimit);
|
|
3961
|
+
}
|
|
3962
|
+
const { sql, params } = sub.#compiledNative();
|
|
3258
3963
|
return this.#db.query<Record<string, unknown>>(sql, params);
|
|
3259
3964
|
}
|
|
3260
3965
|
|
|
3966
|
+
/**
|
|
3967
|
+
* Run a has-many preload query capped to `n` rows PER PARENT (Lucid
|
|
3968
|
+
* `groupLimit`). Wraps the inner query with a `ROW_NUMBER() OVER (PARTITION BY
|
|
3969
|
+
* <fk> ORDER BY <groupOrderBy | pk>)` window, then keeps `rn <= n`. Supported on
|
|
3970
|
+
* Postgres, MySQL 8+, and SQLite 3.25+ (all have window functions).
|
|
3971
|
+
*/
|
|
3972
|
+
async #runGroupLimited(
|
|
3973
|
+
sub: ModelQuery<BaseEntity>,
|
|
3974
|
+
fkColumn: string,
|
|
3975
|
+
n: number,
|
|
3976
|
+
): Promise<Record<string, unknown>[]> {
|
|
3977
|
+
const quoteSeg = (name: string): string => {
|
|
3978
|
+
const qc = this.#dialect === "mysql" ? "`" : '"';
|
|
3979
|
+
return name
|
|
3980
|
+
.split(".")
|
|
3981
|
+
.map((s) => {
|
|
3982
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(s)) {
|
|
3983
|
+
throw new Error(`groupLimit: unsafe identifier '${name}'`);
|
|
3984
|
+
}
|
|
3985
|
+
return `${qc}${s}${qc}`;
|
|
3986
|
+
})
|
|
3987
|
+
.join(".");
|
|
3988
|
+
};
|
|
3989
|
+
const orderCol = sub.#groupOrderBy
|
|
3990
|
+
? sub.#resolveColumn(sub.#groupOrderBy.column)
|
|
3991
|
+
: (getPrimaryKey(sub.#entityClass) ?? "id");
|
|
3992
|
+
const dir = sub.#groupOrderBy?.direction === "desc" ? "DESC" : "ASC";
|
|
3993
|
+
sub.#selectRaw.push({
|
|
3994
|
+
sql: `ROW_NUMBER() OVER (PARTITION BY ${quoteSeg(fkColumn)} ORDER BY ${quoteSeg(orderCol)} ${dir}) AS __atlas_rn`,
|
|
3995
|
+
params: [],
|
|
3996
|
+
});
|
|
3997
|
+
const inner = sub.#compiledNative();
|
|
3998
|
+
const ph =
|
|
3999
|
+
this.#dialect === "postgres" ? `$${inner.params.length + 1}` : "?";
|
|
4000
|
+
const outerSql = `SELECT * FROM (${inner.sql}) AS __atlas_grp WHERE __atlas_rn <= ${ph}`;
|
|
4001
|
+
const rows = await this.#db.query<Record<string, unknown>>(outerSql, [
|
|
4002
|
+
...inner.params,
|
|
4003
|
+
n,
|
|
4004
|
+
]);
|
|
4005
|
+
// Strip the window helper column so it doesn't leak into hydration.
|
|
4006
|
+
for (const row of rows) delete row.__atlas_rn;
|
|
4007
|
+
return rows;
|
|
4008
|
+
}
|
|
4009
|
+
|
|
3261
4010
|
/**
|
|
3262
4011
|
* Build a correlated subquery over a relation. Returns `SubqueryProjection`
|
|
3263
4012
|
* used by withCount / withAggregate. Default select is `COUNT(*)` for `'count'`
|
|
@@ -3435,35 +4184,114 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3435
4184
|
*/
|
|
3436
4185
|
// === Story 29.4 — joins ===========================================================================
|
|
3437
4186
|
|
|
3438
|
-
/** `INNER JOIN
|
|
4187
|
+
/** `INNER JOIN` — alias of {@link innerJoin} (Lucid/Knex `join`). */
|
|
4188
|
+
join(table: string, left: string, right: string): this;
|
|
4189
|
+
join(table: string, left: string, operator: string, right: string): this;
|
|
4190
|
+
join(table: string, build: (j: JoinBuilder) => void): this;
|
|
4191
|
+
join(
|
|
4192
|
+
table: string,
|
|
4193
|
+
leftOrBuild: string | ((j: JoinBuilder) => void),
|
|
4194
|
+
operatorOrRight?: string,
|
|
4195
|
+
right?: string,
|
|
4196
|
+
): this {
|
|
4197
|
+
return this.#pushJoin("INNER", table, leftOrBuild, operatorOrRight, right);
|
|
4198
|
+
}
|
|
4199
|
+
|
|
4200
|
+
/** `INNER JOIN <table> ON <left> [op] <right>` or a callback `ON` builder. */
|
|
3439
4201
|
innerJoin(table: string, left: string, right: string): this;
|
|
4202
|
+
innerJoin(table: string, left: string, operator: string, right: string): this;
|
|
3440
4203
|
innerJoin(table: string, build: (j: JoinBuilder) => void): this;
|
|
3441
4204
|
innerJoin(
|
|
3442
4205
|
table: string,
|
|
3443
4206
|
leftOrBuild: string | ((j: JoinBuilder) => void),
|
|
4207
|
+
operatorOrRight?: string,
|
|
3444
4208
|
right?: string,
|
|
3445
4209
|
): this {
|
|
3446
|
-
return this.#pushJoin("INNER", table, leftOrBuild, right);
|
|
4210
|
+
return this.#pushJoin("INNER", table, leftOrBuild, operatorOrRight, right);
|
|
3447
4211
|
}
|
|
3448
4212
|
|
|
3449
4213
|
leftJoin(table: string, left: string, right: string): this;
|
|
4214
|
+
leftJoin(table: string, left: string, operator: string, right: string): this;
|
|
3450
4215
|
leftJoin(table: string, build: (j: JoinBuilder) => void): this;
|
|
3451
4216
|
leftJoin(
|
|
3452
4217
|
table: string,
|
|
3453
4218
|
leftOrBuild: string | ((j: JoinBuilder) => void),
|
|
4219
|
+
operatorOrRight?: string,
|
|
4220
|
+
right?: string,
|
|
4221
|
+
): this {
|
|
4222
|
+
return this.#pushJoin("LEFT", table, leftOrBuild, operatorOrRight, right);
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
/** `LEFT OUTER JOIN` — alias of {@link leftJoin} (Lucid/Knex `leftOuterJoin`). */
|
|
4226
|
+
leftOuterJoin(table: string, left: string, right: string): this;
|
|
4227
|
+
leftOuterJoin(
|
|
4228
|
+
table: string,
|
|
4229
|
+
left: string,
|
|
4230
|
+
operator: string,
|
|
4231
|
+
right: string,
|
|
4232
|
+
): this;
|
|
4233
|
+
leftOuterJoin(table: string, build: (j: JoinBuilder) => void): this;
|
|
4234
|
+
leftOuterJoin(
|
|
4235
|
+
table: string,
|
|
4236
|
+
leftOrBuild: string | ((j: JoinBuilder) => void),
|
|
4237
|
+
operatorOrRight?: string,
|
|
3454
4238
|
right?: string,
|
|
3455
4239
|
): this {
|
|
3456
|
-
return this.#pushJoin("LEFT", table, leftOrBuild, right);
|
|
4240
|
+
return this.#pushJoin("LEFT", table, leftOrBuild, operatorOrRight, right);
|
|
3457
4241
|
}
|
|
3458
4242
|
|
|
3459
4243
|
rightJoin(table: string, left: string, right: string): this;
|
|
4244
|
+
rightJoin(table: string, left: string, operator: string, right: string): this;
|
|
3460
4245
|
rightJoin(table: string, build: (j: JoinBuilder) => void): this;
|
|
3461
4246
|
rightJoin(
|
|
3462
4247
|
table: string,
|
|
3463
4248
|
leftOrBuild: string | ((j: JoinBuilder) => void),
|
|
4249
|
+
operatorOrRight?: string,
|
|
4250
|
+
right?: string,
|
|
4251
|
+
): this {
|
|
4252
|
+
return this.#pushJoin("RIGHT", table, leftOrBuild, operatorOrRight, right);
|
|
4253
|
+
}
|
|
4254
|
+
|
|
4255
|
+
/** `RIGHT OUTER JOIN` — alias of {@link rightJoin} (Lucid/Knex `rightOuterJoin`). */
|
|
4256
|
+
rightOuterJoin(table: string, left: string, right: string): this;
|
|
4257
|
+
rightOuterJoin(
|
|
4258
|
+
table: string,
|
|
4259
|
+
left: string,
|
|
4260
|
+
operator: string,
|
|
4261
|
+
right: string,
|
|
4262
|
+
): this;
|
|
4263
|
+
rightOuterJoin(table: string, build: (j: JoinBuilder) => void): this;
|
|
4264
|
+
rightOuterJoin(
|
|
4265
|
+
table: string,
|
|
4266
|
+
leftOrBuild: string | ((j: JoinBuilder) => void),
|
|
4267
|
+
operatorOrRight?: string,
|
|
3464
4268
|
right?: string,
|
|
3465
4269
|
): this {
|
|
3466
|
-
return this.#pushJoin("RIGHT", table, leftOrBuild, right);
|
|
4270
|
+
return this.#pushJoin("RIGHT", table, leftOrBuild, operatorOrRight, right);
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
/** `FULL OUTER JOIN` (Lucid/Knex `fullOuterJoin`; Postgres — MySQL/SQLite lack it). */
|
|
4274
|
+
fullOuterJoin(table: string, left: string, right: string): this;
|
|
4275
|
+
fullOuterJoin(
|
|
4276
|
+
table: string,
|
|
4277
|
+
left: string,
|
|
4278
|
+
operator: string,
|
|
4279
|
+
right: string,
|
|
4280
|
+
): this;
|
|
4281
|
+
fullOuterJoin(table: string, build: (j: JoinBuilder) => void): this;
|
|
4282
|
+
fullOuterJoin(
|
|
4283
|
+
table: string,
|
|
4284
|
+
leftOrBuild: string | ((j: JoinBuilder) => void),
|
|
4285
|
+
operatorOrRight?: string,
|
|
4286
|
+
right?: string,
|
|
4287
|
+
): this {
|
|
4288
|
+
return this.#pushJoin(
|
|
4289
|
+
"FULL OUTER",
|
|
4290
|
+
table,
|
|
4291
|
+
leftOrBuild,
|
|
4292
|
+
operatorOrRight,
|
|
4293
|
+
right,
|
|
4294
|
+
);
|
|
3467
4295
|
}
|
|
3468
4296
|
|
|
3469
4297
|
crossJoin(table: string): this {
|
|
@@ -3516,8 +4344,15 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3516
4344
|
|
|
3517
4345
|
// === Story 29.5 — aggregates / exists / pluck =====================================================
|
|
3518
4346
|
|
|
3519
|
-
|
|
4347
|
+
/**
|
|
4348
|
+
* SELECT DISTINCT (Lucid/Knex `distinct`). With columns, those replace the
|
|
4349
|
+
* projection too — `distinct('a', 'b')` ≈ `SELECT DISTINCT a, b`.
|
|
4350
|
+
*/
|
|
4351
|
+
distinct(...columns: string[]): this {
|
|
3520
4352
|
this.#distinct = true;
|
|
4353
|
+
if (columns.length > 0) {
|
|
4354
|
+
this.#select = columns.map((c) => this.#resolveSelect(c));
|
|
4355
|
+
}
|
|
3521
4356
|
return this;
|
|
3522
4357
|
}
|
|
3523
4358
|
|
|
@@ -3569,7 +4404,7 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3569
4404
|
const clone = this.clone();
|
|
3570
4405
|
clone.#select = ["1"];
|
|
3571
4406
|
clone.#limit = 1;
|
|
3572
|
-
const { sql, params } = clone
|
|
4407
|
+
const { sql, params } = clone.#compiledNative();
|
|
3573
4408
|
const rows = await this.#db.query<Record<string, unknown>>(
|
|
3574
4409
|
sql,
|
|
3575
4410
|
params,
|
|
@@ -3587,7 +4422,7 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3587
4422
|
const col = this.#resolveColumn(column);
|
|
3588
4423
|
const clone = this.clone();
|
|
3589
4424
|
clone.#select = [col];
|
|
3590
|
-
const { sql, params } = clone
|
|
4425
|
+
const { sql, params } = clone.#compiledNative();
|
|
3591
4426
|
const rows = await this.#db.query<Record<string, unknown>>(sql, params);
|
|
3592
4427
|
return rows.map((row) => {
|
|
3593
4428
|
const v = row[col];
|
|
@@ -3658,8 +4493,10 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3658
4493
|
unless<V>(
|
|
3659
4494
|
condition: V | undefined | null | false,
|
|
3660
4495
|
fn: (q: this) => void,
|
|
4496
|
+
elseFn?: (q: this) => void,
|
|
3661
4497
|
): this {
|
|
3662
4498
|
if (!condition) fn(this);
|
|
4499
|
+
else if (elseFn) elseFn(this);
|
|
3663
4500
|
return this;
|
|
3664
4501
|
}
|
|
3665
4502
|
|
|
@@ -3669,11 +4506,17 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3669
4506
|
async paginate(page: number, perPage: number): Promise<Paginator<T>> {
|
|
3670
4507
|
const p = Math.max(1, Math.floor(page));
|
|
3671
4508
|
const pp = Math.max(1, Math.floor(perPage));
|
|
3672
|
-
//
|
|
3673
|
-
//
|
|
3674
|
-
|
|
3675
|
-
// COUNT
|
|
4509
|
+
// Adonis Lucid hook order:
|
|
4510
|
+
// beforePaginate([countQuery, query]) → beforeFetch(query)
|
|
4511
|
+
// → (count + data queries) → afterPaginate(paginator) → afterFetch(rows)
|
|
4512
|
+
// The COUNT and data builders are separate clones so a beforePaginate hook
|
|
4513
|
+
// can constrain BOTH (keeping totals in sync), exactly as documented.
|
|
3676
4514
|
const countQ = this.clone();
|
|
4515
|
+
const dataQ = this.clone();
|
|
4516
|
+
await fireHooks(this.#entityClass, "beforePaginate", [countQ, dataQ]);
|
|
4517
|
+
// beforeFetch fires on the main (data) query before either query runs.
|
|
4518
|
+
await fireHooks(this.#entityClass, "beforeFetch", dataQ);
|
|
4519
|
+
// COUNT(*) — strip pagination/order noise from the count clone.
|
|
3677
4520
|
countQ.#limit = undefined;
|
|
3678
4521
|
countQ.#offset = undefined;
|
|
3679
4522
|
countQ.#orderBys = [];
|
|
@@ -3684,12 +4527,12 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3684
4527
|
// group's own size), so `rows[0].count` would be the first group's size, not
|
|
3685
4528
|
// the number of pages. Lucid counts via a subquery: wrap the grouped query
|
|
3686
4529
|
// (select + groupBy + having preserved) and count its rows = group count.
|
|
3687
|
-
const inner = countQ
|
|
4530
|
+
const inner = countQ.#compiledNative();
|
|
3688
4531
|
cSql = `SELECT COUNT(*) AS count FROM (${inner.sql}) AS __paginate_count`;
|
|
3689
4532
|
cParams = inner.params;
|
|
3690
4533
|
} else {
|
|
3691
4534
|
countQ.#select = ["COUNT(*) AS count"];
|
|
3692
|
-
const flat = countQ
|
|
4535
|
+
const flat = countQ.#compiledNative();
|
|
3693
4536
|
cSql = flat.sql;
|
|
3694
4537
|
cParams = flat.params;
|
|
3695
4538
|
}
|
|
@@ -3700,21 +4543,24 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3700
4543
|
);
|
|
3701
4544
|
const total = Number(cRows[0]?.count ?? 0);
|
|
3702
4545
|
|
|
3703
|
-
const dataQ = this.clone();
|
|
3704
4546
|
dataQ.#limit = pp;
|
|
3705
4547
|
dataQ.#offset = (p - 1) * pp;
|
|
3706
|
-
// `#doExec`
|
|
3707
|
-
//
|
|
4548
|
+
// `#doExec` runs the raw fetch + preloads. beforeFetch already fired above;
|
|
4549
|
+
// afterFetch fires AFTER afterPaginate (Lucid order), so #doExec must not
|
|
4550
|
+
// fire either itself — hence #doExec, not exec().
|
|
3708
4551
|
const items = await dataQ.#doExec();
|
|
3709
|
-
await fireHooks(this.#entityClass, "afterPaginate", items);
|
|
3710
4552
|
const metaKeys = this.#entityClass
|
|
3711
4553
|
? getNamingStrategy(this.#entityClass).paginationMetaKeys?.()
|
|
3712
4554
|
: undefined;
|
|
3713
|
-
|
|
4555
|
+
const paginator = new Paginator<T>(
|
|
3714
4556
|
items,
|
|
3715
4557
|
{ total, perPage: pp, currentPage: p },
|
|
3716
4558
|
metaKeys,
|
|
3717
4559
|
);
|
|
4560
|
+
// Lucid: afterPaginate(paginator) then afterFetch(rows).
|
|
4561
|
+
await fireHooks(this.#entityClass, "afterPaginate", paginator);
|
|
4562
|
+
await fireHooks(this.#entityClass, "afterFetch", items);
|
|
4563
|
+
return paginator;
|
|
3718
4564
|
}
|
|
3719
4565
|
|
|
3720
4566
|
/**
|
|
@@ -3813,6 +4659,18 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3813
4659
|
return this;
|
|
3814
4660
|
}
|
|
3815
4661
|
|
|
4662
|
+
/**
|
|
4663
|
+
* Attach arbitrary metadata to the `db:query` event this query emits (Adonis
|
|
4664
|
+
* Lucid `reporterData`) — request id, user id, feature flag, … A listener
|
|
4665
|
+
* reads it off `event.reporterData`. Repeated calls merge. Setting it also
|
|
4666
|
+
* forces emission (like {@link debug}), so the data actually reaches a listener.
|
|
4667
|
+
*/
|
|
4668
|
+
reporterData(data: Record<string, unknown>): this {
|
|
4669
|
+
this.#reporterData = { ...this.#reporterData, ...data };
|
|
4670
|
+
this.#debugFlag = true;
|
|
4671
|
+
return this;
|
|
4672
|
+
}
|
|
4673
|
+
|
|
3816
4674
|
/**
|
|
3817
4675
|
* Context attached to each statement this query runs, so a `db:query`
|
|
3818
4676
|
* listener can say which model and which call produced it — and so
|
|
@@ -3826,17 +4684,18 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3826
4684
|
model: this.#entityClass.name,
|
|
3827
4685
|
method,
|
|
3828
4686
|
debug: this.#debugFlag,
|
|
4687
|
+
reporterData: this.#reporterData,
|
|
4688
|
+
serverTimeoutMs:
|
|
4689
|
+
this.#cancelTimeout && this.#timeoutMs != null
|
|
4690
|
+
? this.#timeoutMs
|
|
4691
|
+
: undefined,
|
|
3829
4692
|
};
|
|
3830
4693
|
}
|
|
3831
4694
|
|
|
3832
4695
|
/** Returns the compiled SQL with bindings interpolated as dialect-safe literals. */
|
|
3833
4696
|
toQuery(): string {
|
|
3834
|
-
const { sql, params } = this
|
|
3835
|
-
|
|
3836
|
-
return sql.replace(/\?|\$\d+/g, () => {
|
|
3837
|
-
const v = params[i++];
|
|
3838
|
-
return this.#literalEscape(v);
|
|
3839
|
-
});
|
|
4697
|
+
const { sql, params } = this.#compiledNative();
|
|
4698
|
+
return interpolateQuery(sql, params);
|
|
3840
4699
|
}
|
|
3841
4700
|
|
|
3842
4701
|
/** Deep clone of this query — mutations on the clone never affect the original. */
|
|
@@ -3857,9 +4716,20 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3857
4716
|
c.#orderBys = [...this.#orderBys];
|
|
3858
4717
|
c.#select = [...this.#select];
|
|
3859
4718
|
c.#limit = this.#limit;
|
|
4719
|
+
c.#groupLimit = this.#groupLimit;
|
|
4720
|
+
c.#groupOrderBy = this.#groupOrderBy
|
|
4721
|
+
? { ...this.#groupOrderBy }
|
|
4722
|
+
: undefined;
|
|
3860
4723
|
c.#offset = this.#offset;
|
|
3861
4724
|
c.#preloads = new Map(this.#preloads);
|
|
4725
|
+
c.#rowTransformers = [...this.#rowTransformers];
|
|
3862
4726
|
c.#selectSubqueries = structuredClone(this.#selectSubqueries);
|
|
4727
|
+
c.#selectRaw = this.#selectRaw.map((s) => ({
|
|
4728
|
+
...s,
|
|
4729
|
+
params: [...s.params],
|
|
4730
|
+
}));
|
|
4731
|
+
c.#timeoutMs = this.#timeoutMs;
|
|
4732
|
+
c.#cancelTimeout = this.#cancelTimeout;
|
|
3863
4733
|
c.#joins = this.#joins.map((j) => ({ sql: j.sql, params: [...j.params] }));
|
|
3864
4734
|
c.#lockMode = this.#lockMode;
|
|
3865
4735
|
c.#lockModifier = this.#lockModifier;
|
|
@@ -3873,6 +4743,7 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3873
4743
|
query: e.query.clone(),
|
|
3874
4744
|
recursive: e.recursive,
|
|
3875
4745
|
materialized: e.materialized,
|
|
4746
|
+
columns: e.columns ? [...e.columns] : undefined,
|
|
3876
4747
|
}));
|
|
3877
4748
|
c.#unions = this.#unions.map((u) => ({
|
|
3878
4749
|
query: u.query.clone(),
|
|
@@ -3880,45 +4751,67 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3880
4751
|
op: u.op,
|
|
3881
4752
|
}));
|
|
3882
4753
|
c.#pivotWheres = structuredCloneSafe(this.#pivotWheres);
|
|
4754
|
+
c.#extraPivotColumns = [...this.#extraPivotColumns];
|
|
3883
4755
|
// Pure closure over pivot metadata — safe to share by reference; it reads the
|
|
3884
4756
|
// clone's own #pivotWheres at build time (passed in), holding no query state.
|
|
3885
4757
|
c.#pivotExists = this.#pivotExists;
|
|
3886
4758
|
c.#debugFlag = this.#debugFlag;
|
|
4759
|
+
c.#reporterData = this.#reporterData
|
|
4760
|
+
? { ...this.#reporterData }
|
|
4761
|
+
: undefined;
|
|
4762
|
+
c.#comments = [...this.#comments];
|
|
3887
4763
|
return c;
|
|
3888
4764
|
}
|
|
3889
4765
|
|
|
3890
4766
|
// === Story 30.2 — update / delete fluent ===========================================================
|
|
3891
4767
|
|
|
3892
|
-
/**
|
|
3893
|
-
|
|
4768
|
+
/**
|
|
4769
|
+
* Execute a fluent UPDATE (Lucid/Knex `update`). Accepts a `{ col: value }` map
|
|
4770
|
+
* OR a single `(column, value)` pair; a value may be a `db.raw(...)` expression.
|
|
4771
|
+
* Returns affected rows (or the RETURNING rows when `returning` is set).
|
|
4772
|
+
*/
|
|
4773
|
+
update(
|
|
4774
|
+
column: string,
|
|
4775
|
+
value: unknown,
|
|
4776
|
+
): DmlBuilder<number | Record<string, unknown>[]>;
|
|
4777
|
+
update(
|
|
3894
4778
|
patch: Record<string, unknown>,
|
|
3895
4779
|
returning?: string[],
|
|
3896
|
-
):
|
|
4780
|
+
): DmlBuilder<number | Record<string, unknown>[]>;
|
|
4781
|
+
update(
|
|
4782
|
+
patchOrColumn: Record<string, unknown> | string,
|
|
4783
|
+
valueOrReturning?: unknown,
|
|
4784
|
+
): DmlBuilder<number | Record<string, unknown>[]> {
|
|
4785
|
+
const patch =
|
|
4786
|
+
typeof patchOrColumn === "string"
|
|
4787
|
+
? { [patchOrColumn]: valueOrReturning }
|
|
4788
|
+
: patchOrColumn;
|
|
4789
|
+
const returning =
|
|
4790
|
+
typeof patchOrColumn === "string"
|
|
4791
|
+
? undefined
|
|
4792
|
+
: (valueOrReturning as string[] | undefined);
|
|
3897
4793
|
if (!patch || Object.keys(patch).length === 0) {
|
|
3898
4794
|
throw new Error("update() requires a non-empty payload");
|
|
3899
4795
|
}
|
|
3900
4796
|
// Lower each value through prepare (DateTime → ISO, @Column adapters) exactly
|
|
3901
4797
|
// like BaseRepository's write paths — the fluent update() must not bypass it.
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
4798
|
+
// A `db.raw(...)` value is emitted as a raw SET expression instead.
|
|
4799
|
+
const setPairs = Object.entries(patch).map(([k, v]): [string, unknown] =>
|
|
4800
|
+
v instanceof RawSql
|
|
4801
|
+
? [this.#resolveColumn(k), { raw: v.sql, rawParams: [...v.params] }]
|
|
4802
|
+
: [this.#resolveColumn(k), this.#prepareValue(k, v)],
|
|
4803
|
+
);
|
|
4804
|
+
return this.#makeDml(
|
|
4805
|
+
(r) => ({
|
|
4806
|
+
kind: "update",
|
|
4807
|
+
table: this.#tableName,
|
|
4808
|
+
set: setPairs,
|
|
4809
|
+
wheres: this.#wheresForDml(),
|
|
4810
|
+
returning: r,
|
|
4811
|
+
ctes: this.#compiledCtes(),
|
|
4812
|
+
}),
|
|
4813
|
+
returning,
|
|
3905
4814
|
);
|
|
3906
|
-
const spec = {
|
|
3907
|
-
kind: "update",
|
|
3908
|
-
table: this.#tableName,
|
|
3909
|
-
set: setPairs,
|
|
3910
|
-
wheres: this.#wheresForDml(),
|
|
3911
|
-
returning: (returning ?? []).map((c) => this.#resolveSelect(c)),
|
|
3912
|
-
};
|
|
3913
|
-
const compiled = compileStatementNative(spec, this.#dialect);
|
|
3914
|
-
if (returning && returning.length > 0) {
|
|
3915
|
-
return this.#db.query<Record<string, unknown>>(
|
|
3916
|
-
compiled.statements[0],
|
|
3917
|
-
compiled.params,
|
|
3918
|
-
);
|
|
3919
|
-
}
|
|
3920
|
-
const r = await this.#db.execute(compiled.statements[0], compiled.params);
|
|
3921
|
-
return r.rowsAffected ?? 0;
|
|
3922
4815
|
}
|
|
3923
4816
|
|
|
3924
4817
|
/**
|
|
@@ -3928,33 +4821,38 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3928
4821
|
* model it issues a hard `DELETE`. Returns affected rows (or rows when
|
|
3929
4822
|
* `returning` is set).
|
|
3930
4823
|
*/
|
|
3931
|
-
|
|
3932
|
-
returning?: string[],
|
|
3933
|
-
): Promise<number | Record<string, unknown>[]> {
|
|
4824
|
+
delete(returning?: string[]): DmlBuilder<number | Record<string, unknown>[]> {
|
|
3934
4825
|
if (this.#softDeletes) {
|
|
3935
|
-
const
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
4826
|
+
const stampedAt = new Date().toISOString();
|
|
4827
|
+
return this.#makeDml(
|
|
4828
|
+
(r) => ({
|
|
4829
|
+
kind: "update",
|
|
4830
|
+
table: this.#tableName,
|
|
4831
|
+
set: [[this.#deletedAtColumn(), stampedAt]],
|
|
4832
|
+
wheres: this.#wheresForDml(),
|
|
4833
|
+
returning: r,
|
|
4834
|
+
ctes: this.#compiledCtes(),
|
|
4835
|
+
}),
|
|
4836
|
+
returning,
|
|
4837
|
+
);
|
|
3943
4838
|
}
|
|
3944
4839
|
return this.forceDelete(returning);
|
|
3945
4840
|
}
|
|
3946
4841
|
|
|
3947
4842
|
/** Hard `DELETE` of the scoped rows, bypassing `@SoftDeletes` (AdonisJS/Lucid `forceDelete`). */
|
|
3948
|
-
|
|
4843
|
+
forceDelete(
|
|
3949
4844
|
returning?: string[],
|
|
3950
|
-
):
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
4845
|
+
): DmlBuilder<number | Record<string, unknown>[]> {
|
|
4846
|
+
return this.#makeDml(
|
|
4847
|
+
(r) => ({
|
|
4848
|
+
kind: "delete",
|
|
4849
|
+
table: this.#tableName,
|
|
4850
|
+
wheres: this.#wheresForDml(),
|
|
4851
|
+
returning: r,
|
|
4852
|
+
ctes: this.#compiledCtes(),
|
|
4853
|
+
}),
|
|
4854
|
+
returning,
|
|
4855
|
+
);
|
|
3958
4856
|
}
|
|
3959
4857
|
|
|
3960
4858
|
/**
|
|
@@ -3963,30 +4861,38 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3963
4861
|
* on a non-soft-delete model. Independent of the current soft-scope — it always
|
|
3964
4862
|
* targets trashed rows (`deleted_at IS NOT NULL`).
|
|
3965
4863
|
*/
|
|
3966
|
-
|
|
4864
|
+
restore(
|
|
3967
4865
|
returning?: string[],
|
|
3968
|
-
):
|
|
3969
|
-
if (!this.#softDeletes)
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
4866
|
+
): DmlBuilder<number | Record<string, unknown>[]> {
|
|
4867
|
+
if (!this.#softDeletes) {
|
|
4868
|
+
return new DmlBuilder(
|
|
4869
|
+
() => Promise.resolve<number | Record<string, unknown>[]>(0),
|
|
4870
|
+
() => ({ sql: "", bindings: [], params: [] }),
|
|
4871
|
+
this.#dmlHooks(),
|
|
4872
|
+
);
|
|
4873
|
+
}
|
|
4874
|
+
return this.#makeDml((r) => {
|
|
4875
|
+
const wheres = this.#userWheresForDml();
|
|
4876
|
+
wheres.push({
|
|
4877
|
+
column: this.#deletedAtColumn(),
|
|
4878
|
+
operator: "IS NOT NULL",
|
|
4879
|
+
value: null,
|
|
4880
|
+
type: "and",
|
|
4881
|
+
});
|
|
4882
|
+
return {
|
|
4883
|
+
kind: "update",
|
|
4884
|
+
table: this.#tableName,
|
|
4885
|
+
set: [[this.#deletedAtColumn(), null]],
|
|
4886
|
+
wheres,
|
|
4887
|
+
returning: r,
|
|
4888
|
+
ctes: this.#compiledCtes(),
|
|
4889
|
+
};
|
|
4890
|
+
}, returning);
|
|
3985
4891
|
}
|
|
3986
4892
|
|
|
3987
4893
|
// === Story 30.3 — increment / decrement already implemented? check ================================
|
|
3988
4894
|
|
|
3989
|
-
increment(column: string, amount
|
|
4895
|
+
increment(column: string, amount?: number): Promise<number>;
|
|
3990
4896
|
increment(patch: Record<string, number>): Promise<number>;
|
|
3991
4897
|
increment(
|
|
3992
4898
|
colOrPatch: string | Record<string, number>,
|
|
@@ -3995,7 +4901,7 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
3995
4901
|
return this.#runIncDec("increment", colOrPatch, amount);
|
|
3996
4902
|
}
|
|
3997
4903
|
|
|
3998
|
-
decrement(column: string, amount
|
|
4904
|
+
decrement(column: string, amount?: number): Promise<number>;
|
|
3999
4905
|
decrement(patch: Record<string, number>): Promise<number>;
|
|
4000
4906
|
decrement(
|
|
4001
4907
|
colOrPatch: string | Record<string, number>,
|
|
@@ -4103,87 +5009,190 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
4103
5009
|
}
|
|
4104
5010
|
|
|
4105
5011
|
#pushJoin(
|
|
4106
|
-
kind: "INNER" | "LEFT" | "RIGHT",
|
|
5012
|
+
kind: "INNER" | "LEFT" | "RIGHT" | "FULL OUTER",
|
|
4107
5013
|
table: string,
|
|
4108
5014
|
leftOrBuild: string | ((j: JoinBuilder) => void),
|
|
5015
|
+
operatorOrRight?: string,
|
|
4109
5016
|
right?: string,
|
|
4110
5017
|
): this {
|
|
4111
5018
|
const tq = this.#quoteCol(table);
|
|
4112
5019
|
if (typeof leftOrBuild === "function") {
|
|
5020
|
+
const parts: JoinPartMQ[] = [];
|
|
4113
5021
|
const jb: JoinBuilder = {
|
|
4114
|
-
parts
|
|
4115
|
-
on(l: string,
|
|
4116
|
-
|
|
4117
|
-
|
|
5022
|
+
parts,
|
|
5023
|
+
on: (l: string, opOrR: string, r?: string) => {
|
|
5024
|
+
parts.push(
|
|
5025
|
+
r === undefined
|
|
5026
|
+
? { kind: "and", left: l, right: opOrR }
|
|
5027
|
+
: { kind: "and", left: l, operator: opOrR, right: r },
|
|
5028
|
+
);
|
|
5029
|
+
return jb;
|
|
4118
5030
|
},
|
|
4119
|
-
andOn(l: string,
|
|
4120
|
-
|
|
4121
|
-
|
|
5031
|
+
andOn: (l: string, opOrR: string, r?: string) => {
|
|
5032
|
+
parts.push(
|
|
5033
|
+
r === undefined
|
|
5034
|
+
? { kind: "and", left: l, right: opOrR }
|
|
5035
|
+
: { kind: "and", left: l, operator: opOrR, right: r },
|
|
5036
|
+
);
|
|
5037
|
+
return jb;
|
|
5038
|
+
},
|
|
5039
|
+
orOn: (l: string, opOrR: string, r?: string) => {
|
|
5040
|
+
parts.push(
|
|
5041
|
+
r === undefined
|
|
5042
|
+
? { kind: "or", left: l, right: opOrR }
|
|
5043
|
+
: { kind: "or", left: l, operator: opOrR, right: r },
|
|
5044
|
+
);
|
|
5045
|
+
return jb;
|
|
5046
|
+
},
|
|
5047
|
+
onVal: (l, v) => {
|
|
5048
|
+
parts.push({ kind: "and", left: l, value: { v } });
|
|
5049
|
+
return jb;
|
|
4122
5050
|
},
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
return
|
|
5051
|
+
andOnVal: (l, v) => {
|
|
5052
|
+
parts.push({ kind: "and", left: l, value: { v } });
|
|
5053
|
+
return jb;
|
|
5054
|
+
},
|
|
5055
|
+
orOnVal: (l, v) => {
|
|
5056
|
+
parts.push({ kind: "or", left: l, value: { v } });
|
|
5057
|
+
return jb;
|
|
5058
|
+
},
|
|
5059
|
+
onIn: (l, values) => {
|
|
5060
|
+
parts.push({ kind: "and", left: l, values: [...values] });
|
|
5061
|
+
return jb;
|
|
5062
|
+
},
|
|
5063
|
+
onNotIn: (l, values) => {
|
|
5064
|
+
parts.push({
|
|
5065
|
+
kind: "and",
|
|
5066
|
+
left: l,
|
|
5067
|
+
values: [...values],
|
|
5068
|
+
notIn: true,
|
|
5069
|
+
});
|
|
5070
|
+
return jb;
|
|
4126
5071
|
},
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
return
|
|
5072
|
+
onNull: (l) => {
|
|
5073
|
+
parts.push({ kind: "and", left: l, nullOp: "IS NULL" });
|
|
5074
|
+
return jb;
|
|
4130
5075
|
},
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
return
|
|
5076
|
+
onNotNull: (l) => {
|
|
5077
|
+
parts.push({ kind: "and", left: l, nullOp: "IS NOT NULL" });
|
|
5078
|
+
return jb;
|
|
4134
5079
|
},
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
return
|
|
5080
|
+
onBetween: (l, range) => {
|
|
5081
|
+
parts.push({ kind: "and", left: l, between: [range[0], range[1]] });
|
|
5082
|
+
return jb;
|
|
5083
|
+
},
|
|
5084
|
+
onNotBetween: (l, range) => {
|
|
5085
|
+
parts.push({
|
|
5086
|
+
kind: "and",
|
|
5087
|
+
left: l,
|
|
5088
|
+
between: [range[0], range[1]],
|
|
5089
|
+
notBetween: true,
|
|
5090
|
+
});
|
|
5091
|
+
return jb;
|
|
5092
|
+
},
|
|
5093
|
+
onExists: (sub) => {
|
|
5094
|
+
const { sql, params } = this.#resolveUnion(sub).#compiledNative();
|
|
5095
|
+
parts.push({ kind: "and", exists: { sql, params, not: false } });
|
|
5096
|
+
return jb;
|
|
5097
|
+
},
|
|
5098
|
+
onNotExists: (sub) => {
|
|
5099
|
+
const { sql, params } = this.#resolveUnion(sub).#compiledNative();
|
|
5100
|
+
parts.push({ kind: "and", exists: { sql, params, not: true } });
|
|
5101
|
+
return jb;
|
|
4138
5102
|
},
|
|
4139
5103
|
};
|
|
4140
5104
|
leftOrBuild(jb);
|
|
4141
|
-
//
|
|
5105
|
+
// A BASE-table column runs the full model prepare (DateTime→ISO + @Column
|
|
5106
|
+
// adapters/casts), keyed by its property. A FOREIGN join column must NOT
|
|
5107
|
+
// borrow the root model's adapter for a same-named column on another
|
|
5108
|
+
// table — apply only universal type-lowering, matching Knex's
|
|
5109
|
+
// model-agnostic join binding.
|
|
5110
|
+
const prep = (col: string, v: unknown): unknown => {
|
|
5111
|
+
const dot = col.lastIndexOf(".");
|
|
5112
|
+
const tablePrefix = dot >= 0 ? col.slice(0, dot) : "";
|
|
5113
|
+
const leaf = dot >= 0 ? col.slice(dot + 1) : col;
|
|
5114
|
+
const isBaseColumn =
|
|
5115
|
+
tablePrefix === "" || sameTableRef(tablePrefix, this.#tableName);
|
|
5116
|
+
return isBaseColumn ? this.#prepareValue(leaf, v) : lowerJoinValue(v);
|
|
5117
|
+
};
|
|
4142
5118
|
const params: unknown[] = [];
|
|
4143
|
-
const on =
|
|
5119
|
+
const on = parts
|
|
4144
5120
|
.map((p, i) => {
|
|
4145
5121
|
const prefix = i === 0 ? "ON" : p.kind === "or" ? "OR" : "AND";
|
|
4146
|
-
if (p.
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
const isBaseColumn =
|
|
4156
|
-
tablePrefix === "" || sameTableRef(tablePrefix, this.#tableName);
|
|
5122
|
+
if (p.exists) {
|
|
5123
|
+
params.push(...p.exists.params);
|
|
5124
|
+
return `${prefix} ${p.exists.not ? "NOT EXISTS" : "EXISTS"} (${p.exists.sql})`;
|
|
5125
|
+
}
|
|
5126
|
+
const col = this.#quoteCol(p.left ?? "");
|
|
5127
|
+
if (p.nullOp) {
|
|
5128
|
+
return `${prefix} ${col} ${p.nullOp}`;
|
|
5129
|
+
}
|
|
5130
|
+
if (p.between) {
|
|
4157
5131
|
params.push(
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
: lowerJoinValue(p.value.v),
|
|
5132
|
+
prep(p.left ?? "", p.between[0]),
|
|
5133
|
+
prep(p.left ?? "", p.between[1]),
|
|
4161
5134
|
);
|
|
4162
|
-
return `${prefix} ${
|
|
5135
|
+
return `${prefix} ${col} ${p.notBetween ? "NOT BETWEEN" : "BETWEEN"} ? AND ?`;
|
|
5136
|
+
}
|
|
5137
|
+
if (p.values) {
|
|
5138
|
+
const placeholders = p.values.map(() => "?").join(", ");
|
|
5139
|
+
for (const v of p.values) params.push(prep(p.left ?? "", v));
|
|
5140
|
+
return `${prefix} ${col} ${p.notIn ? "NOT IN" : "IN"} (${placeholders})`;
|
|
5141
|
+
}
|
|
5142
|
+
if (p.value) {
|
|
5143
|
+
params.push(prep(p.left ?? "", p.value.v));
|
|
5144
|
+
return `${prefix} ${col} ${this.#validateJoinOp(p.operator ?? "=")} ?`;
|
|
4163
5145
|
}
|
|
4164
|
-
return `${prefix} ${this.#
|
|
5146
|
+
return `${prefix} ${col} ${this.#validateJoinOp(p.operator ?? "=")} ${this.#quoteCol(p.right ?? "")}`;
|
|
4165
5147
|
})
|
|
4166
5148
|
.join(" ");
|
|
4167
5149
|
this.#joins.push({ sql: `${kind} JOIN ${tq} ${on}`, params });
|
|
4168
5150
|
return this;
|
|
4169
5151
|
}
|
|
4170
|
-
|
|
5152
|
+
// String form: 3-arg `(left, right)` or 4-arg `(left, operator, right)`.
|
|
5153
|
+
const left = leftOrBuild;
|
|
5154
|
+
const operator = right === undefined ? "=" : (operatorOrRight ?? "=");
|
|
5155
|
+
const rightCol = right === undefined ? operatorOrRight : right;
|
|
5156
|
+
if (rightCol === undefined)
|
|
4171
5157
|
throw new Error(
|
|
4172
5158
|
"join() with string form requires both left and right operands",
|
|
4173
5159
|
);
|
|
4174
5160
|
this.#joins.push({
|
|
4175
|
-
sql: `${kind} JOIN ${tq} ON ${this.#quoteCol(
|
|
5161
|
+
sql: `${kind} JOIN ${tq} ON ${this.#quoteCol(left)} ${this.#validateJoinOp(operator)} ${this.#quoteCol(rightCol)}`,
|
|
4176
5162
|
params: [],
|
|
4177
5163
|
});
|
|
4178
5164
|
return this;
|
|
4179
5165
|
}
|
|
4180
5166
|
|
|
5167
|
+
/** Allowlist the comparison operator embedded verbatim into a JOIN's ON SQL. */
|
|
5168
|
+
#validateJoinOp(op: string): string {
|
|
5169
|
+
const t = op.trim();
|
|
5170
|
+
const up = t.toUpperCase();
|
|
5171
|
+
const allowed = new Set([
|
|
5172
|
+
"=",
|
|
5173
|
+
"<>",
|
|
5174
|
+
"!=",
|
|
5175
|
+
"<",
|
|
5176
|
+
"<=",
|
|
5177
|
+
">",
|
|
5178
|
+
">=",
|
|
5179
|
+
"LIKE",
|
|
5180
|
+
"NOT LIKE",
|
|
5181
|
+
"ILIKE",
|
|
5182
|
+
]);
|
|
5183
|
+
if (allowed.has(t)) return t;
|
|
5184
|
+
if (allowed.has(up)) return up;
|
|
5185
|
+
throw new Error(`Unsupported join operator '${op}'.`);
|
|
5186
|
+
}
|
|
5187
|
+
|
|
4181
5188
|
async #runScalar(expr: string): Promise<unknown> {
|
|
4182
5189
|
const clone = this.clone();
|
|
4183
5190
|
clone.#select = [`${expr} AS __scalar__`];
|
|
4184
5191
|
clone.#orderBys = [];
|
|
4185
|
-
const { sql, params } = clone
|
|
4186
|
-
const rows = await this.#
|
|
5192
|
+
const { sql, params } = clone.#compiledNative();
|
|
5193
|
+
const rows = await this.#raceTimeout(
|
|
5194
|
+
this.#db.query<Record<string, unknown>>(sql, params),
|
|
5195
|
+
);
|
|
4187
5196
|
const row = rows[0];
|
|
4188
5197
|
return row ? row.__scalar__ : null;
|
|
4189
5198
|
}
|
|
@@ -4208,48 +5217,31 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
4208
5217
|
set: setPairs,
|
|
4209
5218
|
wheres: this.#wheresForDml(),
|
|
4210
5219
|
returning: [],
|
|
5220
|
+
ctes: this.#compiledCtes(),
|
|
4211
5221
|
};
|
|
4212
5222
|
const compiled = compileStatementNative(spec, this.#dialect);
|
|
4213
|
-
const r = await this.#
|
|
5223
|
+
const r = await this.#raceTimeout(
|
|
5224
|
+
this.#db.execute(
|
|
5225
|
+
this.#commentPrefix() + compiled.statements[0],
|
|
5226
|
+
compiled.params,
|
|
5227
|
+
this.#meta("dml"),
|
|
5228
|
+
),
|
|
5229
|
+
);
|
|
4214
5230
|
return r.rowsAffected ?? 0;
|
|
4215
5231
|
}
|
|
4216
5232
|
|
|
4217
5233
|
/**
|
|
4218
|
-
*
|
|
4219
|
-
*
|
|
4220
|
-
*
|
|
4221
|
-
*
|
|
5234
|
+
* The user's own WHERE predicates for DML (no soft-delete scope). The DML
|
|
5235
|
+
* compiler now REUSES the SELECT compiler's WHERE lowering, so every predicate
|
|
5236
|
+
* the read builder accepts — standard, `whereRaw`, `whereExists`, sub-queries,
|
|
5237
|
+
* and nested groups — is valid for update/delete too. Passed through unchanged
|
|
5238
|
+
* (same as `#buildSpec` does for reads).
|
|
4222
5239
|
*/
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
const out: Array<Record<string, unknown>> = [];
|
|
4226
|
-
for (const w of this.#wheres) {
|
|
4227
|
-
if ("kind" in w) {
|
|
4228
|
-
if (w.kind === "raw") {
|
|
4229
|
-
out.push({
|
|
4230
|
-
kind: "raw",
|
|
4231
|
-
sql: w.sql,
|
|
4232
|
-
bindings: w.bindings,
|
|
4233
|
-
type: w.type,
|
|
4234
|
-
});
|
|
4235
|
-
continue;
|
|
4236
|
-
}
|
|
4237
|
-
throw new Error(
|
|
4238
|
-
`update/delete do not support '${w.kind}' WHERE clauses. ` +
|
|
4239
|
-
`Supported: plain predicates and whereRaw. Use a raw UPDATE/DELETE for complex criteria.`,
|
|
4240
|
-
);
|
|
4241
|
-
}
|
|
4242
|
-
out.push({
|
|
4243
|
-
column: w.column,
|
|
4244
|
-
operator: w.operator,
|
|
4245
|
-
value: w.value,
|
|
4246
|
-
type: w.type,
|
|
4247
|
-
});
|
|
4248
|
-
}
|
|
4249
|
-
return out;
|
|
5240
|
+
#userWheresForDml(): WhereClause[] {
|
|
5241
|
+
return [...this.#wheres];
|
|
4250
5242
|
}
|
|
4251
5243
|
|
|
4252
|
-
#wheresForDml():
|
|
5244
|
+
#wheresForDml(): WhereClause[] {
|
|
4253
5245
|
const out = this.#userWheresForDml();
|
|
4254
5246
|
// Mirror the read scope (`#buildSpec`): a `@SoftDeletes` model's bulk
|
|
4255
5247
|
// update/delete/increment/decrement must NOT touch trashed rows under the
|
|
@@ -4277,6 +5269,63 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
4277
5269
|
return out;
|
|
4278
5270
|
}
|
|
4279
5271
|
|
|
5272
|
+
/** Compile a DML spec with the comment prefix — for a lazy builder's `.toSQL()`. */
|
|
5273
|
+
#compileDmlSpec(spec: Record<string, unknown>): {
|
|
5274
|
+
sql: string;
|
|
5275
|
+
bindings: unknown[];
|
|
5276
|
+
params: unknown[];
|
|
5277
|
+
} {
|
|
5278
|
+
const compiled = compileStatementNative(spec, this.#dialect);
|
|
5279
|
+
const sql = this.#commentPrefix() + compiled.statements[0];
|
|
5280
|
+
return { sql, bindings: compiled.params, params: compiled.params };
|
|
5281
|
+
}
|
|
5282
|
+
|
|
5283
|
+
/** Hooks a lazy DML builder delegates back to (upsert clauses are N/A on a model query). */
|
|
5284
|
+
#dmlHooks(): DmlChainHooks {
|
|
5285
|
+
return {
|
|
5286
|
+
onConflict: () => {},
|
|
5287
|
+
merge: () => {},
|
|
5288
|
+
ignore: () => {},
|
|
5289
|
+
returning: (...cols) => {
|
|
5290
|
+
for (const c of cols) {
|
|
5291
|
+
if (Array.isArray(c)) this.#dmlReturning.push(...c);
|
|
5292
|
+
else this.#dmlReturning.push(c);
|
|
5293
|
+
}
|
|
5294
|
+
},
|
|
5295
|
+
timeout: (ms) => {
|
|
5296
|
+
this.#timeoutMs = ms;
|
|
5297
|
+
},
|
|
5298
|
+
comment: (t) => {
|
|
5299
|
+
this.comment(t);
|
|
5300
|
+
},
|
|
5301
|
+
debug: (enabled) => {
|
|
5302
|
+
this.debug(enabled);
|
|
5303
|
+
},
|
|
5304
|
+
reporterData: (data) => {
|
|
5305
|
+
this.reporterData(data);
|
|
5306
|
+
},
|
|
5307
|
+
};
|
|
5308
|
+
}
|
|
5309
|
+
|
|
5310
|
+
/** Wrap a DML spec-builder as a lazy, chainable, inspectable {@link DmlBuilder}. */
|
|
5311
|
+
#makeDml(
|
|
5312
|
+
build: (returning: string[]) => Record<string, unknown>,
|
|
5313
|
+
paramReturning?: string[],
|
|
5314
|
+
): DmlBuilder<number | Record<string, unknown>[]> {
|
|
5315
|
+
const resolved = () =>
|
|
5316
|
+
[...(paramReturning ?? []), ...this.#dmlReturning].map((c) =>
|
|
5317
|
+
this.#resolveSelect(c),
|
|
5318
|
+
);
|
|
5319
|
+
return new DmlBuilder(
|
|
5320
|
+
() => {
|
|
5321
|
+
const r = resolved();
|
|
5322
|
+
return this.#runDml(build(r), r);
|
|
5323
|
+
},
|
|
5324
|
+
() => this.#compileDmlSpec(build(resolved())),
|
|
5325
|
+
this.#dmlHooks(),
|
|
5326
|
+
);
|
|
5327
|
+
}
|
|
5328
|
+
|
|
4280
5329
|
/** Compile + run a DML spec: returns affected-row count, or rows when `returning` is set. */
|
|
4281
5330
|
async #runDml(
|
|
4282
5331
|
spec: Record<string, unknown>,
|
|
@@ -4284,12 +5333,21 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
4284
5333
|
): Promise<number | Record<string, unknown>[]> {
|
|
4285
5334
|
const compiled = compileStatementNative(spec, this.#dialect);
|
|
4286
5335
|
if (returning && returning.length > 0) {
|
|
4287
|
-
return this.#
|
|
4288
|
-
|
|
4289
|
-
|
|
5336
|
+
return this.#raceTimeout(
|
|
5337
|
+
this.#db.query<Record<string, unknown>>(
|
|
5338
|
+
this.#commentPrefix() + compiled.statements[0],
|
|
5339
|
+
compiled.params,
|
|
5340
|
+
this.#meta("dml"),
|
|
5341
|
+
),
|
|
4290
5342
|
);
|
|
4291
5343
|
}
|
|
4292
|
-
const r = await this.#
|
|
5344
|
+
const r = await this.#raceTimeout(
|
|
5345
|
+
this.#db.execute(
|
|
5346
|
+
this.#commentPrefix() + compiled.statements[0],
|
|
5347
|
+
compiled.params,
|
|
5348
|
+
this.#meta("dml"),
|
|
5349
|
+
),
|
|
5350
|
+
);
|
|
4293
5351
|
return r.rowsAffected ?? 0;
|
|
4294
5352
|
}
|
|
4295
5353
|
|
|
@@ -4306,15 +5364,6 @@ export class ModelQuery<T extends BaseEntity> {
|
|
|
4306
5364
|
* parameters via the Rust compiler — this escaper is never on the hot path.
|
|
4307
5365
|
* If you are tempted to feed `.toQuery()` output into `db.prepare()`, STOP.
|
|
4308
5366
|
*/
|
|
4309
|
-
#literalEscape(v: unknown): string {
|
|
4310
|
-
if (v === null || v === undefined) return "NULL";
|
|
4311
|
-
if (typeof v === "number") return String(v);
|
|
4312
|
-
if (typeof v === "boolean") return v ? "1" : "0";
|
|
4313
|
-
if (v instanceof Date) return `'${v.toISOString()}'`;
|
|
4314
|
-
// Strings — escape single quotes per SQL. NOT injection-safe against `\'`.
|
|
4315
|
-
return `'${String(v).replace(/'/g, "''")}'`;
|
|
4316
|
-
}
|
|
4317
|
-
|
|
4318
5367
|
/**
|
|
4319
5368
|
* Build a parenthesised WHERE group from a callback. A throwaway ModelQuery
|
|
4320
5369
|
* on the SAME table is used as the scratch builder so the callback can call
|