@spine-event-engine/storage-datastore 2.0.0-snapshot.15 → 2.0.0-snapshot.18
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.
|
@@ -27,6 +27,9 @@ const casRetryDelayMs = 100;
|
|
|
27
27
|
const wrappedReadOptions = { wrapNumbers: true };
|
|
28
28
|
/**
|
|
29
29
|
* Private flat Datastore-entity codec for one record storage handle.
|
|
30
|
+
*
|
|
31
|
+
* @typeParam I Identifier type encoded in Datastore keys.
|
|
32
|
+
* @typeParam R Generated record message type encoded as payload bytes.
|
|
30
33
|
*/
|
|
31
34
|
class FlatEntityCodec {
|
|
32
35
|
recordSpec;
|
|
@@ -36,6 +39,18 @@ class FlatEntityCodec {
|
|
|
36
39
|
#namespace;
|
|
37
40
|
#idColumn;
|
|
38
41
|
#columnMapping;
|
|
42
|
+
/**
|
|
43
|
+
* Initializes the Datastore kind, tenant namespace, ID codec, and column mapping.
|
|
44
|
+
*
|
|
45
|
+
* @param context Storage tenant context.
|
|
46
|
+
* @param recordSpec Generated record and column contract.
|
|
47
|
+
* @param client Datastore client supplying the default namespace.
|
|
48
|
+
* @param maxClientSideScan Maximum local materialization budget.
|
|
49
|
+
* @param group Optional storage group used in the physical kind.
|
|
50
|
+
* @param kind Optional explicit physical kind.
|
|
51
|
+
* @param namespaceConverter Converts tenant IDs to Datastore namespaces.
|
|
52
|
+
* @param stringifiers Converts message-valued IDs and columns.
|
|
53
|
+
*/
|
|
39
54
|
constructor(context, recordSpec, client, maxClientSideScan, group, kind, namespaceConverter, stringifiers) {
|
|
40
55
|
this.recordSpec = recordSpec;
|
|
41
56
|
this.maxClientSideScan = maxClientSideScan;
|
|
@@ -54,12 +69,34 @@ class FlatEntityCodec {
|
|
|
54
69
|
this.#idColumn = new DatastoreIdColumn(recordSpec.idType, stringifiers);
|
|
55
70
|
this.#columnMapping = new DatastoreColumnMapping(stringifiers);
|
|
56
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Builds a namespaced Datastore key for a record identifier.
|
|
74
|
+
*
|
|
75
|
+
* @param client Datastore client that creates keys.
|
|
76
|
+
* @param id Typed record identifier.
|
|
77
|
+
* @returns Physical key for this kind and namespace.
|
|
78
|
+
*/
|
|
57
79
|
key(client, id) {
|
|
58
80
|
return client.key({
|
|
59
|
-
path: [this.#kind, this
|
|
81
|
+
path: [this.#kind, this.idName(id)],
|
|
60
82
|
...(this.#namespace === undefined ? {} : { namespace: this.#namespace }),
|
|
61
83
|
});
|
|
62
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Returns the canonical Datastore key name for a logical record ID.
|
|
87
|
+
* @param id Typed record identifier.
|
|
88
|
+
* @returns Key name shared by equivalent logical IDs.
|
|
89
|
+
*/
|
|
90
|
+
idName(id) {
|
|
91
|
+
return this.#idColumn.value(id);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Encodes record bytes and declared materialized columns as entity properties.
|
|
95
|
+
*
|
|
96
|
+
* @param record Generated record to serialize.
|
|
97
|
+
* @param columns Materialized column values by declared name.
|
|
98
|
+
* @returns Datastore property map including the binary payload.
|
|
99
|
+
*/
|
|
63
100
|
encode(record, columns) {
|
|
64
101
|
const data = {
|
|
65
102
|
[payloadProperty]: Buffer.from(toBinary(this.recordSpec.recordType, record, { writeUnknownFields: false })),
|
|
@@ -69,6 +106,13 @@ class FlatEntityCodec {
|
|
|
69
106
|
}
|
|
70
107
|
return data;
|
|
71
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Decodes a record ID from an entity's physical key.
|
|
111
|
+
*
|
|
112
|
+
* @param entity Raw Datastore entity.
|
|
113
|
+
* @param client Client whose key symbol identifies the entity key.
|
|
114
|
+
* @returns Typed record identifier.
|
|
115
|
+
*/
|
|
72
116
|
id(entity, client) {
|
|
73
117
|
const key = datastoreKey(entity, client.KEY);
|
|
74
118
|
const name = datastoreKeyName(key);
|
|
@@ -77,36 +121,85 @@ class FlatEntityCodec {
|
|
|
77
121
|
}
|
|
78
122
|
return this.#idColumn.read(name);
|
|
79
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Reads declared column values from a decoded entity record.
|
|
126
|
+
*
|
|
127
|
+
* @param entity Raw Datastore entity.
|
|
128
|
+
* @returns Declared column values by name.
|
|
129
|
+
*/
|
|
80
130
|
columns(entity) {
|
|
81
131
|
return new Map(this.recordSpec.columns.map((column) => [column.name, column.valueIn(this.decode(entity))]));
|
|
82
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Marks the binary record payload as excluded from Datastore indexes.
|
|
135
|
+
*
|
|
136
|
+
* @param data Entity property map being saved.
|
|
137
|
+
* @returns Payload property name to leave unindexed.
|
|
138
|
+
*/
|
|
83
139
|
unindexedProperties(data) {
|
|
84
140
|
void data;
|
|
85
141
|
return [payloadProperty];
|
|
86
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* Creates a provider query for this handle's kind and namespace.
|
|
145
|
+
*
|
|
146
|
+
* @param client Datastore client that creates queries.
|
|
147
|
+
* @returns Query scoped to this record family.
|
|
148
|
+
*/
|
|
87
149
|
createQuery(client) {
|
|
88
150
|
return this.#namespace === undefined
|
|
89
151
|
? client.createQuery(this.#kind)
|
|
90
152
|
: client.createQuery(this.#namespace, this.#kind);
|
|
91
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Validates a declared column and returns its Datastore property name.
|
|
156
|
+
*
|
|
157
|
+
* @param column Requested record column name.
|
|
158
|
+
* @returns Datastore property name for the column.
|
|
159
|
+
*/
|
|
92
160
|
columnProperty(column) {
|
|
93
161
|
if (!this.recordSpec.columns.some((candidate) => candidate.name === column)) {
|
|
94
162
|
throw new Error(`Datastore record column "${column}" is not declared.`);
|
|
95
163
|
}
|
|
96
164
|
return column;
|
|
97
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* Returns a row limit with one sentinel beyond the local scan budget.
|
|
168
|
+
*
|
|
169
|
+
* @returns Provider row limit used to detect budget overflow.
|
|
170
|
+
*/
|
|
98
171
|
queryLimit() {
|
|
99
172
|
return this.maxClientSideScan + 1;
|
|
100
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Returns the tenant namespace selected for this handle.
|
|
176
|
+
*
|
|
177
|
+
* @returns Datastore namespace, if tenant scoping requires one.
|
|
178
|
+
*/
|
|
101
179
|
namespace() {
|
|
102
180
|
return this.#namespace;
|
|
103
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Converts a declared record column value to its provider representation.
|
|
184
|
+
*
|
|
185
|
+
* @param column Declared record column name.
|
|
186
|
+
* @param value Runtime column value.
|
|
187
|
+
* @returns Datastore-compatible column value.
|
|
188
|
+
*/
|
|
104
189
|
columnValue(column, value) {
|
|
105
190
|
const declared = this.recordSpec.columns.find((candidate) => candidate.name === column);
|
|
106
191
|
if (declared === undefined)
|
|
107
192
|
throw new Error(`Datastore record column "${column}" is not declared.`);
|
|
108
193
|
return ColumnMappings.value(this.#columnMapping, declared.type, value);
|
|
109
194
|
}
|
|
195
|
+
/**
|
|
196
|
+
* Captures ordered values and the physical key from the final page entity.
|
|
197
|
+
*
|
|
198
|
+
* @param entity Last raw Datastore entity in a page.
|
|
199
|
+
* @param order Provider ordering used for the page.
|
|
200
|
+
* @param keySymbol Client symbol for the physical entity key.
|
|
201
|
+
* @returns Stable keyset continuation for the next page.
|
|
202
|
+
*/
|
|
110
203
|
pageCursor(entity, order, keySymbol) {
|
|
111
204
|
const key = datastoreKey(entity, keySymbol);
|
|
112
205
|
const record = this.decode(entity);
|
|
@@ -122,6 +215,12 @@ class FlatEntityCodec {
|
|
|
122
215
|
throw new Error("Datastore provider page continuation is malformed.");
|
|
123
216
|
return { values, key };
|
|
124
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* Decodes a generated record from an entity's binary payload.
|
|
220
|
+
*
|
|
221
|
+
* @param entity Raw Datastore entity containing the payload.
|
|
222
|
+
* @returns Generated record without unknown fields.
|
|
223
|
+
*/
|
|
125
224
|
decode(entity) {
|
|
126
225
|
const payload = entity[payloadProperty];
|
|
127
226
|
if (!(payload instanceof Uint8Array)) {
|
|
@@ -169,6 +268,9 @@ function cursorJson(_key, value) {
|
|
|
169
268
|
}
|
|
170
269
|
/**
|
|
171
270
|
* Provides one Datastore-backed storage handle for a record family.
|
|
271
|
+
*
|
|
272
|
+
* @typeParam I Record identifier type used by this handle.
|
|
273
|
+
* @typeParam R Generated record message type stored by this handle.
|
|
172
274
|
*/
|
|
173
275
|
export class DatastoreRecordStorage extends RecordStorage {
|
|
174
276
|
client;
|
|
@@ -340,7 +442,7 @@ export class DatastoreRecordStorage extends RecordStorage {
|
|
|
340
442
|
queryCapabilities() {
|
|
341
443
|
return {
|
|
342
444
|
comparisons: ["equal", "greaterThan", "lessThan", "greaterOrEqual", "lessOrEqual"],
|
|
343
|
-
features: ["order", "
|
|
445
|
+
features: ["order", "limit"],
|
|
344
446
|
};
|
|
345
447
|
}
|
|
346
448
|
/**
|
|
@@ -349,12 +451,79 @@ export class DatastoreRecordStorage extends RecordStorage {
|
|
|
349
451
|
* @returns The candidate storage entries.
|
|
350
452
|
*/
|
|
351
453
|
async queryPlanRecordEntries(plan) {
|
|
352
|
-
|
|
454
|
+
const legal = DatastoreQueryPushdown.legal(plan);
|
|
455
|
+
const largeIds = plan.exhaustive && plan.predicate !== undefined
|
|
456
|
+
? DatastoreQueryPushdown.largeIdSet(plan.predicate, plan.order)
|
|
457
|
+
: undefined;
|
|
458
|
+
if (plan.predicate !== undefined && legal === undefined && largeIds === undefined)
|
|
353
459
|
throw new TypeError("Datastore normalized query has an illegal predicate or inequality ordering.");
|
|
354
460
|
for (const order of plan.order ?? [])
|
|
355
461
|
this.#codec.columnProperty(order.column);
|
|
356
|
-
|
|
357
|
-
|
|
462
|
+
if (plan.exhaustive) {
|
|
463
|
+
if (largeIds !== undefined)
|
|
464
|
+
return this.largeIdPlanEntries(largeIds);
|
|
465
|
+
return this.exhaustivePlanEntries(plan, true);
|
|
466
|
+
}
|
|
467
|
+
return this.boundedPlanEntries(plan);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Reads a mandatory large ID set through provider-legal key batches.
|
|
471
|
+
*
|
|
472
|
+
* @param ids Explicit IDs that every matching record must have.
|
|
473
|
+
* @returns Unique candidates for global predicate and ordering evaluation.
|
|
474
|
+
*/
|
|
475
|
+
async largeIdPlanEntries(ids) {
|
|
476
|
+
const unique = new Map();
|
|
477
|
+
for (const id of ids)
|
|
478
|
+
unique.set(this.#codec.idName(id), id);
|
|
479
|
+
const selected = [...unique.values()];
|
|
480
|
+
const entries = [];
|
|
481
|
+
for (let start = 0; start < selected.length; start += 30) {
|
|
482
|
+
const batch = selected.slice(start, start + 30);
|
|
483
|
+
entries.push(...(await this.exhaustivePlanEntries({ exhaustive: true, predicate: { kind: "ids", ids: batch } }, true)));
|
|
484
|
+
}
|
|
485
|
+
return entries;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Reads every provider page before storage applies the global query semantics.
|
|
489
|
+
*
|
|
490
|
+
* @param plan Validated normalized query plan.
|
|
491
|
+
* @param pushdown Whether the provider can apply the predicate and ordering.
|
|
492
|
+
* @returns Every candidate entry in provider page order.
|
|
493
|
+
*/
|
|
494
|
+
async exhaustivePlanEntries(plan, pushdown) {
|
|
495
|
+
const entries = [];
|
|
496
|
+
let cursor;
|
|
497
|
+
const seen = new Set();
|
|
498
|
+
for (;;) {
|
|
499
|
+
const query = this.planQuery(plan, pushdown);
|
|
500
|
+
query.limit(128);
|
|
501
|
+
if (cursor !== undefined)
|
|
502
|
+
query.start(cursor);
|
|
503
|
+
const response = await this.provider(() => this.client.runQuery(query, wrappedReadOptions));
|
|
504
|
+
entries.push(...DatastoreResults.entities(response).map((entity) => ({
|
|
505
|
+
id: this.#codec.id(entity, this.client),
|
|
506
|
+
record: this.#codec.decode(entity),
|
|
507
|
+
})));
|
|
508
|
+
const info = DatastoreResults.queryInfo(response);
|
|
509
|
+
if (!info.more)
|
|
510
|
+
return entries;
|
|
511
|
+
const next = info.cursor;
|
|
512
|
+
const key = typeof next === "string" ? next : next.toString("base64");
|
|
513
|
+
if (seen.has(key))
|
|
514
|
+
throw new Error("Datastore query continuation did not advance.");
|
|
515
|
+
seen.add(key);
|
|
516
|
+
cursor = next;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Reads one bounded provider result for public and Process Manager plans.
|
|
521
|
+
*
|
|
522
|
+
* @param plan Validated normalized query plan.
|
|
523
|
+
* @returns Candidate entries within the provider's configured bound.
|
|
524
|
+
*/
|
|
525
|
+
async boundedPlanEntries(plan) {
|
|
526
|
+
const query = this.planQuery(plan, true);
|
|
358
527
|
const providerLimit = Math.min(this.#codec.queryLimit(), plan.limit ?? Number.POSITIVE_INFINITY, (plan.candidateLimit ?? defaultQueryCandidateLimit) + 1);
|
|
359
528
|
query.limit(providerLimit);
|
|
360
529
|
const entities = DatastoreResults.entities(await this.provider(() => this.client.runQuery(query, wrappedReadOptions)));
|
|
@@ -366,6 +535,21 @@ export class DatastoreRecordStorage extends RecordStorage {
|
|
|
366
535
|
record: this.#codec.decode(entity),
|
|
367
536
|
}));
|
|
368
537
|
}
|
|
538
|
+
/**
|
|
539
|
+
* Builds one Datastore query with safe predicate and order pushdown.
|
|
540
|
+
*
|
|
541
|
+
* @param plan Normalized query plan to translate.
|
|
542
|
+
* @param pushdown Whether the plan is legal for provider filtering.
|
|
543
|
+
* @returns Query without a page bound or cursor.
|
|
544
|
+
*/
|
|
545
|
+
planQuery(plan, pushdown) {
|
|
546
|
+
const query = this.#codec.createQuery(this.client);
|
|
547
|
+
if (pushdown)
|
|
548
|
+
DatastoreQueryPushdown.plan(query, plan, (id) => this.#codec.key(this.client, id), (column) => this.#codec.columnProperty(column), (column, value) => this.#codec.columnValue(column, value));
|
|
549
|
+
if (plan.exhaustive && (!pushdown || (plan.order?.length ?? 0) === 0))
|
|
550
|
+
query.order("__key__");
|
|
551
|
+
return query;
|
|
552
|
+
}
|
|
369
553
|
/**
|
|
370
554
|
* Reads the record at one storage slot.
|
|
371
555
|
* @param id The storage slot identifier.
|
|
@@ -397,6 +581,14 @@ export class DatastoreRecordStorage extends RecordStorage {
|
|
|
397
581
|
}
|
|
398
582
|
throw new Error("Datastore compare-and-set retry limit was reached.");
|
|
399
583
|
}
|
|
584
|
+
/**
|
|
585
|
+
* Executes one transactional compare-and-set attempt against the stored payload.
|
|
586
|
+
*
|
|
587
|
+
* @param id Record slot selected for mutation.
|
|
588
|
+
* @param expected Required prior materialized record, or absence.
|
|
589
|
+
* @param next Replacement materialized record, or deletion.
|
|
590
|
+
* @returns True when the expected payload matched and commit completed.
|
|
591
|
+
*/
|
|
400
592
|
async attemptCompareAndSet(id, expected, next) {
|
|
401
593
|
const key = this.#codec.key(this.client, id);
|
|
402
594
|
const transaction = this.transaction();
|
|
@@ -458,6 +650,13 @@ export class DatastoreRecordStorage extends RecordStorage {
|
|
|
458
650
|
excludeFromIndexes: this.#codec.unindexedProperties(data),
|
|
459
651
|
}));
|
|
460
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* Executes a provider operation and hides provider error details from callers.
|
|
655
|
+
*
|
|
656
|
+
* @typeParam T Result produced by the provider operation.
|
|
657
|
+
* @param operation Provider request to execute.
|
|
658
|
+
* @returns Provider result after successful execution.
|
|
659
|
+
*/
|
|
461
660
|
async provider(operation) {
|
|
462
661
|
try {
|
|
463
662
|
return await operation();
|
|
@@ -481,24 +680,36 @@ export class DatastoreRecordStorage extends RecordStorage {
|
|
|
481
680
|
}
|
|
482
681
|
}
|
|
483
682
|
/**
|
|
484
|
-
*
|
|
683
|
+
* Compares decoded query values and persisted payload bytes.
|
|
485
684
|
*/
|
|
486
|
-
|
|
685
|
+
class RecordValueMethods {
|
|
487
686
|
// prettier-ignore
|
|
488
687
|
/**
|
|
489
688
|
* Compares canonical local values for equality.
|
|
689
|
+
*
|
|
690
|
+
* @param left First decoded query value.
|
|
691
|
+
* @param right Second decoded query value.
|
|
692
|
+
* @returns True when the query evaluator considers the values equal.
|
|
490
693
|
*/
|
|
491
694
|
equal(left, right) {
|
|
492
695
|
return StorageQueryValues.equal(left, right);
|
|
493
696
|
}
|
|
494
697
|
/**
|
|
495
698
|
* Compares canonical local values for ordering.
|
|
699
|
+
*
|
|
700
|
+
* @param left First decoded query value.
|
|
701
|
+
* @param right Second decoded query value.
|
|
702
|
+
* @returns Signed query sort comparison.
|
|
496
703
|
*/
|
|
497
704
|
compare(left, right) {
|
|
498
705
|
return StorageQueryValues.compare(left, right);
|
|
499
706
|
}
|
|
500
707
|
/**
|
|
501
708
|
* Compares two persisted payload byte arrays.
|
|
709
|
+
*
|
|
710
|
+
* @param left First persisted payload candidate.
|
|
711
|
+
* @param right Second persisted payload candidate.
|
|
712
|
+
* @returns True only when both are byte arrays with identical bytes.
|
|
502
713
|
*/
|
|
503
714
|
payloadEqual(left, right) {
|
|
504
715
|
return (left instanceof Uint8Array &&
|
|
@@ -506,7 +717,11 @@ const RecordValues = Object.freeze(new (class {
|
|
|
506
717
|
left.byteLength === right.byteLength &&
|
|
507
718
|
left.every((value, index) => value === right[index]));
|
|
508
719
|
}
|
|
509
|
-
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Exposes comparisons for decoded query values and persisted payload bytes.
|
|
723
|
+
*/
|
|
724
|
+
const RecordValues = Object.freeze(new RecordValueMethods());
|
|
510
725
|
/**
|
|
511
726
|
* Raised when a query would exceed the adapter's configured finite client-side scan budget.
|
|
512
727
|
*/
|
|
@@ -531,6 +746,13 @@ class DatastoreQueryPushdownHelper {
|
|
|
531
746
|
// prettier-ignore
|
|
532
747
|
/**
|
|
533
748
|
* Translates the legacy record-query surface.
|
|
749
|
+
*
|
|
750
|
+
* @typeParam I Record identifier type used by the query.
|
|
751
|
+
* @param query Mutable Datastore query receiving filters and ordering.
|
|
752
|
+
* @param recordQuery Legacy record query to translate.
|
|
753
|
+
* @param keyFor Converts a record ID to a Datastore key.
|
|
754
|
+
* @param columnProperty Resolves a declared column to a property name.
|
|
755
|
+
* @param columnValue Converts a column value for the provider.
|
|
534
756
|
*/
|
|
535
757
|
translate(query, recordQuery, keyFor, columnProperty, columnValue) {
|
|
536
758
|
if (recordQuery.ids !== undefined && recordQuery.ids.length > 0) {
|
|
@@ -560,6 +782,13 @@ class DatastoreQueryPushdownHelper {
|
|
|
560
782
|
}
|
|
561
783
|
/**
|
|
562
784
|
* Applies the all-or-nothing legal portion of a normalized query plan.
|
|
785
|
+
*
|
|
786
|
+
* @typeParam I Record identifier type used by the plan.
|
|
787
|
+
* @param query Mutable Datastore query receiving safe clauses.
|
|
788
|
+
* @param plan Normalized plan whose predicate and ordering are inspected.
|
|
789
|
+
* @param keyFor Converts a record ID to a Datastore key.
|
|
790
|
+
* @param columnProperty Resolves a declared column to a property name.
|
|
791
|
+
* @param columnValue Converts a column value for the provider.
|
|
563
792
|
*/
|
|
564
793
|
plan(query, plan, keyFor, columnProperty, columnValue) {
|
|
565
794
|
const legal = this.legal(plan);
|
|
@@ -576,6 +805,10 @@ class DatastoreQueryPushdownHelper {
|
|
|
576
805
|
}
|
|
577
806
|
/**
|
|
578
807
|
* Returns the predicate only when every clause satisfies provider restrictions.
|
|
808
|
+
*
|
|
809
|
+
* @typeParam I Record identifier type in ID predicates.
|
|
810
|
+
* @param plan Normalized plan checked for supported predicate shape and ordering.
|
|
811
|
+
* @returns Predicate when Datastore can execute it completely.
|
|
579
812
|
*/
|
|
580
813
|
legal(plan) {
|
|
581
814
|
const predicate = plan.predicate;
|
|
@@ -605,8 +838,40 @@ class DatastoreQueryPushdownHelper {
|
|
|
605
838
|
}
|
|
606
839
|
return predicate;
|
|
607
840
|
}
|
|
841
|
+
/**
|
|
842
|
+
* Finds a mandatory ID set too wide for one Datastore IN filter.
|
|
843
|
+
*
|
|
844
|
+
* @typeParam I Identifier type in the predicate.
|
|
845
|
+
* @param predicate Predicate checked for a mandatory large ID set.
|
|
846
|
+
* @param order Ordering checked against any inequality comparison.
|
|
847
|
+
* @returns A required large ID set, or undefined for other shapes.
|
|
848
|
+
*/
|
|
849
|
+
largeIdSet(predicate, order) {
|
|
850
|
+
const leaves = predicate.kind === "all" ? predicate.predicates : [predicate];
|
|
851
|
+
if (leaves.some((leaf) => leaf.kind !== "ids" && leaf.kind !== "comparison"))
|
|
852
|
+
return undefined;
|
|
853
|
+
const large = leaves.find((leaf) => leaf.kind === "ids" && leaf.ids.length > 30);
|
|
854
|
+
if (large?.kind !== "ids")
|
|
855
|
+
return undefined;
|
|
856
|
+
const narrowed = leaves.map((leaf) => leaf.kind === "ids" && leaf.ids.length > 30 ? { ...leaf, ids: leaf.ids.slice(0, 1) } : leaf);
|
|
857
|
+
const representative = predicate.kind === "all"
|
|
858
|
+
? { kind: "all", predicates: narrowed }
|
|
859
|
+
: { kind: "ids", ids: large.ids.slice(0, 1) };
|
|
860
|
+
const legal = this.legal({
|
|
861
|
+
predicate: representative,
|
|
862
|
+
...(order === undefined ? {} : { order }),
|
|
863
|
+
});
|
|
864
|
+
return legal === undefined ? undefined : large.ids;
|
|
865
|
+
}
|
|
608
866
|
/**
|
|
609
867
|
* Adds one already-legal normalized predicate to the provider query.
|
|
868
|
+
*
|
|
869
|
+
* @typeParam I Record identifier type in ID predicates.
|
|
870
|
+
* @param query Mutable Datastore query receiving provider filters.
|
|
871
|
+
* @param predicate Normalized predicate approved for provider execution.
|
|
872
|
+
* @param keyFor Converts a record ID to a Datastore key.
|
|
873
|
+
* @param columnProperty Resolves a declared column to a property name.
|
|
874
|
+
* @param columnValue Converts a comparison value for the provider.
|
|
610
875
|
*/
|
|
611
876
|
predicate(query, predicate, keyFor, columnProperty, columnValue) {
|
|
612
877
|
if (predicate.kind === "all") {
|
|
@@ -645,13 +910,14 @@ class DatastoreQueryPushdownHelper {
|
|
|
645
910
|
*/
|
|
646
911
|
const DatastoreQueryPushdown = Object.freeze(new DatastoreQueryPushdownHelper());
|
|
647
912
|
/**
|
|
648
|
-
* Selects safe provider bounds for direct record queries.
|
|
913
|
+
* Selects safe provider bounds for direct legacy record queries.
|
|
649
914
|
*/
|
|
650
|
-
|
|
915
|
+
class DatastoreRecordQueryMethods {
|
|
651
916
|
// prettier-ignore
|
|
652
917
|
/**
|
|
653
918
|
* Returns a caller limit only when it cannot change local query semantics.
|
|
654
919
|
*
|
|
920
|
+
* @typeParam I Record identifier type in the legacy query.
|
|
655
921
|
* @param query Supplies the requested record query.
|
|
656
922
|
* @param scanLimit Supplies the configured materialization limit.
|
|
657
923
|
* @returns The finite provider row bound.
|
|
@@ -672,6 +938,7 @@ const DatastoreRecordQuery = Object.freeze(new (class {
|
|
|
672
938
|
/**
|
|
673
939
|
* Returns whether a legacy query is exactly an ascending identifier keyset page.
|
|
674
940
|
*
|
|
941
|
+
* @typeParam I Record identifier type in the legacy query.
|
|
675
942
|
* @param query The record query to classify.
|
|
676
943
|
* @returns `true` when Datastore can apply its limit and continuation completely.
|
|
677
944
|
*/
|
|
@@ -691,7 +958,11 @@ const DatastoreRecordQuery = Object.freeze(new (class {
|
|
|
691
958
|
continuationValue?.field === "id" &&
|
|
692
959
|
RecordValues.equal(continuationValue.value, continuation.id))));
|
|
693
960
|
}
|
|
694
|
-
}
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
* Provides the bounded legacy record-query checks used by this adapter.
|
|
964
|
+
*/
|
|
965
|
+
const DatastoreRecordQuery = Object.freeze(new DatastoreRecordQueryMethods());
|
|
695
966
|
/**
|
|
696
967
|
* Applies record-query filtering, ordering, continuation, and paging locally.
|
|
697
968
|
*/
|
|
@@ -699,6 +970,12 @@ class LocalQueryResultsHelper {
|
|
|
699
970
|
// prettier-ignore
|
|
700
971
|
/**
|
|
701
972
|
* Applies one legacy record query to materialized candidate entries.
|
|
973
|
+
*
|
|
974
|
+
* @typeParam I Record identifier type in the candidates.
|
|
975
|
+
* @typeParam R Generated record message type in the candidates.
|
|
976
|
+
* @param entries Materialized candidates with declared column values.
|
|
977
|
+
* @param query Legacy filter, ordering, continuation, and paging request.
|
|
978
|
+
* @returns Selected record entries after local query evaluation.
|
|
702
979
|
*/
|
|
703
980
|
apply(entries, query) {
|
|
704
981
|
const matching = entries.filter((entry) => this.matches(entry, query));
|
|
@@ -714,6 +991,12 @@ class LocalQueryResultsHelper {
|
|
|
714
991
|
}
|
|
715
992
|
/**
|
|
716
993
|
* Tests whether a candidate satisfies the ID and equality filters.
|
|
994
|
+
*
|
|
995
|
+
* @typeParam I Record identifier type in the candidate.
|
|
996
|
+
* @typeParam R Generated record message type in the candidate.
|
|
997
|
+
* @param entry Candidate record with materialized columns.
|
|
998
|
+
* @param query Legacy ID and column filters.
|
|
999
|
+
* @returns True when the candidate satisfies every requested filter.
|
|
717
1000
|
*/
|
|
718
1001
|
matches(entry, query) {
|
|
719
1002
|
return ((query.ids === undefined ||
|
|
@@ -726,7 +1009,14 @@ class LocalQueryResultsHelper {
|
|
|
726
1009
|
}));
|
|
727
1010
|
}
|
|
728
1011
|
/**
|
|
729
|
-
*
|
|
1012
|
+
* Compares candidates by requested sort fields and canonical ID tie-breaker.
|
|
1013
|
+
*
|
|
1014
|
+
* @typeParam I Record identifier type in the candidates.
|
|
1015
|
+
* @typeParam R Generated record message type in the candidates.
|
|
1016
|
+
* @param left First candidate entry.
|
|
1017
|
+
* @param right Second candidate entry.
|
|
1018
|
+
* @param orders Requested field order in priority order.
|
|
1019
|
+
* @returns Signed sort comparison with ID as final tie-breaker.
|
|
730
1020
|
*/
|
|
731
1021
|
order(left, right, orders) {
|
|
732
1022
|
for (const order of orders) {
|
|
@@ -738,6 +1028,13 @@ class LocalQueryResultsHelper {
|
|
|
738
1028
|
}
|
|
739
1029
|
/**
|
|
740
1030
|
* Compares a candidate with a normalized continuation cursor.
|
|
1031
|
+
*
|
|
1032
|
+
* @typeParam I Record identifier type in the candidate and cursor.
|
|
1033
|
+
* @typeParam R Generated record message type in the candidate.
|
|
1034
|
+
* @param entry Candidate record with materialized columns.
|
|
1035
|
+
* @param orders Requested field order used by the cursor.
|
|
1036
|
+
* @param after Continuation cursor from the preceding page.
|
|
1037
|
+
* @returns Signed comparison after applying order directions and ID tie-breaker.
|
|
741
1038
|
*/
|
|
742
1039
|
continuation(entry, orders, after) {
|
|
743
1040
|
for (let index = 0; index < orders.length; index += 1) {
|
|
@@ -751,7 +1048,13 @@ class LocalQueryResultsHelper {
|
|
|
751
1048
|
return RecordValues.compare(entry.id, after.id);
|
|
752
1049
|
}
|
|
753
1050
|
/**
|
|
754
|
-
*
|
|
1051
|
+
* Reads an ID or indexed column value from a candidate.
|
|
1052
|
+
*
|
|
1053
|
+
* @typeParam I Record identifier type in the candidate.
|
|
1054
|
+
* @typeParam R Generated record message type in the candidate.
|
|
1055
|
+
* @param entry Candidate record with materialized columns.
|
|
1056
|
+
* @param field ID or declared column name.
|
|
1057
|
+
* @returns Candidate ID or stored column value.
|
|
755
1058
|
*/
|
|
756
1059
|
value(entry, field) {
|
|
757
1060
|
return field === "id" ? entry.id : entry.columns.get(field);
|
|
@@ -767,14 +1070,20 @@ const LocalQueryResults = Object.freeze(new LocalQueryResultsHelper());
|
|
|
767
1070
|
class DatastoreTransactionsHelper {
|
|
768
1071
|
// prettier-ignore
|
|
769
1072
|
/**
|
|
770
|
-
*
|
|
1073
|
+
* Replaces transaction errors with a stable public failure message.
|
|
1074
|
+
*
|
|
1075
|
+
* @param error Provider failure whose details must be hidden.
|
|
1076
|
+
* @returns New error without provider details.
|
|
771
1077
|
*/
|
|
772
1078
|
redact(error) {
|
|
773
1079
|
void error;
|
|
774
1080
|
return new Error("Datastore transaction failed.");
|
|
775
1081
|
}
|
|
776
1082
|
/**
|
|
777
|
-
*
|
|
1083
|
+
* Checks for a safe-to-retry Datastore transaction conflict.
|
|
1084
|
+
*
|
|
1085
|
+
* @param error Provider failure to classify.
|
|
1086
|
+
* @returns True for non-sensitive Datastore conflict code 10.
|
|
778
1087
|
*/
|
|
779
1088
|
retry(error) {
|
|
780
1089
|
return (error instanceof Error &&
|
|
@@ -783,6 +1092,9 @@ class DatastoreTransactionsHelper {
|
|
|
783
1092
|
}
|
|
784
1093
|
/**
|
|
785
1094
|
* Waits using exponential jitter before a bounded CAS retry.
|
|
1095
|
+
*
|
|
1096
|
+
* @param attempt Zero-based retry attempt used for backoff.
|
|
1097
|
+
* @returns Completes after the randomized delay.
|
|
786
1098
|
*/
|
|
787
1099
|
async wait(attempt) {
|
|
788
1100
|
const exponentialDelay = casRetryDelayMs * 2 ** attempt;
|
|
@@ -800,7 +1112,10 @@ const DatastoreTransactions = Object.freeze(new DatastoreTransactionsHelper());
|
|
|
800
1112
|
class DatastoreResultsHelper {
|
|
801
1113
|
// prettier-ignore
|
|
802
1114
|
/**
|
|
803
|
-
*
|
|
1115
|
+
* Reads the optional first entity from a Datastore read response.
|
|
1116
|
+
*
|
|
1117
|
+
* @param response Raw provider read response.
|
|
1118
|
+
* @returns First validated entity, if a row was returned.
|
|
804
1119
|
*/
|
|
805
1120
|
first(response) {
|
|
806
1121
|
if (!Array.isArray(response))
|
|
@@ -809,7 +1124,10 @@ class DatastoreResultsHelper {
|
|
|
809
1124
|
return entity === undefined ? undefined : this.entity(entity);
|
|
810
1125
|
}
|
|
811
1126
|
/**
|
|
812
|
-
*
|
|
1127
|
+
* Reads entity rows from a Datastore query response.
|
|
1128
|
+
*
|
|
1129
|
+
* @param response Raw provider query response.
|
|
1130
|
+
* @returns Validated entity objects in provider order.
|
|
813
1131
|
*/
|
|
814
1132
|
entities(response) {
|
|
815
1133
|
if (!Array.isArray(response) || !Array.isArray(response[0])) {
|
|
@@ -818,7 +1136,10 @@ class DatastoreResultsHelper {
|
|
|
818
1136
|
return response[0].map((value) => this.entity(value));
|
|
819
1137
|
}
|
|
820
1138
|
/**
|
|
821
|
-
*
|
|
1139
|
+
* Reads the provider continuation and the remaining-row indication.
|
|
1140
|
+
*
|
|
1141
|
+
* @param response Raw provider query response.
|
|
1142
|
+
* @returns Validated cursor and whether another page exists.
|
|
822
1143
|
*/
|
|
823
1144
|
queryInfo(response) {
|
|
824
1145
|
if (!Array.isArray(response) || typeof response[1] !== "object" || response[1] === null)
|
|
@@ -831,15 +1152,21 @@ class DatastoreResultsHelper {
|
|
|
831
1152
|
moreResults !== "MORE_RESULTS_AFTER_CURSOR")
|
|
832
1153
|
throw new Error("Datastore returned an invalid query response.");
|
|
833
1154
|
const more = moreResults === "MORE_RESULTS_AFTER_LIMIT" || moreResults === "MORE_RESULTS_AFTER_CURSOR";
|
|
834
|
-
if (more
|
|
835
|
-
|
|
1155
|
+
if (more) {
|
|
1156
|
+
if (!(cursor instanceof Buffer || typeof cursor === "string"))
|
|
1157
|
+
throw new Error("Datastore returned an invalid query response.");
|
|
1158
|
+
return { cursor, more: true };
|
|
1159
|
+
}
|
|
836
1160
|
return {
|
|
837
1161
|
cursor: cursor instanceof Buffer || typeof cursor === "string" ? cursor : undefined,
|
|
838
|
-
more,
|
|
1162
|
+
more: false,
|
|
839
1163
|
};
|
|
840
1164
|
}
|
|
841
1165
|
/**
|
|
842
1166
|
* Validates one provider entity object.
|
|
1167
|
+
*
|
|
1168
|
+
* @param value Candidate entity returned by Datastore.
|
|
1169
|
+
* @returns Record-like entity object.
|
|
843
1170
|
*/
|
|
844
1171
|
entity(value) {
|
|
845
1172
|
if (typeof value !== "object" || value === null) {
|