linkgress-orm 0.4.64 → 0.4.66
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/entity/db-context.d.ts +12 -0
- package/dist/entity/db-context.d.ts.map +1 -1
- package/dist/entity/db-context.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/dist/query/cte-root-query.d.ts +16 -0
- package/dist/query/cte-root-query.d.ts.map +1 -1
- package/dist/query/cte-root-query.js +25 -1
- package/dist/query/cte-root-query.js.map +1 -1
- package/dist/query/mock-row-cache.d.ts +38 -0
- package/dist/query/mock-row-cache.d.ts.map +1 -0
- package/dist/query/mock-row-cache.js +65 -0
- package/dist/query/mock-row-cache.js.map +1 -0
- package/dist/query/query-builder.d.ts +33 -0
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +208 -106
- package/dist/query/query-builder.js.map +1 -1
- package/package.json +80 -80
|
@@ -15,6 +15,7 @@ const cte_builder_1 = require("./cte-builder");
|
|
|
15
15
|
const collection_strategy_factory_1 = require("./collection-strategy.factory");
|
|
16
16
|
const union_builder_1 = require("./union-builder");
|
|
17
17
|
const future_query_1 = require("./future-query");
|
|
18
|
+
const mock_row_cache_1 = require("./mock-row-cache");
|
|
18
19
|
const join_utils_1 = require("./join-utils");
|
|
19
20
|
/**
|
|
20
21
|
* Field type categories for optimized result transformation
|
|
@@ -57,6 +58,27 @@ function getRelationEntriesForSchema(schema) {
|
|
|
57
58
|
// Fallback: build the array (for schemas that weren't built with the new TableBuilder)
|
|
58
59
|
return Object.entries(schema.relations);
|
|
59
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Mock-row descriptor cache for {@link ReferenceQueryBuilder.createMockTargetRow}.
|
|
63
|
+
*
|
|
64
|
+
* Building a reference mock row costs O(columns + relations) `Object.defineProperty`
|
|
65
|
+
* calls plus fresh closures per row — and deep selectors (cart → items → product →
|
|
66
|
+
* price → …) rebuild that graph from scratch on EVERY query build. All of it is
|
|
67
|
+
* deterministic in (target schema, relation alias, navigation path), so the property
|
|
68
|
+
* descriptors are built once per signature and reused: each new mock row is a bare
|
|
69
|
+
* object + one `Object.defineProperties` call with the shared descriptor map.
|
|
70
|
+
*
|
|
71
|
+
* Per-instance state (the lazy FieldRef cache and the memoized navigation rows)
|
|
72
|
+
* lives in symbol-keyed slots read by the shared getters through `this`, so sharing
|
|
73
|
+
* descriptors across rows is safe. The navigation-path arrays captured at build
|
|
74
|
+
* time are shared by content — nothing downstream mutates them (they are always
|
|
75
|
+
* spread-copied when extended).
|
|
76
|
+
*/
|
|
77
|
+
const MOCK_ROW_FIELD_REFS = Symbol('linkgressMockFieldRefs');
|
|
78
|
+
const MOCK_ROW_NAV_CACHE = Symbol('linkgressMockNavCache');
|
|
79
|
+
const navigationPathSignature = (path) => path
|
|
80
|
+
.map(step => `${step.alias}:${step.targetTable}:${(step.foreignKeys ?? []).join('+')}:${(step.matches ?? []).join('+')}:${step.isMandatory ? 1 : 0}:${step.sourceAlias ?? ''}`)
|
|
81
|
+
.join('>');
|
|
60
82
|
/**
|
|
61
83
|
* Performance utility: Get target schema for a relation, using cached version if available
|
|
62
84
|
*/
|
|
@@ -734,6 +756,39 @@ class SelectQueryBuilder {
|
|
|
734
756
|
this.offsetValue = count;
|
|
735
757
|
return this;
|
|
736
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Append a row-level lock clause to the final SELECT — `FOR UPDATE` (with the
|
|
761
|
+
* optional `SKIP LOCKED` / `NOWAIT` modifiers). The lock is taken on the rows
|
|
762
|
+
* the statement reads, making a following check+write in the SAME transaction
|
|
763
|
+
* (or a later CTE leg of a fused statement) atomic against every other
|
|
764
|
+
* `forUpdate` reader of those rows — the DB-side replacement for app-level
|
|
765
|
+
* distributed locks around read-then-write sequences (TOCTOU).
|
|
766
|
+
*
|
|
767
|
+
* Always pair with `.orderBy(...)` for a deterministic lock ORDER (lock
|
|
768
|
+
* multi-row sets in a stable order — e.g. ascending id — to avoid deadlocks).
|
|
769
|
+
*
|
|
770
|
+
* Execution paths: toList()/firstOrDefault() emit the clause; UNION builds and
|
|
771
|
+
* nested-collection CTE legs intentionally drop it (a lock on a lateral join
|
|
772
|
+
* leg is meaningless). A query used as a Subquery/CTE leg through
|
|
773
|
+
* `asSubquery()` carries it verbatim — that is the fused-conditional-INSERT
|
|
774
|
+
* pattern's lock leg.
|
|
775
|
+
*
|
|
776
|
+
* @example
|
|
777
|
+
* .orderBy(g => g.id)
|
|
778
|
+
* .forUpdate()
|
|
779
|
+
* .toList()
|
|
780
|
+
*/
|
|
781
|
+
forUpdate(options) {
|
|
782
|
+
if (options?.skipLocked && options?.noWait) {
|
|
783
|
+
throw new Error('forUpdate: skipLocked and noWait are mutually exclusive');
|
|
784
|
+
}
|
|
785
|
+
this.lockClause = options?.skipLocked
|
|
786
|
+
? 'FOR UPDATE SKIP LOCKED'
|
|
787
|
+
: options?.noWait
|
|
788
|
+
? 'FOR UPDATE NOWAIT'
|
|
789
|
+
: 'FOR UPDATE';
|
|
790
|
+
return this;
|
|
791
|
+
}
|
|
737
792
|
orderBy(selector) {
|
|
738
793
|
const mockRow = this._createMockRow();
|
|
739
794
|
const selectedMock = this.selector(mockRow);
|
|
@@ -3139,11 +3194,11 @@ class SelectQueryBuilder {
|
|
|
3139
3194
|
joinClauses.push(collection.joinClause);
|
|
3140
3195
|
}
|
|
3141
3196
|
// Build the final CTE query
|
|
3142
|
-
const sql = `WITH "__mutation__" AS (
|
|
3143
|
-
${mutationWithReturning}
|
|
3144
|
-
)
|
|
3145
|
-
SELECT ${selectParts.join(', ')}
|
|
3146
|
-
FROM "__mutation__"
|
|
3197
|
+
const sql = `WITH "__mutation__" AS (
|
|
3198
|
+
${mutationWithReturning}
|
|
3199
|
+
)
|
|
3200
|
+
SELECT ${selectParts.join(', ')}
|
|
3201
|
+
FROM "__mutation__"
|
|
3147
3202
|
${joinClauses.join('\n')}`;
|
|
3148
3203
|
return { sql, params: allParams, nestedPaths };
|
|
3149
3204
|
}
|
|
@@ -4321,7 +4376,12 @@ ${joinClauses.join('\n')}`;
|
|
|
4321
4376
|
}
|
|
4322
4377
|
// Add DISTINCT if needed
|
|
4323
4378
|
const distinctClause = this.isDistinct ? 'DISTINCT ' : '';
|
|
4324
|
-
|
|
4379
|
+
// Row-level lock clause (forUpdate) — emitted only on the TOP-LEVEL statement:
|
|
4380
|
+
// nested-collection CTE legs run as lateral joins where a lock is meaningless
|
|
4381
|
+
// (and would be a syntax error inside some leg shapes), so the flag is read
|
|
4382
|
+
// ONLY here at the final assembly point.
|
|
4383
|
+
const lockClause = this.lockClause ? `\n${this.lockClause}` : '';
|
|
4384
|
+
finalQuery += `SELECT ${distinctClause}${selectParts.join(', ')}\n${fromClause}\n${whereClause}\n${orderByClause}\n${limitClause}${lockClause}`.trim();
|
|
4325
4385
|
return {
|
|
4326
4386
|
sql: finalQuery,
|
|
4327
4387
|
params: context.allParams,
|
|
@@ -5538,120 +5598,162 @@ class ReferenceQueryBuilder {
|
|
|
5538
5598
|
*/
|
|
5539
5599
|
createMockTargetRow() {
|
|
5540
5600
|
if (this.targetTableSchema) {
|
|
5541
|
-
|
|
5542
|
-
//
|
|
5543
|
-
|
|
5544
|
-
//
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
//
|
|
5548
|
-
|
|
5549
|
-
const
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
}
|
|
5579
|
-
return cached;
|
|
5580
|
-
},
|
|
5581
|
-
enumerable: true,
|
|
5582
|
-
configurable: true,
|
|
5583
|
-
});
|
|
5601
|
+
// Descriptor-level cache — see MockRowCache's doc. Everything the descriptors close
|
|
5602
|
+
// over is fully determined by (target schema object identity, relationName,
|
|
5603
|
+
// sourceAlias, navigation-path content), so rows with the same signature can share
|
|
5604
|
+
// one prebuilt PropertyDescriptorMap. The cross-row cache is OPT-IN via the static
|
|
5605
|
+
// switch (MockRowCache.setEnabled — the host app flips it from its own config):
|
|
5606
|
+
// with it off, each row gets a FRESH descriptor set (the pre-0.4.66 memory profile
|
|
5607
|
+
// — nothing retained beyond the row's lifetime), while the per-row FieldRef and
|
|
5608
|
+
// navigation slots below stay enabled either way (they are row-scoped).
|
|
5609
|
+
const descriptors = mock_row_cache_1.MockRowCache.getOrBuild(`${this.targetTable}|${this.relationName}|${this.sourceAlias ?? ''}|${navigationPathSignature(this.navigationPath)}`, () => this.buildMockRowDescriptors());
|
|
5610
|
+
const mock = {
|
|
5611
|
+
[MOCK_ROW_FIELD_REFS]: {},
|
|
5612
|
+
[MOCK_ROW_NAV_CACHE]: {},
|
|
5613
|
+
};
|
|
5614
|
+
Object.defineProperties(mock, descriptors);
|
|
5615
|
+
return mock;
|
|
5616
|
+
}
|
|
5617
|
+
else {
|
|
5618
|
+
// Fallback: use the shared nested proxy that supports deep property access
|
|
5619
|
+
return createNestedFieldRefProxy(this.relationName);
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
/**
|
|
5623
|
+
* Builds the shared property-descriptor map for {@link createMockTargetRow}'s cached path.
|
|
5624
|
+
* The getters read per-row state through `this`-bound symbol slots, so one descriptor
|
|
5625
|
+
* map serves every row of the same signature.
|
|
5626
|
+
*/
|
|
5627
|
+
buildMockRowDescriptors() {
|
|
5628
|
+
// Add columns - use pre-computed column name map if available
|
|
5629
|
+
const columnNameMap = getColumnNameMapForSchema(this.targetTableSchema);
|
|
5630
|
+
const tableAlias = this.relationName;
|
|
5631
|
+
// Build a mapper lookup for columns (only when needed)
|
|
5632
|
+
const columnMappers = {};
|
|
5633
|
+
const columnSqlTypes = {};
|
|
5634
|
+
for (const [colName, colBuilder] of Object.entries(this.targetTableSchema.columns)) {
|
|
5635
|
+
const config = colBuilder.build();
|
|
5636
|
+
if (config.mapper) {
|
|
5637
|
+
columnMappers[colName] = config.mapper;
|
|
5584
5638
|
}
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
// If sourceAlias is empty, we're in the main query and references are joined in the FROM clause
|
|
5588
|
-
let extendedNavPath = [];
|
|
5589
|
-
if (this.sourceAlias) {
|
|
5590
|
-
// Build the current navigation step to include in path for nested collections
|
|
5591
|
-
// This represents the join from sourceAlias to this.relationName (this.targetTable)
|
|
5592
|
-
const currentNavStep = {
|
|
5593
|
-
alias: this.relationName,
|
|
5594
|
-
targetTable: this.targetTable,
|
|
5595
|
-
foreignKeys: this.foreignKeys,
|
|
5596
|
-
matches: this.matches.length > 0 ? this.matches : ['id'], // Default to 'id' if not specified
|
|
5597
|
-
isMandatory: this.isMandatory,
|
|
5598
|
-
sourceAlias: this.sourceAlias,
|
|
5599
|
-
};
|
|
5600
|
-
extendedNavPath = [...this.navigationPath, currentNavStep];
|
|
5639
|
+
if (config.type) {
|
|
5640
|
+
columnSqlTypes[colName] = config.type;
|
|
5601
5641
|
}
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
5609
|
-
|
|
5610
|
-
|
|
5611
|
-
|
|
5642
|
+
}
|
|
5643
|
+
const sourceTable = this.targetTable; // Actual table name for schema lookup
|
|
5644
|
+
// Collect all navigation aliases from the path leading to this reference
|
|
5645
|
+
// This is needed for WHERE conditions that use multi-level navigation (e.g., task.level.name)
|
|
5646
|
+
const navigationAliases = this.navigationPath.map(nav => nav.alias);
|
|
5647
|
+
const descriptors = {};
|
|
5648
|
+
for (const [colName, dbColumnName] of columnNameMap) {
|
|
5649
|
+
const mapper = columnMappers[colName];
|
|
5650
|
+
descriptors[colName] = {
|
|
5651
|
+
get() {
|
|
5652
|
+
const slots = this;
|
|
5653
|
+
const fieldRefCache = slots[MOCK_ROW_FIELD_REFS] ?? (slots[MOCK_ROW_FIELD_REFS] = {});
|
|
5654
|
+
let cached = fieldRefCache[colName];
|
|
5655
|
+
if (!cached) {
|
|
5656
|
+
cached = fieldRefCache[colName] = {
|
|
5657
|
+
__fieldName: colName,
|
|
5658
|
+
__dbColumnName: dbColumnName,
|
|
5659
|
+
__tableAlias: tableAlias, // Alias for SQL generation
|
|
5660
|
+
__sourceTable: sourceTable, // Actual table name for mapper lookup
|
|
5661
|
+
__mapper: mapper, // Include mapper for toDriver transformation in conditions
|
|
5662
|
+
__sqlType: columnSqlTypes[colName], // Column SQL type — lets flag* emit width-exact mask casts
|
|
5663
|
+
__navigationAliases: navigationAliases, // All intermediate navigation aliases for JOIN resolution
|
|
5664
|
+
};
|
|
5612
5665
|
}
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5666
|
+
return cached;
|
|
5667
|
+
},
|
|
5668
|
+
enumerable: true,
|
|
5669
|
+
configurable: true,
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
// Build extended navigation path for nested collections
|
|
5673
|
+
// Only build navigation path if we have a sourceAlias (meaning we're inside a collection's selector)
|
|
5674
|
+
// If sourceAlias is empty, we're in the main query and references are joined in the FROM clause
|
|
5675
|
+
let extendedNavPath = [];
|
|
5676
|
+
if (this.sourceAlias) {
|
|
5677
|
+
// Build the current navigation step to include in path for nested collections
|
|
5678
|
+
// This represents the join from sourceAlias to this.relationName (this.targetTable)
|
|
5679
|
+
const currentNavStep = {
|
|
5680
|
+
alias: this.relationName,
|
|
5681
|
+
targetTable: this.targetTable,
|
|
5682
|
+
foreignKeys: this.foreignKeys,
|
|
5683
|
+
matches: this.matches.length > 0 ? this.matches : ['id'], // Default to 'id' if not specified
|
|
5684
|
+
isMandatory: this.isMandatory,
|
|
5685
|
+
sourceAlias: this.sourceAlias,
|
|
5686
|
+
};
|
|
5687
|
+
extendedNavPath = [...this.navigationPath, currentNavStep];
|
|
5688
|
+
}
|
|
5689
|
+
// Values captured at descriptor-build time — identical for every row of this
|
|
5690
|
+
// signature (the registry is the process-wide schema registry; `sourceAlias`
|
|
5691
|
+
// determines whether nested references track their join path).
|
|
5692
|
+
const schemaRegistry = this.schemaRegistry;
|
|
5693
|
+
const parentSourceAlias = this.sourceAlias;
|
|
5694
|
+
// Add navigation properties (both collections and references)
|
|
5695
|
+
if (this.targetTableSchema.relations) {
|
|
5696
|
+
for (const [relName, relConfig] of Object.entries(this.targetTableSchema.relations)) {
|
|
5697
|
+
// Try to get target schema from registry (preferred, has full relations) or targetTableBuilder
|
|
5698
|
+
let nestedTargetSchema;
|
|
5699
|
+
if (this.schemaRegistry) {
|
|
5700
|
+
nestedTargetSchema = this.schemaRegistry.get(relConfig.targetTable);
|
|
5701
|
+
}
|
|
5702
|
+
if (!nestedTargetSchema && relConfig.targetTableBuilder) {
|
|
5703
|
+
nestedTargetSchema = relConfig.targetTableBuilder.build();
|
|
5704
|
+
}
|
|
5705
|
+
if (relConfig.type === 'many') {
|
|
5706
|
+
// Collection navigation
|
|
5707
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow)
|
|
5708
|
+
descriptors[relName] = {
|
|
5709
|
+
get() {
|
|
5710
|
+
const slots = this;
|
|
5711
|
+
const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
|
|
5712
|
+
// Memoize per row: selectors revisit the same navigation repeatedly
|
|
5713
|
+
// (aggregates, predicates), and each fresh visit used to rebuild the
|
|
5714
|
+
// whole sub-graph.
|
|
5715
|
+
let cached = navCache[relName];
|
|
5716
|
+
if (cached === undefined) {
|
|
5618
5717
|
const fk = relConfig.foreignKey || relConfig.foreignKeys?.[0] || '';
|
|
5619
|
-
|
|
5718
|
+
cached = navCache[relName] = new CollectionQueryBuilder(relName, relConfig.targetTable, fk, tableAlias, // Use alias (relationName) for correlation in lateral joins
|
|
5620
5719
|
nestedTargetSchema, // Pass the target schema directly
|
|
5621
|
-
|
|
5720
|
+
schemaRegistry, // Pass schema registry for nested resolution
|
|
5622
5721
|
extendedNavPath, // Pass navigation path for intermediate joins (empty if main query)
|
|
5623
5722
|
relConfig.foreignKeys, // Propagate composite FK / literal predicates
|
|
5624
5723
|
relConfig.matches);
|
|
5625
|
-
}
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5634
|
-
|
|
5635
|
-
|
|
5724
|
+
}
|
|
5725
|
+
return cached;
|
|
5726
|
+
},
|
|
5727
|
+
enumerable: false,
|
|
5728
|
+
configurable: true,
|
|
5729
|
+
};
|
|
5730
|
+
}
|
|
5731
|
+
else {
|
|
5732
|
+
// Reference navigation
|
|
5733
|
+
// Non-enumerable to prevent Object.entries triggering getters (avoids stack overflow
|
|
5734
|
+
// with circular relations like User->Posts->User)
|
|
5735
|
+
descriptors[relName] = {
|
|
5736
|
+
get() {
|
|
5737
|
+
const slots = this;
|
|
5738
|
+
const navCache = slots[MOCK_ROW_NAV_CACHE] ?? (slots[MOCK_ROW_NAV_CACHE] = {});
|
|
5739
|
+
let cached = navCache[relName];
|
|
5740
|
+
if (cached === undefined) {
|
|
5636
5741
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
|
|
5637
|
-
|
|
5742
|
+
schemaRegistry, // Pass schema registry for nested resolution
|
|
5638
5743
|
extendedNavPath, // Pass navigation path for nested collections
|
|
5639
|
-
|
|
5744
|
+
parentSourceAlias ? tableAlias : '' // Only set source if tracking path
|
|
5640
5745
|
);
|
|
5641
|
-
|
|
5642
|
-
}
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5746
|
+
cached = navCache[relName] = refBuilder.createMockTargetRow();
|
|
5747
|
+
}
|
|
5748
|
+
return cached;
|
|
5749
|
+
},
|
|
5750
|
+
enumerable: false,
|
|
5751
|
+
configurable: true,
|
|
5752
|
+
};
|
|
5647
5753
|
}
|
|
5648
5754
|
}
|
|
5649
|
-
return mock;
|
|
5650
|
-
}
|
|
5651
|
-
else {
|
|
5652
|
-
// Fallback: use the shared nested proxy that supports deep property access
|
|
5653
|
-
return createNestedFieldRefProxy(this.relationName);
|
|
5654
5755
|
}
|
|
5756
|
+
return descriptors;
|
|
5655
5757
|
}
|
|
5656
5758
|
}
|
|
5657
5759
|
exports.ReferenceQueryBuilder = ReferenceQueryBuilder;
|