@distrohelena/canton-typescript-sdk 1.0.5 → 1.0.7
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 +20 -0
- package/dist/cjs/daml-interface/runtime/daml-value-converter.js +20 -7
- package/dist/cjs/query/canonical/in-memory-query-evaluator.js +10 -4
- package/dist/cjs/query/canonical/query-normalizer.js +26 -4
- package/dist/cjs/query/canonical/query-timestamp.js +29 -0
- package/dist/cjs/query/pqs/pqs-sql-compiler.js +4 -2
- package/dist/daml-interface/runtime/daml-value-converter.js +20 -7
- package/dist/query/canonical/in-memory-query-evaluator.js +10 -4
- package/dist/query/canonical/query-ast.d.ts +1 -0
- package/dist/query/canonical/query-normalizer.js +26 -4
- package/dist/query/canonical/query-timestamp.d.ts +2 -0
- package/dist/query/canonical/query-timestamp.js +26 -0
- package/dist/query/model-types.d.ts +5 -1
- package/dist/query/pqs/pqs-sql-compiler.js +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -839,6 +839,26 @@ uses `[{ createdAt: "desc" }, { contractId: "asc" }]`. `exercises` intentionally
|
|
|
839
839
|
because the v1 PQS profile does not declare a stable key. The manager validates
|
|
840
840
|
the selected PQS schema profile before its first PQS query.
|
|
841
841
|
|
|
842
|
+
Payload and JSON-path filters accept `as: "timestamp"` to compare ISO timestamps
|
|
843
|
+
at microsecond precision in both PQS and memory:
|
|
844
|
+
|
|
845
|
+
```typescript
|
|
846
|
+
where: {
|
|
847
|
+
payload: {
|
|
848
|
+
match: {
|
|
849
|
+
expiresAt: { as: "timestamp", gt: "2026-09-09T12:00:00.000001Z" },
|
|
850
|
+
},
|
|
851
|
+
},
|
|
852
|
+
}
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
For relational JSON fields, use `{ path: ["expiresAt"], as: "timestamp", gt: value }`.
|
|
856
|
+
Values require a timezone and at most six fractional digits. Equivalent instants
|
|
857
|
+
compare equally regardless of timezone offset or fractional padding. Omit `as`
|
|
858
|
+
or use `as: "text"` for string comparisons; `like` and `ilike` require text.
|
|
859
|
+
This filter option does not change timestamp projections or lifecycle fields
|
|
860
|
+
represented as JavaScript `Date`, which retain millisecond precision.
|
|
861
|
+
|
|
842
862
|
- Ledger endpoint:
|
|
843
863
|
- `versionService.getLedgerApiVersionAsync(...)`: `json`, `grpc`
|
|
844
864
|
- `healthService.checkAsync(...)`: `grpc` only
|
|
@@ -120,20 +120,24 @@ function decodeProtobufRecord(record, descriptor, registry, path) {
|
|
|
120
120
|
for (const field of descriptor.fields) {
|
|
121
121
|
const sourceField = fields.find((candidate) => candidate.label === field.damlLabel);
|
|
122
122
|
if (sourceField === undefined) {
|
|
123
|
-
|
|
123
|
+
output[field.propertyName] = decodeMissingRecordField(field, fieldPath(path, field.propertyName));
|
|
124
|
+
continue;
|
|
124
125
|
}
|
|
125
126
|
output[field.propertyName] = decodeRequiredProtobufValue(sourceField.value, field.type, registry, fieldPath(path, field.propertyName));
|
|
126
127
|
}
|
|
127
|
-
if (fields.
|
|
128
|
+
if (fields.some((field) => !descriptor.fields.some((descriptorField) => descriptorField.damlLabel === field.label))) {
|
|
128
129
|
throw materializationError(path, "record contains an unexpected field");
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
else {
|
|
132
|
-
if (fields.length
|
|
133
|
+
if (fields.length > descriptor.fields.length) {
|
|
133
134
|
throw materializationError(path, "record has the wrong number of positional fields");
|
|
134
135
|
}
|
|
135
136
|
descriptor.fields.forEach((field, index) => {
|
|
136
|
-
|
|
137
|
+
const sourceField = fields[index];
|
|
138
|
+
output[field.propertyName] = sourceField === undefined
|
|
139
|
+
? decodeMissingRecordField(field, fieldPath(path, field.propertyName))
|
|
140
|
+
: decodeRequiredProtobufValue(sourceField.value, field.type, registry, fieldPath(path, field.propertyName));
|
|
137
141
|
});
|
|
138
142
|
}
|
|
139
143
|
return new daml_values_js_1.DamlRecord(output);
|
|
@@ -212,11 +216,13 @@ function decodeJsonTextMap(value, descriptor, registry, path) {
|
|
|
212
216
|
function decodeJsonRecord(value, descriptor, registry, path) {
|
|
213
217
|
const output = Object.create(null);
|
|
214
218
|
if (Array.isArray(value)) {
|
|
215
|
-
if (value.length
|
|
219
|
+
if (value.length > descriptor.fields.length) {
|
|
216
220
|
throw materializationError(path, "record has the wrong number of positional fields");
|
|
217
221
|
}
|
|
218
222
|
descriptor.fields.forEach((field, index) => {
|
|
219
|
-
output[field.propertyName] =
|
|
223
|
+
output[field.propertyName] = index >= value.length
|
|
224
|
+
? decodeMissingRecordField(field, fieldPath(path, field.propertyName))
|
|
225
|
+
: decodeDamlValue({ kind: "json", value: value[index] }, field.type, registry, fieldPath(path, field.propertyName));
|
|
220
226
|
});
|
|
221
227
|
return new daml_values_js_1.DamlRecord(output);
|
|
222
228
|
}
|
|
@@ -229,12 +235,19 @@ function decodeJsonRecord(value, descriptor, registry, path) {
|
|
|
229
235
|
}
|
|
230
236
|
for (const field of descriptor.fields) {
|
|
231
237
|
if (!Object.hasOwn(record, field.damlLabel)) {
|
|
232
|
-
|
|
238
|
+
output[field.propertyName] = decodeMissingRecordField(field, fieldPath(path, field.propertyName));
|
|
239
|
+
continue;
|
|
233
240
|
}
|
|
234
241
|
output[field.propertyName] = decodeDamlValue({ kind: "json", value: Reflect.get(record, field.damlLabel) }, field.type, registry, fieldPath(path, field.propertyName));
|
|
235
242
|
}
|
|
236
243
|
return new daml_values_js_1.DamlRecord(output);
|
|
237
244
|
}
|
|
245
|
+
function decodeMissingRecordField(field, path) {
|
|
246
|
+
if (field.type.kind === "optional") {
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
throw materializationError(path, "required record field is absent");
|
|
250
|
+
}
|
|
238
251
|
function decodeJsonVariant(value, descriptor, registry, path) {
|
|
239
252
|
const envelope = requireObject(value, path, "variant");
|
|
240
253
|
const tag = requireString(Reflect.get(envelope, "tag"), path, "variant tag");
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.InMemoryQueryEvaluator = void 0;
|
|
4
4
|
const query_dataset_js_1 = require("./query-dataset.js");
|
|
5
5
|
const query_schema_js_1 = require("./query-schema.js");
|
|
6
|
+
const query_timestamp_js_1 = require("./query-timestamp.js");
|
|
6
7
|
/** Reference evaluator for normalized canonical plans; it intentionally has no transport concerns. */
|
|
7
8
|
class InMemoryQueryEvaluator {
|
|
8
9
|
execute(dataset, query) {
|
|
@@ -40,8 +41,10 @@ class InMemoryQueryEvaluator {
|
|
|
40
41
|
return predicate.quantifier === "one" ? result[0] === true : predicate.quantifier === "some" ? result.some(Boolean) : predicate.quantifier === "none" ? !result.some(Boolean) : result.every(Boolean);
|
|
41
42
|
}
|
|
42
43
|
case "scalar": {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
44
|
+
const raw = at(row, predicate.path);
|
|
45
|
+
const convert = (value) => predicate.as === "timestamp" && value !== null && value !== undefined ? (0, query_timestamp_js_1.timestampMicroseconds)(value) : value;
|
|
46
|
+
const actual = predicate.operator === "is" || predicate.operator === "isNot" ? raw : convert(raw);
|
|
47
|
+
const expected = Array.isArray(predicate.value) ? predicate.value.map(convert) : convert(predicate.value);
|
|
45
48
|
switch (predicate.operator) {
|
|
46
49
|
case "equals": return actual !== null && actual !== undefined && expected !== null && expected !== undefined && equal(actual, expected);
|
|
47
50
|
case "in": return actual !== null && actual !== undefined && Array.isArray(expected) && expected.some((candidate) => candidate !== null && candidate !== undefined && equal(actual, candidate));
|
|
@@ -171,7 +174,10 @@ function freeze(value) {
|
|
|
171
174
|
return Object.freeze(value);
|
|
172
175
|
}
|
|
173
176
|
function equal(left, right) {
|
|
174
|
-
if (left
|
|
177
|
+
if (typeof left === "bigint" || typeof right === "bigint") {
|
|
178
|
+
return left === right;
|
|
179
|
+
}
|
|
180
|
+
else if (left instanceof Date) {
|
|
175
181
|
return left.toISOString() === (right instanceof Date ? right.toISOString() : String(right));
|
|
176
182
|
}
|
|
177
183
|
else if (right instanceof Date) {
|
|
@@ -196,7 +202,7 @@ function compare(left, right) {
|
|
|
196
202
|
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
|
|
197
203
|
}
|
|
198
204
|
function numeric(value) {
|
|
199
|
-
return typeof value === "string" && /^-?\d+$/.test(value) ? BigInt(value) : typeof value === "number" && Number.isFinite(value) ? BigInt(value) : undefined;
|
|
205
|
+
return typeof value === "bigint" ? value : typeof value === "string" && /^-?\d+$/.test(value) ? BigInt(value) : typeof value === "number" && Number.isFinite(value) ? BigInt(value) : undefined;
|
|
200
206
|
}
|
|
201
207
|
function postgresCompare(left, right, direction) {
|
|
202
208
|
const leftNull = left === null || left === undefined;
|
|
@@ -7,6 +7,7 @@ exports.normalizeCount = normalizeCount;
|
|
|
7
7
|
exports.normalizeAggregate = normalizeAggregate;
|
|
8
8
|
exports.normalizeGroupBy = normalizeGroupBy;
|
|
9
9
|
const model_types_js_1 = require("../model-types.js");
|
|
10
|
+
const query_timestamp_js_1 = require("./query-timestamp.js");
|
|
10
11
|
const query_schema_js_1 = require("./query-schema.js");
|
|
11
12
|
const scalarOperators = new Set([
|
|
12
13
|
"equals", "in", "is", "isNot", "lt", "lte", "gt", "gte", "like", "ilike", "has",
|
|
@@ -236,7 +237,8 @@ function normalizeScalarFilter(relation, field, value) {
|
|
|
236
237
|
function normalizeJsonFilter(field, value) {
|
|
237
238
|
const filter = object(value, `${field} JSON filter`);
|
|
238
239
|
const path = jsonPath(filter.path, `${field}.path`);
|
|
239
|
-
const
|
|
240
|
+
const as = normalizeTimestampFilterType(filter);
|
|
241
|
+
const entries = Object.entries(filter).filter(([operator]) => operator !== "path" && operator !== "as");
|
|
240
242
|
if (entries.length === 0) {
|
|
241
243
|
throw new Error(`${field} JSON filter must contain an operator`);
|
|
242
244
|
}
|
|
@@ -245,11 +247,14 @@ function normalizeJsonFilter(field, value) {
|
|
|
245
247
|
throw new Error(`${operator} is not supported for ${field}`);
|
|
246
248
|
}
|
|
247
249
|
validateOperatorValue(operator, operand, field, "string", true);
|
|
248
|
-
|
|
250
|
+
if (as !== undefined)
|
|
251
|
+
validateTimestampOperand(operator, operand);
|
|
252
|
+
return { kind: "scalar", path: [field, ...path], operator: operator, value: operand, ...(as === undefined ? {} : { as }) };
|
|
249
253
|
});
|
|
250
254
|
}
|
|
251
255
|
function normalizePayloadScalarFilter(path, value) {
|
|
252
|
-
const
|
|
256
|
+
const as = normalizeTimestampFilterType(value);
|
|
257
|
+
const entries = Object.entries(value).filter(([operator]) => operator !== "as");
|
|
253
258
|
if (entries.length === 0) {
|
|
254
259
|
throw new Error("payload filter must not be empty");
|
|
255
260
|
}
|
|
@@ -258,9 +263,26 @@ function normalizePayloadScalarFilter(path, value) {
|
|
|
258
263
|
throw new Error(`${operator} is not supported for payload`);
|
|
259
264
|
}
|
|
260
265
|
validateOperatorValue(operator, operand, "payload", "string", false);
|
|
261
|
-
|
|
266
|
+
if (as !== undefined)
|
|
267
|
+
validateTimestampOperand(operator, operand);
|
|
268
|
+
return { kind: "scalar", path, operator: operator, value: operand, ...(as === undefined ? {} : { as }) };
|
|
262
269
|
});
|
|
263
270
|
}
|
|
271
|
+
function normalizeTimestampFilterType(filter) {
|
|
272
|
+
if (filter.as !== undefined && filter.as !== "text" && filter.as !== "timestamp") {
|
|
273
|
+
throw new Error("JSON filter as must be text or timestamp");
|
|
274
|
+
}
|
|
275
|
+
return filter.as === "timestamp" ? "timestamp" : undefined;
|
|
276
|
+
}
|
|
277
|
+
function validateTimestampOperand(operator, value) {
|
|
278
|
+
if (operator === "like" || operator === "ilike") {
|
|
279
|
+
throw new Error(`${operator} is not supported for timestamp filters`);
|
|
280
|
+
}
|
|
281
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
282
|
+
if (entry !== null)
|
|
283
|
+
(0, query_timestamp_js_1.timestampMicroseconds)(entry);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
264
286
|
function normalizeTemplateScalarFilter(field, value) {
|
|
265
287
|
if (!isFilter(value)) {
|
|
266
288
|
throw new Error(`templateId.${field} must be a filter object`);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.timestampMicroseconds = timestampMicroseconds;
|
|
4
|
+
/** Parse an explicit ISO timestamp without passing its fractional seconds through Date. */
|
|
5
|
+
function timestampMicroseconds(value) {
|
|
6
|
+
const match = typeof value === "string"
|
|
7
|
+
? /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.(\d{1,6}))?(Z|[+-]\d{2}:\d{2})$/.exec(value)
|
|
8
|
+
: null;
|
|
9
|
+
if (match === null) {
|
|
10
|
+
throw new Error("Invalid timestamp: expected ISO date-time with a timezone and at most six fractional digits");
|
|
11
|
+
}
|
|
12
|
+
const base = `${match[1]}T${match[2]}`;
|
|
13
|
+
const milliseconds = Date.parse(`${base}Z`);
|
|
14
|
+
if (!Number.isFinite(milliseconds) || new Date(milliseconds).toISOString().slice(0, 19) !== base) {
|
|
15
|
+
throw new Error("Invalid timestamp calendar date or time");
|
|
16
|
+
}
|
|
17
|
+
const zone = match[4];
|
|
18
|
+
const hours = zone === "Z" ? 0 : Number(zone.slice(1, 3));
|
|
19
|
+
const minutes = zone === "Z" ? 0 : Number(zone.slice(4, 6));
|
|
20
|
+
if (hours > 15 || minutes > 59) {
|
|
21
|
+
throw new Error("Invalid timestamp timezone offset");
|
|
22
|
+
}
|
|
23
|
+
const offset = BigInt((hours * 60 + minutes) * (zone[0] === "-" ? -1 : 1));
|
|
24
|
+
const micros = BigInt(milliseconds) * 1000n + BigInt((match[3] ?? "").padEnd(6, "0")) - offset * 60000000n;
|
|
25
|
+
if (micros < -62135596800000000n || micros > 253402300799999999n) {
|
|
26
|
+
throw new Error("Timestamp is outside the DAML ledger range");
|
|
27
|
+
}
|
|
28
|
+
return micros;
|
|
29
|
+
}
|
|
@@ -257,7 +257,9 @@ function compileCanonicalPredicate(relation, predicate, alias, profile, add, log
|
|
|
257
257
|
? `${compileCanonicalPhysicalField(relation, field, alias, profile)} #>> ${add(path)}::text[]`
|
|
258
258
|
: compileCanonicalPhysicalField(relation, field, alias, profile);
|
|
259
259
|
const inverseEventType = relation === "__events" && field === "type" && (predicate.operator === "equals" || predicate.operator === "in");
|
|
260
|
-
const expression =
|
|
260
|
+
const expression = predicate.as === "timestamp" && predicate.operator !== "is" && predicate.operator !== "isNot"
|
|
261
|
+
? `(${canonicalExpression})::timestamptz`
|
|
262
|
+
: inverseEventType ? qualified(alias, column) : canonicalExpression;
|
|
261
263
|
const value = inverseEventType ? physicalEventType(predicate.value) : predicate.value;
|
|
262
264
|
const sql = { equals: "=", lt: "<", lte: "<=", gt: ">", gte: ">=", like: "like", ilike: "ilike" }[predicate.operator];
|
|
263
265
|
if (predicate.operator === "is")
|
|
@@ -265,7 +267,7 @@ function compileCanonicalPredicate(relation, predicate, alias, profile, add, log
|
|
|
265
267
|
if (predicate.operator === "isNot")
|
|
266
268
|
return `${expression} is not null`;
|
|
267
269
|
if (predicate.operator === "in")
|
|
268
|
-
return value.length === 0 ? "false" : `${expression} = any(${add(value)})`;
|
|
270
|
+
return value.length === 0 ? "false" : `${expression} = any(${add(value)}${predicate.as === "timestamp" ? "::timestamptz[]" : ""})`;
|
|
269
271
|
if (predicate.operator === "has")
|
|
270
272
|
return `${add(value)} = any(${qualified(alias, column)})`;
|
|
271
273
|
if (sql === undefined)
|
|
@@ -116,20 +116,24 @@ function decodeProtobufRecord(record, descriptor, registry, path) {
|
|
|
116
116
|
for (const field of descriptor.fields) {
|
|
117
117
|
const sourceField = fields.find((candidate) => candidate.label === field.damlLabel);
|
|
118
118
|
if (sourceField === undefined) {
|
|
119
|
-
|
|
119
|
+
output[field.propertyName] = decodeMissingRecordField(field, fieldPath(path, field.propertyName));
|
|
120
|
+
continue;
|
|
120
121
|
}
|
|
121
122
|
output[field.propertyName] = decodeRequiredProtobufValue(sourceField.value, field.type, registry, fieldPath(path, field.propertyName));
|
|
122
123
|
}
|
|
123
|
-
if (fields.
|
|
124
|
+
if (fields.some((field) => !descriptor.fields.some((descriptorField) => descriptorField.damlLabel === field.label))) {
|
|
124
125
|
throw materializationError(path, "record contains an unexpected field");
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
else {
|
|
128
|
-
if (fields.length
|
|
129
|
+
if (fields.length > descriptor.fields.length) {
|
|
129
130
|
throw materializationError(path, "record has the wrong number of positional fields");
|
|
130
131
|
}
|
|
131
132
|
descriptor.fields.forEach((field, index) => {
|
|
132
|
-
|
|
133
|
+
const sourceField = fields[index];
|
|
134
|
+
output[field.propertyName] = sourceField === undefined
|
|
135
|
+
? decodeMissingRecordField(field, fieldPath(path, field.propertyName))
|
|
136
|
+
: decodeRequiredProtobufValue(sourceField.value, field.type, registry, fieldPath(path, field.propertyName));
|
|
133
137
|
});
|
|
134
138
|
}
|
|
135
139
|
return new DamlRecord(output);
|
|
@@ -208,11 +212,13 @@ function decodeJsonTextMap(value, descriptor, registry, path) {
|
|
|
208
212
|
function decodeJsonRecord(value, descriptor, registry, path) {
|
|
209
213
|
const output = Object.create(null);
|
|
210
214
|
if (Array.isArray(value)) {
|
|
211
|
-
if (value.length
|
|
215
|
+
if (value.length > descriptor.fields.length) {
|
|
212
216
|
throw materializationError(path, "record has the wrong number of positional fields");
|
|
213
217
|
}
|
|
214
218
|
descriptor.fields.forEach((field, index) => {
|
|
215
|
-
output[field.propertyName] =
|
|
219
|
+
output[field.propertyName] = index >= value.length
|
|
220
|
+
? decodeMissingRecordField(field, fieldPath(path, field.propertyName))
|
|
221
|
+
: decodeDamlValue({ kind: "json", value: value[index] }, field.type, registry, fieldPath(path, field.propertyName));
|
|
216
222
|
});
|
|
217
223
|
return new DamlRecord(output);
|
|
218
224
|
}
|
|
@@ -225,12 +231,19 @@ function decodeJsonRecord(value, descriptor, registry, path) {
|
|
|
225
231
|
}
|
|
226
232
|
for (const field of descriptor.fields) {
|
|
227
233
|
if (!Object.hasOwn(record, field.damlLabel)) {
|
|
228
|
-
|
|
234
|
+
output[field.propertyName] = decodeMissingRecordField(field, fieldPath(path, field.propertyName));
|
|
235
|
+
continue;
|
|
229
236
|
}
|
|
230
237
|
output[field.propertyName] = decodeDamlValue({ kind: "json", value: Reflect.get(record, field.damlLabel) }, field.type, registry, fieldPath(path, field.propertyName));
|
|
231
238
|
}
|
|
232
239
|
return new DamlRecord(output);
|
|
233
240
|
}
|
|
241
|
+
function decodeMissingRecordField(field, path) {
|
|
242
|
+
if (field.type.kind === "optional") {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
throw materializationError(path, "required record field is absent");
|
|
246
|
+
}
|
|
234
247
|
function decodeJsonVariant(value, descriptor, registry, path) {
|
|
235
248
|
const envelope = requireObject(value, path, "variant");
|
|
236
249
|
const tag = requireString(Reflect.get(envelope, "tag"), path, "variant tag");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createQueryDataset, immutableQueryValue, relatedQueryRows } from "./query-dataset.js";
|
|
2
2
|
import { queryRelationEdges, queryRelationMetadata } from "./query-schema.js";
|
|
3
|
+
import { timestampMicroseconds } from "./query-timestamp.js";
|
|
3
4
|
/** Reference evaluator for normalized canonical plans; it intentionally has no transport concerns. */
|
|
4
5
|
export class InMemoryQueryEvaluator {
|
|
5
6
|
execute(dataset, query) {
|
|
@@ -37,8 +38,10 @@ export class InMemoryQueryEvaluator {
|
|
|
37
38
|
return predicate.quantifier === "one" ? result[0] === true : predicate.quantifier === "some" ? result.some(Boolean) : predicate.quantifier === "none" ? !result.some(Boolean) : result.every(Boolean);
|
|
38
39
|
}
|
|
39
40
|
case "scalar": {
|
|
40
|
-
const
|
|
41
|
-
const
|
|
41
|
+
const raw = at(row, predicate.path);
|
|
42
|
+
const convert = (value) => predicate.as === "timestamp" && value !== null && value !== undefined ? timestampMicroseconds(value) : value;
|
|
43
|
+
const actual = predicate.operator === "is" || predicate.operator === "isNot" ? raw : convert(raw);
|
|
44
|
+
const expected = Array.isArray(predicate.value) ? predicate.value.map(convert) : convert(predicate.value);
|
|
42
45
|
switch (predicate.operator) {
|
|
43
46
|
case "equals": return actual !== null && actual !== undefined && expected !== null && expected !== undefined && equal(actual, expected);
|
|
44
47
|
case "in": return actual !== null && actual !== undefined && Array.isArray(expected) && expected.some((candidate) => candidate !== null && candidate !== undefined && equal(actual, candidate));
|
|
@@ -167,7 +170,10 @@ function freeze(value) {
|
|
|
167
170
|
return Object.freeze(value);
|
|
168
171
|
}
|
|
169
172
|
function equal(left, right) {
|
|
170
|
-
if (left
|
|
173
|
+
if (typeof left === "bigint" || typeof right === "bigint") {
|
|
174
|
+
return left === right;
|
|
175
|
+
}
|
|
176
|
+
else if (left instanceof Date) {
|
|
171
177
|
return left.toISOString() === (right instanceof Date ? right.toISOString() : String(right));
|
|
172
178
|
}
|
|
173
179
|
else if (right instanceof Date) {
|
|
@@ -192,7 +198,7 @@ function compare(left, right) {
|
|
|
192
198
|
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
|
|
193
199
|
}
|
|
194
200
|
function numeric(value) {
|
|
195
|
-
return typeof value === "string" && /^-?\d+$/.test(value) ? BigInt(value) : typeof value === "number" && Number.isFinite(value) ? BigInt(value) : undefined;
|
|
201
|
+
return typeof value === "bigint" ? value : typeof value === "string" && /^-?\d+$/.test(value) ? BigInt(value) : typeof value === "number" && Number.isFinite(value) ? BigInt(value) : undefined;
|
|
196
202
|
}
|
|
197
203
|
function postgresCompare(left, right, direction) {
|
|
198
204
|
const leftNull = left === null || left === undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { assertQueryOrderBy, assertQueryPageArgs } from "../model-types.js";
|
|
2
|
+
import { timestampMicroseconds } from "./query-timestamp.js";
|
|
2
3
|
import { queryRelationEdges, queryRelationMetadata, } from "./query-schema.js";
|
|
3
4
|
const scalarOperators = new Set([
|
|
4
5
|
"equals", "in", "is", "isNot", "lt", "lte", "gt", "gte", "like", "ilike", "has",
|
|
@@ -227,7 +228,8 @@ function normalizeScalarFilter(relation, field, value) {
|
|
|
227
228
|
function normalizeJsonFilter(field, value) {
|
|
228
229
|
const filter = object(value, `${field} JSON filter`);
|
|
229
230
|
const path = jsonPath(filter.path, `${field}.path`);
|
|
230
|
-
const
|
|
231
|
+
const as = normalizeTimestampFilterType(filter);
|
|
232
|
+
const entries = Object.entries(filter).filter(([operator]) => operator !== "path" && operator !== "as");
|
|
231
233
|
if (entries.length === 0) {
|
|
232
234
|
throw new Error(`${field} JSON filter must contain an operator`);
|
|
233
235
|
}
|
|
@@ -236,11 +238,14 @@ function normalizeJsonFilter(field, value) {
|
|
|
236
238
|
throw new Error(`${operator} is not supported for ${field}`);
|
|
237
239
|
}
|
|
238
240
|
validateOperatorValue(operator, operand, field, "string", true);
|
|
239
|
-
|
|
241
|
+
if (as !== undefined)
|
|
242
|
+
validateTimestampOperand(operator, operand);
|
|
243
|
+
return { kind: "scalar", path: [field, ...path], operator: operator, value: operand, ...(as === undefined ? {} : { as }) };
|
|
240
244
|
});
|
|
241
245
|
}
|
|
242
246
|
function normalizePayloadScalarFilter(path, value) {
|
|
243
|
-
const
|
|
247
|
+
const as = normalizeTimestampFilterType(value);
|
|
248
|
+
const entries = Object.entries(value).filter(([operator]) => operator !== "as");
|
|
244
249
|
if (entries.length === 0) {
|
|
245
250
|
throw new Error("payload filter must not be empty");
|
|
246
251
|
}
|
|
@@ -249,9 +254,26 @@ function normalizePayloadScalarFilter(path, value) {
|
|
|
249
254
|
throw new Error(`${operator} is not supported for payload`);
|
|
250
255
|
}
|
|
251
256
|
validateOperatorValue(operator, operand, "payload", "string", false);
|
|
252
|
-
|
|
257
|
+
if (as !== undefined)
|
|
258
|
+
validateTimestampOperand(operator, operand);
|
|
259
|
+
return { kind: "scalar", path, operator: operator, value: operand, ...(as === undefined ? {} : { as }) };
|
|
253
260
|
});
|
|
254
261
|
}
|
|
262
|
+
function normalizeTimestampFilterType(filter) {
|
|
263
|
+
if (filter.as !== undefined && filter.as !== "text" && filter.as !== "timestamp") {
|
|
264
|
+
throw new Error("JSON filter as must be text or timestamp");
|
|
265
|
+
}
|
|
266
|
+
return filter.as === "timestamp" ? "timestamp" : undefined;
|
|
267
|
+
}
|
|
268
|
+
function validateTimestampOperand(operator, value) {
|
|
269
|
+
if (operator === "like" || operator === "ilike") {
|
|
270
|
+
throw new Error(`${operator} is not supported for timestamp filters`);
|
|
271
|
+
}
|
|
272
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
273
|
+
if (entry !== null)
|
|
274
|
+
timestampMicroseconds(entry);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
255
277
|
function normalizeTemplateScalarFilter(field, value) {
|
|
256
278
|
if (!isFilter(value)) {
|
|
257
279
|
throw new Error(`templateId.${field} must be a filter object`);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Parse an explicit ISO timestamp without passing its fractional seconds through Date. */
|
|
2
|
+
export function timestampMicroseconds(value) {
|
|
3
|
+
const match = typeof value === "string"
|
|
4
|
+
? /^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})(?:\.(\d{1,6}))?(Z|[+-]\d{2}:\d{2})$/.exec(value)
|
|
5
|
+
: null;
|
|
6
|
+
if (match === null) {
|
|
7
|
+
throw new Error("Invalid timestamp: expected ISO date-time with a timezone and at most six fractional digits");
|
|
8
|
+
}
|
|
9
|
+
const base = `${match[1]}T${match[2]}`;
|
|
10
|
+
const milliseconds = Date.parse(`${base}Z`);
|
|
11
|
+
if (!Number.isFinite(milliseconds) || new Date(milliseconds).toISOString().slice(0, 19) !== base) {
|
|
12
|
+
throw new Error("Invalid timestamp calendar date or time");
|
|
13
|
+
}
|
|
14
|
+
const zone = match[4];
|
|
15
|
+
const hours = zone === "Z" ? 0 : Number(zone.slice(1, 3));
|
|
16
|
+
const minutes = zone === "Z" ? 0 : Number(zone.slice(4, 6));
|
|
17
|
+
if (hours > 15 || minutes > 59) {
|
|
18
|
+
throw new Error("Invalid timestamp timezone offset");
|
|
19
|
+
}
|
|
20
|
+
const offset = BigInt((hours * 60 + minutes) * (zone[0] === "-" ? -1 : 1));
|
|
21
|
+
const micros = BigInt(milliseconds) * 1000n + BigInt((match[3] ?? "").padEnd(6, "0")) - offset * 60000000n;
|
|
22
|
+
if (micros < -62135596800000000n || micros > 253402300799999999n) {
|
|
23
|
+
throw new Error("Timestamp is outside the DAML ledger range");
|
|
24
|
+
}
|
|
25
|
+
return micros;
|
|
26
|
+
}
|
|
@@ -20,6 +20,8 @@ export interface ArrayMembershipFilter {
|
|
|
20
20
|
}
|
|
21
21
|
export interface JsonPathFilter {
|
|
22
22
|
readonly path: readonly [string, ...readonly string[]];
|
|
23
|
+
/** Compare ISO timestamps at microsecond precision; omitted means text. */
|
|
24
|
+
readonly as?: "text" | "timestamp";
|
|
23
25
|
readonly equals?: string;
|
|
24
26
|
readonly in?: readonly string[];
|
|
25
27
|
readonly lt?: string;
|
|
@@ -138,6 +140,8 @@ export interface TemplateId {
|
|
|
138
140
|
readonly entityName: string;
|
|
139
141
|
}
|
|
140
142
|
type PayloadValueFilter = {
|
|
143
|
+
readonly as?: "text" | "timestamp";
|
|
144
|
+
} & ({
|
|
141
145
|
readonly equals: string;
|
|
142
146
|
readonly lt?: never;
|
|
143
147
|
readonly lte?: never;
|
|
@@ -193,7 +197,7 @@ type PayloadValueFilter = {
|
|
|
193
197
|
readonly gt?: never;
|
|
194
198
|
readonly gte?: never;
|
|
195
199
|
readonly like?: never;
|
|
196
|
-
};
|
|
200
|
+
});
|
|
197
201
|
export type PayloadMatch = {
|
|
198
202
|
readonly [field: string]: PayloadMatch | PayloadValueFilter;
|
|
199
203
|
};
|
|
@@ -248,7 +248,9 @@ function compileCanonicalPredicate(relation, predicate, alias, profile, add, log
|
|
|
248
248
|
? `${compileCanonicalPhysicalField(relation, field, alias, profile)} #>> ${add(path)}::text[]`
|
|
249
249
|
: compileCanonicalPhysicalField(relation, field, alias, profile);
|
|
250
250
|
const inverseEventType = relation === "__events" && field === "type" && (predicate.operator === "equals" || predicate.operator === "in");
|
|
251
|
-
const expression =
|
|
251
|
+
const expression = predicate.as === "timestamp" && predicate.operator !== "is" && predicate.operator !== "isNot"
|
|
252
|
+
? `(${canonicalExpression})::timestamptz`
|
|
253
|
+
: inverseEventType ? qualified(alias, column) : canonicalExpression;
|
|
252
254
|
const value = inverseEventType ? physicalEventType(predicate.value) : predicate.value;
|
|
253
255
|
const sql = { equals: "=", lt: "<", lte: "<=", gt: ">", gte: ">=", like: "like", ilike: "ilike" }[predicate.operator];
|
|
254
256
|
if (predicate.operator === "is")
|
|
@@ -256,7 +258,7 @@ function compileCanonicalPredicate(relation, predicate, alias, profile, add, log
|
|
|
256
258
|
if (predicate.operator === "isNot")
|
|
257
259
|
return `${expression} is not null`;
|
|
258
260
|
if (predicate.operator === "in")
|
|
259
|
-
return value.length === 0 ? "false" : `${expression} = any(${add(value)})`;
|
|
261
|
+
return value.length === 0 ? "false" : `${expression} = any(${add(value)}${predicate.as === "timestamp" ? "::timestamptz[]" : ""})`;
|
|
260
262
|
if (predicate.operator === "has")
|
|
261
263
|
return `${add(value)} = any(${qualified(alias, column)})`;
|
|
262
264
|
if (sql === undefined)
|