@distrohelena/canton-typescript-sdk 0.1.36 → 0.1.38
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 +143 -34
- package/dist/cjs/core/types/requests/{submit-command-request.js → submit-commands-request.js} +10 -6
- package/dist/cjs/index.js +10 -8
- package/dist/cjs/query/canonical/in-memory-query-evaluator.js +291 -0
- package/dist/cjs/query/canonical/public-identity.js +41 -0
- package/dist/cjs/query/canonical/query-ast.js +2 -0
- package/dist/cjs/query/canonical/query-dataset.js +351 -0
- package/dist/cjs/query/canonical/query-normalizer.js +657 -0
- package/dist/cjs/query/canonical/query-schema.js +86 -0
- package/dist/cjs/query/canton-manager.js +14 -5
- package/dist/cjs/query/errors/query-snapshot-incomplete-error.js +19 -0
- package/dist/cjs/query/grpc/grpc-contract-cache.js +484 -0
- package/dist/cjs/query/grpc/grpc-package-relation-reader.js +218 -0
- package/dist/cjs/query/grpc/grpc-query-client.js +249 -0
- package/dist/cjs/query/grpc/grpc-query-snapshot-reader.js +238 -0
- package/dist/cjs/query/grpc/grpc-query-value-mapper.js +204 -0
- package/dist/cjs/query/grpc/grpc-relation-mapper.js +794 -0
- package/dist/cjs/query/pqs/pqs-query-client.js +151 -582
- package/dist/cjs/query/pqs/pqs-relational-sql-compiler.js +138 -95
- package/dist/cjs/query/pqs/pqs-result-shape.js +28 -0
- package/dist/cjs/query/pqs/pqs-schema-profile.js +49 -48
- package/dist/cjs/query/pqs/pqs-sql-compiler.js +343 -289
- package/dist/cjs/query/pqs/pqs-sql-syntax.js +10 -0
- package/dist/cjs/services/command/command-service-client.js +16 -3
- package/dist/cjs/services/commands/command-payload-builder.js +1 -1
- package/dist/cjs/services/commands/command-submission-pipeline.js +21 -6
- package/dist/cjs/testing/runtime/declarative-action-executor.js +3 -3
- package/dist/cjs/transports/grpc/grpc-transport.js +2 -2
- package/dist/cjs/transports/grpc/mappers/commands-mapper.js +6 -6
- package/dist/cjs/transports/grpc/mappers/interactive-command-mapper.js +1 -1
- package/dist/cjs/transports/json/json-transport.js +1 -1
- package/dist/cjs/transports/json/mappers/commands-mapper.js +3 -3
- package/dist/core/transports/transport.interface.d.ts +4 -4
- package/dist/core/types/prepared-command-submission.d.ts +3 -3
- package/dist/core/types/requests/{submit-command-request.d.ts → submit-commands-request.d.ts} +8 -4
- package/dist/core/types/requests/{submit-command-request.js → submit-commands-request.js} +8 -4
- package/dist/index.d.ts +5 -2
- package/dist/index.js +2 -1
- package/dist/query/canonical/in-memory-query-evaluator.d.ts +22 -0
- package/dist/query/canonical/in-memory-query-evaluator.js +287 -0
- package/dist/query/canonical/public-identity.d.ts +14 -0
- package/dist/query/canonical/public-identity.js +35 -0
- package/dist/query/canonical/query-ast.d.ts +104 -0
- package/dist/query/canonical/query-ast.js +1 -0
- package/dist/query/canonical/query-dataset.d.ts +63 -0
- package/dist/query/canonical/query-dataset.js +343 -0
- package/dist/query/canonical/query-normalizer.d.ts +10 -0
- package/dist/query/canonical/query-normalizer.js +648 -0
- package/dist/query/canonical/query-schema.d.ts +25 -0
- package/dist/query/canonical/query-schema.js +83 -0
- package/dist/query/canton-manager.js +14 -5
- package/dist/query/errors/query-snapshot-incomplete-error.d.ts +14 -0
- package/dist/query/errors/query-snapshot-incomplete-error.js +15 -0
- package/dist/query/grpc/grpc-contract-cache.d.ts +27 -0
- package/dist/query/grpc/grpc-contract-cache.js +479 -0
- package/dist/query/grpc/grpc-package-relation-reader.d.ts +43 -0
- package/dist/query/grpc/grpc-package-relation-reader.js +213 -0
- package/dist/query/grpc/grpc-query-client.d.ts +42 -0
- package/dist/query/grpc/grpc-query-client.js +245 -0
- package/dist/query/grpc/grpc-query-snapshot-reader.d.ts +33 -0
- package/dist/query/grpc/grpc-query-snapshot-reader.js +233 -0
- package/dist/query/grpc/grpc-query-value-mapper.d.ts +15 -0
- package/dist/query/grpc/grpc-query-value-mapper.js +195 -0
- package/dist/query/grpc/grpc-relation-mapper.d.ts +67 -0
- package/dist/query/grpc/grpc-relation-mapper.js +789 -0
- package/dist/query/model-types.d.ts +2 -0
- package/dist/query/pqs/pqs-query-client.d.ts +10 -20
- package/dist/query/pqs/pqs-query-client.js +154 -585
- package/dist/query/pqs/pqs-relational-sql-compiler.d.ts +9 -17
- package/dist/query/pqs/pqs-relational-sql-compiler.js +136 -95
- package/dist/query/pqs/pqs-result-shape.d.ts +25 -0
- package/dist/query/pqs/pqs-result-shape.js +25 -0
- package/dist/query/pqs/pqs-schema-profile.js +49 -48
- package/dist/query/pqs/pqs-sql-compiler.d.ts +18 -3
- package/dist/query/pqs/pqs-sql-compiler.js +337 -288
- package/dist/query/pqs/pqs-sql-syntax.d.ts +2 -0
- package/dist/query/pqs/pqs-sql-syntax.js +6 -0
- package/dist/query/query-client.d.ts +16 -0
- package/dist/services/command/command-service-client.d.ts +9 -4
- package/dist/services/command/command-service-client.js +16 -3
- package/dist/services/command-submission/command-submission-service-client.d.ts +2 -2
- package/dist/services/commands/command-payload-builder.d.ts +2 -2
- package/dist/services/commands/command-payload-builder.js +1 -1
- package/dist/services/commands/command-submission-pipeline.d.ts +5 -4
- package/dist/services/commands/command-submission-pipeline.js +21 -6
- package/dist/testing/runtime/declarative-action-executor.js +3 -3
- package/dist/transports/grpc/grpc-transport.d.ts +4 -4
- package/dist/transports/grpc/grpc-transport.js +3 -3
- package/dist/transports/grpc/mappers/commands-mapper.d.ts +3 -3
- package/dist/transports/grpc/mappers/commands-mapper.js +4 -4
- package/dist/transports/grpc/mappers/interactive-command-mapper.d.ts +3 -3
- package/dist/transports/grpc/mappers/interactive-command-mapper.js +1 -1
- package/dist/transports/json/json-transport.d.ts +2 -2
- package/dist/transports/json/json-transport.js +2 -2
- package/dist/transports/json/mappers/commands-mapper.d.ts +2 -2
- package/dist/transports/json/mappers/commands-mapper.js +2 -2
- package/node/start-local-splice-0.6.12.sh +6 -0
- package/node/start-local-splice-0.6.13.sh +6 -0
- package/node/start-local-splice-0.6.14.sh +6 -0
- package/node/start-local-splice-0.7.0.sh +6 -0
- package/node/stop-local-splice-0.6.12.sh +6 -0
- package/node/stop-local-splice-0.6.13.sh +6 -0
- package/node/stop-local-splice-0.6.14.sh +6 -0
- package/node/stop-local-splice-0.7.0.sh +6 -0
- package/node/test-start-local-splice-0.6.12.sh +28 -0
- package/node/test-start-local-splice-0.6.13.sh +28 -0
- package/node/test-start-local-splice-0.6.14.sh +28 -0
- package/node/test-start-local-splice-0.7.0.sh +28 -0
- package/node/test-stop-local-splice-0.6.12.sh +28 -0
- package/node/test-stop-local-splice-0.6.13.sh +28 -0
- package/node/test-stop-local-splice-0.6.14.sh +28 -0
- package/node/test-stop-local-splice-0.7.0.sh +28 -0
- package/package.json +29 -4
- package/dist/cjs/query/grpc/grpc-contract-query-client.js +0 -178
- package/dist/query/grpc/grpc-contract-query-client.d.ts +0 -33
- package/dist/query/grpc/grpc-contract-query-client.js +0 -174
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { createQueryDataset, immutableQueryValue, relatedQueryRows } from "./query-dataset.js";
|
|
2
|
+
import { queryRelationEdges, queryRelationMetadata } from "./query-schema.js";
|
|
3
|
+
/** Reference evaluator for normalized canonical plans; it intentionally has no transport concerns. */
|
|
4
|
+
export class InMemoryQueryEvaluator {
|
|
5
|
+
execute(dataset, query) {
|
|
6
|
+
const compiled = createQueryDataset(dataset);
|
|
7
|
+
switch (query.kind) {
|
|
8
|
+
case "findMany": return freeze(this.findMany(compiled, query));
|
|
9
|
+
case "findUnique": return freeze(this.findMany(compiled, { kind: "findMany", relation: query.relation, predicate: query.predicate, select: query.select, includes: query.includes, orderBy: [], skip: 0, take: 1, activeOnly: false })[0]);
|
|
10
|
+
case "count": return this.filtered(compiled, query.relation, query.predicate, query.parties, query.activeOnly).length;
|
|
11
|
+
case "aggregate": return freeze(this.aggregate(this.filtered(compiled, query.relation, query.predicate), query));
|
|
12
|
+
case "groupBy": return freeze(this.groupBy(compiled, query));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
findMany(dataset, query) {
|
|
16
|
+
const rows = this.order(this.filtered(dataset, query.relation, query.predicate, query.parties, query.activeOnly), query.orderBy)
|
|
17
|
+
.slice(query.skip, query.take === undefined ? undefined : query.skip + query.take);
|
|
18
|
+
return rows.map((row) => this.shape(dataset, query.relation, row, query.select, query.includes));
|
|
19
|
+
}
|
|
20
|
+
filtered(dataset, relation, predicate, parties, activeOnly = false) {
|
|
21
|
+
return dataset.rows[relation].filter((row) => (!activeOnly || row.active === true)
|
|
22
|
+
&& (parties === undefined || (Array.isArray(row.witnesses) && row.witnesses.some((party) => parties.includes(String(party)))))
|
|
23
|
+
&& (predicate === undefined || this.matches(dataset, relation, row, predicate)));
|
|
24
|
+
}
|
|
25
|
+
matches(dataset, relation, row, predicate) {
|
|
26
|
+
switch (predicate.kind) {
|
|
27
|
+
case "and": return predicate.children.every((child) => this.matches(dataset, relation, row, child));
|
|
28
|
+
case "or": return predicate.children.some((child) => this.matches(dataset, relation, row, child));
|
|
29
|
+
case "not": return !this.matches(dataset, relation, row, predicate.child);
|
|
30
|
+
case "relation": {
|
|
31
|
+
const edge = queryRelationEdges[relation]?.[predicate.edge];
|
|
32
|
+
if (edge === undefined) {
|
|
33
|
+
throw new Error(`Unknown ${relation}.${predicate.edge} edge`);
|
|
34
|
+
}
|
|
35
|
+
const related = this.related(dataset, relation, row, predicate.edge);
|
|
36
|
+
const result = related.map((target) => this.matches(dataset, edge.target, target, predicate.predicate));
|
|
37
|
+
return predicate.quantifier === "one" ? result[0] === true : predicate.quantifier === "some" ? result.some(Boolean) : predicate.quantifier === "none" ? !result.some(Boolean) : result.every(Boolean);
|
|
38
|
+
}
|
|
39
|
+
case "scalar": {
|
|
40
|
+
const actual = at(row, predicate.path);
|
|
41
|
+
const expected = predicate.value;
|
|
42
|
+
switch (predicate.operator) {
|
|
43
|
+
case "equals": return actual !== null && actual !== undefined && expected !== null && expected !== undefined && equal(actual, expected);
|
|
44
|
+
case "in": return actual !== null && actual !== undefined && Array.isArray(expected) && expected.some((candidate) => candidate !== null && candidate !== undefined && equal(actual, candidate));
|
|
45
|
+
case "is": return actual === null || actual === undefined;
|
|
46
|
+
case "isNot": return actual !== null && actual !== undefined;
|
|
47
|
+
case "lt": return compare(actual, expected) < 0;
|
|
48
|
+
case "lte": return compare(actual, expected) <= 0;
|
|
49
|
+
case "gt": return compare(actual, expected) > 0;
|
|
50
|
+
case "gte": return compare(actual, expected) >= 0;
|
|
51
|
+
case "like": return typeof actual === "string" && typeof expected === "string" && sqlLike(actual, expected, false);
|
|
52
|
+
case "ilike": return typeof actual === "string" && typeof expected === "string" && sqlLike(actual, expected, true);
|
|
53
|
+
case "has": return Array.isArray(actual) && actual.some((entry) => equal(entry, expected));
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
related(dataset, relation, row, edge) {
|
|
60
|
+
return [...relatedQueryRows(dataset, relation, row, edge)];
|
|
61
|
+
}
|
|
62
|
+
shape(dataset, relation, row, selection, includes) {
|
|
63
|
+
const fields = selection?.fields ?? Object.keys(row);
|
|
64
|
+
const result = Object.fromEntries(fields.filter((field) => Object.hasOwn(row, field)).map((field) => [field, clone(row[field])]));
|
|
65
|
+
for (const projection of selection?.json ?? []) {
|
|
66
|
+
result[projection.name] = project(at(row, [projection.field, ...projection.path]), projection.as);
|
|
67
|
+
}
|
|
68
|
+
for (const include of includes) {
|
|
69
|
+
const targets = this.related(dataset, relation, row, include.edge).filter((target) => include.predicate === undefined || this.matches(dataset, include.relation, target, include.predicate));
|
|
70
|
+
const bounded = this.order(targets, include.orderBy).slice(include.skip, include.take === undefined ? undefined : include.skip + include.take)
|
|
71
|
+
.map((target) => this.shape(dataset, include.relation, target, include.select, include.includes));
|
|
72
|
+
result[include.edge] = include.cardinality === "many" ? bounded : (bounded[0] ?? null);
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
order(rows, orderBy) {
|
|
77
|
+
return rows.map((row, index) => ({ row, index })).sort((left, right) => {
|
|
78
|
+
for (const order of orderBy) {
|
|
79
|
+
const compared = postgresCompare(at(left.row, order.path), at(right.row, order.path), order.direction);
|
|
80
|
+
if (compared !== 0) {
|
|
81
|
+
return compared;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return left.index - right.index;
|
|
85
|
+
}).map(({ row }) => row);
|
|
86
|
+
}
|
|
87
|
+
aggregate(rows, query) {
|
|
88
|
+
const metadata = queryRelationMetadata[query.relation];
|
|
89
|
+
const result = {};
|
|
90
|
+
if (query.aggregates.count) {
|
|
91
|
+
result.count = rows.length;
|
|
92
|
+
}
|
|
93
|
+
for (const operation of ["min", "max", "sum"]) {
|
|
94
|
+
if (query.aggregates[operation].length > 0) {
|
|
95
|
+
for (const field of query.aggregates[operation]) {
|
|
96
|
+
if (!metadata.numericFields.includes(field)) {
|
|
97
|
+
throw new Error(`${field} is not a numeric aggregate field of ${query.relation}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
result[operation] = Object.fromEntries(query.aggregates[operation].map((field) => [field, aggregate(rows.map((row) => at(row, [field])), operation)]));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
groupBy(dataset, query) {
|
|
106
|
+
const groups = new Map();
|
|
107
|
+
for (const row of this.filtered(dataset, query.relation, query.predicate)) {
|
|
108
|
+
const keyVariants = query.by.map((key) => this.groupValues(dataset, query.relation, row, key));
|
|
109
|
+
for (const keys of cartesian(keyVariants)) {
|
|
110
|
+
const identity = JSON.stringify(keys.map(keyIdentity));
|
|
111
|
+
const group = groups.get(identity) ?? { keys, rows: [] };
|
|
112
|
+
group.rows.push(row);
|
|
113
|
+
groups.set(identity, group);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return [...groups.values()].map(({ keys, rows }) => {
|
|
117
|
+
const result = Object.fromEntries(query.by.map((key, index) => [groupName(key), clone(keys[index])]));
|
|
118
|
+
if (query.aggregates.count) {
|
|
119
|
+
result.count = rows.length;
|
|
120
|
+
}
|
|
121
|
+
for (const operation of ["min", "max", "sum"]) {
|
|
122
|
+
for (const field of query.aggregates[operation]) {
|
|
123
|
+
result[`${operation}_${field}`] = aggregate(rows.map((row) => at(row, [field])), operation);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
groupValues(dataset, relation, row, key) {
|
|
130
|
+
if (key.kind === "field") {
|
|
131
|
+
const value = at(row, key.path);
|
|
132
|
+
return Array.isArray(value) ? value : [value];
|
|
133
|
+
}
|
|
134
|
+
else if (key.kind === "json") {
|
|
135
|
+
return [project(at(row, [key.field, ...key.path]), key.as)];
|
|
136
|
+
}
|
|
137
|
+
else if (key.kind !== "bucket") {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const value = key.path.length === 1 ? at(row, key.path) : this.related(dataset, relation, row, key.path[0])[0] && at(this.related(dataset, relation, row, key.path[0])[0], [key.path[1]]);
|
|
141
|
+
return [bucket(value, key.bucket)];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function at(value, path) {
|
|
145
|
+
let current = value;
|
|
146
|
+
for (const key of path) {
|
|
147
|
+
if (current === null || current === undefined || typeof current !== "object") {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
current = current[key];
|
|
151
|
+
}
|
|
152
|
+
return current;
|
|
153
|
+
}
|
|
154
|
+
function clone(value) {
|
|
155
|
+
return immutableQueryValue(value);
|
|
156
|
+
}
|
|
157
|
+
function freeze(value) {
|
|
158
|
+
if (value === null || typeof value !== "object" || value instanceof Date || value instanceof Uint8Array) {
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
else if (Object.isFrozen(value)) {
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
164
|
+
for (const child of Object.values(value)) {
|
|
165
|
+
freeze(child);
|
|
166
|
+
}
|
|
167
|
+
return Object.freeze(value);
|
|
168
|
+
}
|
|
169
|
+
function equal(left, right) {
|
|
170
|
+
if (left instanceof Date) {
|
|
171
|
+
return left.toISOString() === (right instanceof Date ? right.toISOString() : String(right));
|
|
172
|
+
}
|
|
173
|
+
else if (right instanceof Date) {
|
|
174
|
+
return String(left) === right.toISOString();
|
|
175
|
+
}
|
|
176
|
+
else if (left instanceof Uint8Array || right instanceof Uint8Array) {
|
|
177
|
+
return JSON.stringify(Array.from(left)) === JSON.stringify(Array.from(right));
|
|
178
|
+
}
|
|
179
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
180
|
+
}
|
|
181
|
+
function compare(left, right) {
|
|
182
|
+
if (left === null || left === undefined || right === null || right === undefined) {
|
|
183
|
+
return Number.NaN;
|
|
184
|
+
}
|
|
185
|
+
else if (left instanceof Date || right instanceof Date) {
|
|
186
|
+
const leftTime = left instanceof Date ? left.getTime() : new Date(String(left)).getTime();
|
|
187
|
+
const rightTime = right instanceof Date ? right.getTime() : new Date(String(right)).getTime();
|
|
188
|
+
return !Number.isFinite(leftTime) || !Number.isFinite(rightTime) ? Number.NaN : leftTime < rightTime ? -1 : leftTime > rightTime ? 1 : 0;
|
|
189
|
+
}
|
|
190
|
+
const leftValue = numeric(left) ?? String(left);
|
|
191
|
+
const rightValue = numeric(right) ?? String(right);
|
|
192
|
+
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
|
|
193
|
+
}
|
|
194
|
+
function numeric(value) {
|
|
195
|
+
return typeof value === "string" && /^-?\d+$/.test(value) ? BigInt(value) : typeof value === "number" && Number.isFinite(value) ? BigInt(value) : undefined;
|
|
196
|
+
}
|
|
197
|
+
function postgresCompare(left, right, direction) {
|
|
198
|
+
const leftNull = left === null || left === undefined;
|
|
199
|
+
const rightNull = right === null || right === undefined;
|
|
200
|
+
if (leftNull || rightNull) {
|
|
201
|
+
if (leftNull && rightNull) {
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
return leftNull ? (direction === "asc" ? 1 : -1) : (direction === "asc" ? -1 : 1);
|
|
205
|
+
}
|
|
206
|
+
const result = compare(left, right);
|
|
207
|
+
return direction === "asc" ? result : -result;
|
|
208
|
+
}
|
|
209
|
+
function sqlLike(value, pattern, insensitive) {
|
|
210
|
+
let expression = "^";
|
|
211
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
212
|
+
const character = pattern[index];
|
|
213
|
+
if (character === "\\" && index + 1 < pattern.length) {
|
|
214
|
+
expression += escapeRegex(pattern[++index]);
|
|
215
|
+
}
|
|
216
|
+
else if (character === "%") {
|
|
217
|
+
expression += ".*";
|
|
218
|
+
}
|
|
219
|
+
else if (character === "_") {
|
|
220
|
+
expression += ".";
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
expression += escapeRegex(character);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return new RegExp(`${expression}$`, insensitive ? "isu" : "su").test(value);
|
|
227
|
+
}
|
|
228
|
+
function escapeRegex(value) {
|
|
229
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
230
|
+
}
|
|
231
|
+
function project(value, as) {
|
|
232
|
+
if (value === null || value === undefined) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
else if (as === "boolean") {
|
|
236
|
+
return value === true || value === "true";
|
|
237
|
+
}
|
|
238
|
+
else if (as === "timestamp") {
|
|
239
|
+
return immutableQueryValue(value instanceof Date ? value : new Date(String(value)));
|
|
240
|
+
}
|
|
241
|
+
return String(value);
|
|
242
|
+
}
|
|
243
|
+
function aggregate(values, operation) {
|
|
244
|
+
const present = values.filter((value) => value !== null && value !== undefined);
|
|
245
|
+
if (present.length === 0) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const numbers = present.map((value) => BigInt(String(value)));
|
|
249
|
+
if (operation === "sum") {
|
|
250
|
+
return numbers.reduce((total, value) => total + value, 0n).toString();
|
|
251
|
+
}
|
|
252
|
+
return numbers.reduce((winner, value) => operation === "min" ? value < winner ? value : winner : value > winner ? value : winner).toString();
|
|
253
|
+
}
|
|
254
|
+
function cartesian(parts) {
|
|
255
|
+
return parts.reduce((all, part) => all.flatMap((prefix) => part.map((value) => [...prefix, value])), [[]]);
|
|
256
|
+
}
|
|
257
|
+
function keyIdentity(value) {
|
|
258
|
+
return value instanceof Date ? value.toISOString() : value;
|
|
259
|
+
}
|
|
260
|
+
function groupName(key) {
|
|
261
|
+
return key.kind === "field" ? key.path.join("_") : key.kind === "json" ? key.name : key.path.join("_") + "_" + key.bucket;
|
|
262
|
+
}
|
|
263
|
+
function bucket(value, unit) {
|
|
264
|
+
if (value === null || value === undefined) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const date = value instanceof Date ? new Date(value.getTime()) : new Date(String(value));
|
|
268
|
+
if (Number.isNaN(date.getTime())) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
else if (unit === "hour") {
|
|
272
|
+
date.setUTCMinutes(0, 0, 0);
|
|
273
|
+
}
|
|
274
|
+
else if (unit === "day") {
|
|
275
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
276
|
+
}
|
|
277
|
+
else if (unit === "month") {
|
|
278
|
+
date.setUTCDate(1);
|
|
279
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
280
|
+
}
|
|
281
|
+
else {
|
|
282
|
+
const day = date.getUTCDay() || 7;
|
|
283
|
+
date.setUTCDate(date.getUTCDate() - day + 1);
|
|
284
|
+
date.setUTCHours(0, 0, 0, 0);
|
|
285
|
+
}
|
|
286
|
+
return date;
|
|
287
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encodes an arbitrary UTF-8 semantic identity as a positive decimal integer.
|
|
3
|
+
*
|
|
4
|
+
* The leading decimal digit is a sentinel and each byte occupies three decimal
|
|
5
|
+
* digits, so leading NUL bytes and the empty string remain distinct. This is an
|
|
6
|
+
* injective encoding, not a hash.
|
|
7
|
+
*/
|
|
8
|
+
export declare function canonicalPublicNumericIdentity(identity: string): string;
|
|
9
|
+
/** Frames multiple semantic components without a delimiter ambiguity. */
|
|
10
|
+
export declare function canonicalPublicNumericIdentityParts(parts: readonly string[]): string;
|
|
11
|
+
/** PostgreSQL expression exactly equivalent to {@link canonicalPublicNumericIdentity}. */
|
|
12
|
+
export declare function compileCanonicalPublicNumericIdentitySql(text: string): string;
|
|
13
|
+
/** PostgreSQL framing equivalent to {@link canonicalPublicNumericIdentityParts}. */
|
|
14
|
+
export declare function compileCanonicalPublicNumericIdentityPartsSql(parts: readonly string[]): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
const MAX_CANONICAL_PUBLIC_IDENTITY_BYTES = 43_690;
|
|
3
|
+
/**
|
|
4
|
+
* Encodes an arbitrary UTF-8 semantic identity as a positive decimal integer.
|
|
5
|
+
*
|
|
6
|
+
* The leading decimal digit is a sentinel and each byte occupies three decimal
|
|
7
|
+
* digits, so leading NUL bytes and the empty string remain distinct. This is an
|
|
8
|
+
* injective encoding, not a hash.
|
|
9
|
+
*/
|
|
10
|
+
export function canonicalPublicNumericIdentity(identity) {
|
|
11
|
+
const bytes = Buffer.from(identity, "utf8");
|
|
12
|
+
if (bytes.length > MAX_CANONICAL_PUBLIC_IDENTITY_BYTES) {
|
|
13
|
+
throw new RangeError(`Canonical public identity exceeds ${MAX_CANONICAL_PUBLIC_IDENTITY_BYTES} UTF-8 bytes`);
|
|
14
|
+
}
|
|
15
|
+
let value = "1";
|
|
16
|
+
for (const byte of bytes) {
|
|
17
|
+
value += byte.toString().padStart(3, "0");
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
/** Frames multiple semantic components without a delimiter ambiguity. */
|
|
22
|
+
export function canonicalPublicNumericIdentityParts(parts) {
|
|
23
|
+
return canonicalPublicNumericIdentity(parts.map((part) => `${Buffer.byteLength(part, "utf8")}:${part}`).join(""));
|
|
24
|
+
}
|
|
25
|
+
/** PostgreSQL expression exactly equivalent to {@link canonicalPublicNumericIdentity}. */
|
|
26
|
+
export function compileCanonicalPublicNumericIdentitySql(text) {
|
|
27
|
+
const bytes = `convert_to((${text})::text, 'UTF8')`;
|
|
28
|
+
const encoded = `('1' || coalesce((select string_agg(lpad(get_byte(canonical_identity_bytes, canonical_identity_byte)::text, 3, '0'), '' order by canonical_identity_byte) from generate_series(0, octet_length(canonical_identity_bytes) - 1) as canonical_identity_byte), ''))::numeric`;
|
|
29
|
+
const overflow = `('1' || repeat('0', octet_length(canonical_identity_bytes) * 3))::numeric`;
|
|
30
|
+
return `(select case when octet_length(canonical_identity_bytes) <= ${MAX_CANONICAL_PUBLIC_IDENTITY_BYTES} then ${encoded} else ${overflow} end from (select ${bytes} as canonical_identity_bytes) canonical_identity)`;
|
|
31
|
+
}
|
|
32
|
+
/** PostgreSQL framing equivalent to {@link canonicalPublicNumericIdentityParts}. */
|
|
33
|
+
export function compileCanonicalPublicNumericIdentityPartsSql(parts) {
|
|
34
|
+
return `concat(${parts.flatMap((part) => [`octet_length(convert_to((${part})::text, 'UTF8'))`, "':'", `(${part})::text`]).join(", ")})`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { QueryRelation } from "./query-schema.js";
|
|
2
|
+
export type ScalarOperator = "equals" | "in" | "is" | "isNot" | "lt" | "lte" | "gt" | "gte" | "like" | "ilike" | "has";
|
|
3
|
+
export type QueryOrderDirection = "asc" | "desc";
|
|
4
|
+
export type QueryPredicate = {
|
|
5
|
+
readonly kind: "and" | "or";
|
|
6
|
+
readonly children: readonly QueryPredicate[];
|
|
7
|
+
} | {
|
|
8
|
+
readonly kind: "not";
|
|
9
|
+
readonly child: QueryPredicate;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "scalar";
|
|
12
|
+
readonly path: readonly string[];
|
|
13
|
+
readonly operator: ScalarOperator;
|
|
14
|
+
readonly value: unknown;
|
|
15
|
+
} | {
|
|
16
|
+
readonly kind: "relation";
|
|
17
|
+
readonly edge: string;
|
|
18
|
+
readonly quantifier: "one" | "some" | "none" | "every";
|
|
19
|
+
readonly predicate: QueryPredicate;
|
|
20
|
+
};
|
|
21
|
+
export interface NormalizedOrder {
|
|
22
|
+
readonly path: readonly string[];
|
|
23
|
+
readonly direction: QueryOrderDirection;
|
|
24
|
+
}
|
|
25
|
+
export interface NormalizedJsonSelection {
|
|
26
|
+
readonly name: string;
|
|
27
|
+
readonly field: string;
|
|
28
|
+
readonly path: readonly string[];
|
|
29
|
+
readonly as: "text" | "numeric" | "boolean" | "timestamp";
|
|
30
|
+
}
|
|
31
|
+
export interface NormalizedSelection {
|
|
32
|
+
readonly fields: readonly string[];
|
|
33
|
+
readonly json: readonly NormalizedJsonSelection[];
|
|
34
|
+
}
|
|
35
|
+
export interface NormalizedInclude {
|
|
36
|
+
readonly edge: string;
|
|
37
|
+
readonly relation: QueryRelation;
|
|
38
|
+
readonly cardinality: "one" | "many";
|
|
39
|
+
readonly predicate?: QueryPredicate;
|
|
40
|
+
readonly select?: NormalizedSelection;
|
|
41
|
+
readonly includes: readonly NormalizedInclude[];
|
|
42
|
+
readonly orderBy: readonly NormalizedOrder[];
|
|
43
|
+
readonly skip: number;
|
|
44
|
+
readonly take?: number;
|
|
45
|
+
}
|
|
46
|
+
export interface NormalizedFindManyQuery {
|
|
47
|
+
readonly kind: "findMany";
|
|
48
|
+
readonly relation: QueryRelation;
|
|
49
|
+
readonly parties?: readonly string[];
|
|
50
|
+
readonly predicate?: QueryPredicate;
|
|
51
|
+
readonly select?: NormalizedSelection;
|
|
52
|
+
readonly includes: readonly NormalizedInclude[];
|
|
53
|
+
readonly orderBy: readonly NormalizedOrder[];
|
|
54
|
+
readonly skip: number;
|
|
55
|
+
readonly take?: number;
|
|
56
|
+
readonly activeOnly: boolean;
|
|
57
|
+
}
|
|
58
|
+
export interface NormalizedFindUniqueQuery {
|
|
59
|
+
readonly kind: "findUnique";
|
|
60
|
+
readonly relation: QueryRelation;
|
|
61
|
+
readonly predicate: QueryPredicate;
|
|
62
|
+
readonly select?: NormalizedSelection;
|
|
63
|
+
readonly includes: readonly NormalizedInclude[];
|
|
64
|
+
}
|
|
65
|
+
export interface NormalizedCountQuery {
|
|
66
|
+
readonly kind: "count";
|
|
67
|
+
readonly relation: QueryRelation;
|
|
68
|
+
readonly parties?: readonly string[];
|
|
69
|
+
readonly predicate?: QueryPredicate;
|
|
70
|
+
readonly activeOnly: boolean;
|
|
71
|
+
}
|
|
72
|
+
export interface NormalizedAggregateSelection {
|
|
73
|
+
readonly count: boolean;
|
|
74
|
+
readonly min: readonly string[];
|
|
75
|
+
readonly max: readonly string[];
|
|
76
|
+
readonly sum: readonly string[];
|
|
77
|
+
}
|
|
78
|
+
export interface NormalizedAggregateQuery {
|
|
79
|
+
readonly kind: "aggregate";
|
|
80
|
+
readonly relation: QueryRelation;
|
|
81
|
+
readonly predicate?: QueryPredicate;
|
|
82
|
+
readonly aggregates: NormalizedAggregateSelection;
|
|
83
|
+
}
|
|
84
|
+
export type NormalizedGroupKey = {
|
|
85
|
+
readonly kind: "field";
|
|
86
|
+
readonly path: readonly string[];
|
|
87
|
+
} | {
|
|
88
|
+
readonly kind: "json";
|
|
89
|
+
readonly field: string;
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly path: readonly string[];
|
|
92
|
+
readonly as: "text" | "numeric" | "boolean" | "timestamp";
|
|
93
|
+
} | {
|
|
94
|
+
readonly kind: "bucket";
|
|
95
|
+
readonly path: readonly string[];
|
|
96
|
+
readonly bucket: "hour" | "day" | "week" | "month";
|
|
97
|
+
};
|
|
98
|
+
export interface NormalizedGroupByQuery {
|
|
99
|
+
readonly kind: "groupBy";
|
|
100
|
+
readonly relation: QueryRelation;
|
|
101
|
+
readonly predicate?: QueryPredicate;
|
|
102
|
+
readonly by: readonly NormalizedGroupKey[];
|
|
103
|
+
readonly aggregates: NormalizedAggregateSelection;
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type QueryRelation } from "./query-schema.js";
|
|
2
|
+
/** Canonical rows are deliberately source-neutral; public ids are semantic values. */
|
|
3
|
+
export type QueryRow = Readonly<Record<string, unknown>>;
|
|
4
|
+
export type QueryRowSets = Readonly<Record<QueryRelation, readonly QueryRow[]>>;
|
|
5
|
+
/** A directional join from the owning row to a target relation row. */
|
|
6
|
+
export interface QueryEdgeLookup {
|
|
7
|
+
/** Whether the snapshot contains every target needed to traverse this edge. Defaults to true. */
|
|
8
|
+
readonly complete?: boolean;
|
|
9
|
+
/** Public row paths, retained for legacy datasets. */
|
|
10
|
+
readonly from?: readonly string[];
|
|
11
|
+
readonly to?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Snapshot-local join values aligned by source/target row position. They are
|
|
14
|
+
* deliberately stored beside the rows rather than attached to them so query
|
|
15
|
+
* results expose only schema fields.
|
|
16
|
+
*/
|
|
17
|
+
readonly privateKeys?: Readonly<{
|
|
18
|
+
readonly source: readonly (readonly unknown[])[];
|
|
19
|
+
readonly target: readonly (readonly unknown[])[];
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export type QueryEdgeLookups = Readonly<Partial<Record<QueryRelation, Readonly<Record<string, QueryEdgeLookup>>>>>;
|
|
23
|
+
/**
|
|
24
|
+
* A deterministic, in-memory canonical data source. `uniqueKeys` documents
|
|
25
|
+
* public row uniqueness only; the evaluator never gives those fields special semantics.
|
|
26
|
+
*/
|
|
27
|
+
export interface QueryDataset {
|
|
28
|
+
readonly rows: QueryRowSets;
|
|
29
|
+
readonly edges: QueryEdgeLookups;
|
|
30
|
+
readonly uniqueKeys: Readonly<Record<QueryRelation, readonly (readonly string[])[]>>;
|
|
31
|
+
}
|
|
32
|
+
/** An edge is declared by the schema but its targets were unavailable in this snapshot. */
|
|
33
|
+
export declare class IncompleteQueryEdgeError extends Error {
|
|
34
|
+
readonly relation: QueryRelation;
|
|
35
|
+
readonly edge: string;
|
|
36
|
+
constructor(relation: QueryRelation, edge: string);
|
|
37
|
+
}
|
|
38
|
+
/** Validates and freezes source-neutral rows, then builds deterministic edge indexes once. */
|
|
39
|
+
export declare function createQueryDataset(input: QueryDataset): QueryDataset;
|
|
40
|
+
/** Uses the factory index, compiling a legacy dataset on first use for compatibility. */
|
|
41
|
+
export declare function relatedQueryRows(dataset: QueryDataset, relation: QueryRelation, row: QueryRow, edge: string): readonly QueryRow[];
|
|
42
|
+
/** A Date which preserves Date's public read API but rejects mutation. */
|
|
43
|
+
export declare class ImmutableQueryDate extends Date {
|
|
44
|
+
/** Legacy Date API, absent from newer TypeScript lib declarations. */
|
|
45
|
+
setYear(): never;
|
|
46
|
+
setTime(): never;
|
|
47
|
+
setMilliseconds(): never;
|
|
48
|
+
setSeconds(): never;
|
|
49
|
+
setMinutes(): never;
|
|
50
|
+
setHours(): never;
|
|
51
|
+
setDate(): never;
|
|
52
|
+
setMonth(): never;
|
|
53
|
+
setFullYear(): never;
|
|
54
|
+
setUTCMilliseconds(): never;
|
|
55
|
+
setUTCSeconds(): never;
|
|
56
|
+
setUTCMinutes(): never;
|
|
57
|
+
setUTCHours(): never;
|
|
58
|
+
setUTCDate(): never;
|
|
59
|
+
setUTCMonth(): never;
|
|
60
|
+
setUTCFullYear(): never;
|
|
61
|
+
}
|
|
62
|
+
/** Recursively clones and freezes query values without retaining mutable input references. */
|
|
63
|
+
export declare function immutableQueryValue<T>(value: T): T;
|