@xml-auditfile/auditfile 0.0.0-stage → 0.0.2-alpha.3
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/LICENSE +201 -0
- package/README.md +183 -2
- package/package.json +62 -4
- package/src/auditfile.js +186 -0
- package/src/definitions/dialect.js +40 -0
- package/src/definitions/element-definition.js +27 -0
- package/src/definitions/localized-text.js +41 -0
- package/src/definitions/manifest.js +160 -0
- package/src/definitions/names.js +42 -0
- package/src/definitions/resources.js +29 -0
- package/src/definitions/view-definition.js +160 -0
- package/src/diagnostics/collector.js +139 -0
- package/src/diagnostics/diagnostic.js +53 -0
- package/src/diagnostics/report.js +56 -0
- package/src/generated/spec.js +74 -0
- package/src/index.js +20 -0
- package/src/internal/composite-key.js +2 -0
- package/src/internal/decimal.js +193 -0
- package/src/internal/encoding.js +118 -0
- package/src/internal/json.js +106 -0
- package/src/internal/sha256.js +147 -0
- package/src/internal/streams.js +232 -0
- package/src/internal/xml-reader.js +247 -0
- package/src/memory/memory-sink.js +68 -0
- package/src/reading/mapping-plan.js +91 -0
- package/src/reading/read-options.js +136 -0
- package/src/reading/read-session.js +446 -0
- package/src/reading/row-loader.js +362 -0
- package/src/reading/row.js +123 -0
- package/src/reading/source.js +146 -0
- package/src/reading/value-converter.js +136 -0
- package/src/schema/schema.js +256 -0
- package/src/sinks/sink.js +53 -0
- package/src/validation/reference-checker.js +146 -0
- package/src/validation/xaf-rules.js +167 -0
- package/src/versioning/version-resolver.js +119 -0
- package/src/versioning/version.js +217 -0
- package/src/views/view-result.js +20 -0
- package/src/views/view-runner.js +449 -0
- package/src/xsd/datatypes.js +141 -0
- package/src/xsd/regex.js +121 -0
- package/src/xsd/schema-set.js +555 -0
- package/src/xsd/validator.js +484 -0
- package/types/auditfile.d.ts +57 -0
- package/types/definitions/dialect.d.ts +27 -0
- package/types/definitions/element-definition.d.ts +16 -0
- package/types/definitions/localized-text.d.ts +25 -0
- package/types/definitions/manifest.d.ts +118 -0
- package/types/definitions/names.d.ts +25 -0
- package/types/definitions/resources.d.ts +9 -0
- package/types/definitions/view-definition.d.ts +85 -0
- package/types/diagnostics/collector.d.ts +38 -0
- package/types/diagnostics/diagnostic.d.ts +37 -0
- package/types/diagnostics/report.d.ts +32 -0
- package/types/generated/spec.d.ts +6 -0
- package/types/index.d.ts +18 -0
- package/types/internal/composite-key.d.ts +2 -0
- package/types/internal/decimal.d.ts +55 -0
- package/types/internal/encoding.d.ts +43 -0
- package/types/internal/json.d.ts +17 -0
- package/types/internal/sha256.d.ts +17 -0
- package/types/internal/streams.d.ts +35 -0
- package/types/internal/xml-reader.d.ts +50 -0
- package/types/memory/memory-sink.d.ts +29 -0
- package/types/reading/mapping-plan.d.ts +45 -0
- package/types/reading/read-options.d.ts +69 -0
- package/types/reading/read-session.d.ts +28 -0
- package/types/reading/row-loader.d.ts +18 -0
- package/types/reading/row.d.ts +53 -0
- package/types/reading/source.d.ts +73 -0
- package/types/reading/value-converter.d.ts +15 -0
- package/types/schema/schema.d.ts +116 -0
- package/types/sinks/sink.d.ts +59 -0
- package/types/validation/reference-checker.d.ts +11 -0
- package/types/validation/xaf-rules.d.ts +13 -0
- package/types/versioning/version-resolver.d.ts +25 -0
- package/types/versioning/version.d.ts +71 -0
- package/types/views/view-result.d.ts +13 -0
- package/types/views/view-runner.d.ts +1 -0
- package/types/xsd/datatypes.d.ts +40 -0
- package/types/xsd/regex.d.ts +6 -0
- package/types/xsd/schema-set.d.ts +20 -0
- package/types/xsd/validator.d.ts +48 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { ColumnType } from '../definitions/manifest.js';
|
|
2
|
+
import { ViewAggregate, ViewOperator } from '../definitions/names.js';
|
|
3
|
+
import { DiagnosticCollector, Location } from '../diagnostics/collector.js';
|
|
4
|
+
import { SEPARATOR } from '../internal/composite-key.js';
|
|
5
|
+
import { Decimal } from '../internal/decimal.js';
|
|
6
|
+
import { toText, tryConvert } from '../reading/value-converter.js';
|
|
7
|
+
import { ViewResult } from './view-result.js';
|
|
8
|
+
|
|
9
|
+
// Runs a view over rows from any store. An input row is the source row plus one slot per join;
|
|
10
|
+
// joins are left joins, so a missing ancestor or lookup leaves its columns null.
|
|
11
|
+
|
|
12
|
+
const MAXIMUM_DIAGNOSTICS = 100;
|
|
13
|
+
|
|
14
|
+
/** @internal */
|
|
15
|
+
export function runView(view, schema, rows, parameters, language) {
|
|
16
|
+
const diagnostics = new DiagnosticCollector(language, MAXIMUM_DIAGNOSTICS);
|
|
17
|
+
const slots = [view.source, ...view.joins.map((j) => j.table)];
|
|
18
|
+
|
|
19
|
+
const missing = findMissing(view, schema, slots);
|
|
20
|
+
if (missing !== null) {
|
|
21
|
+
diagnostics.add('XAF-Q001', Location.NONE, { view: view.name, standard: schema.versionKey, missing });
|
|
22
|
+
return new ViewResult(view, false, [], diagnostics.items);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const resolve = (reference) => {
|
|
26
|
+
const [table, column] = split(reference);
|
|
27
|
+
return new Ref(slots.indexOf(table), schema.table(table).findColumn(column));
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const filters = buildFilters(view, parameters, resolve);
|
|
31
|
+
const columns = view.columns.map((c) => ({ column: c, expression: c.value === null ? null : new Expression(c.value, resolve) }));
|
|
32
|
+
const joins = buildJoins(view, schema, rows, resolve, diagnostics);
|
|
33
|
+
|
|
34
|
+
const inputs = [];
|
|
35
|
+
for (const source of rows(view.source)) {
|
|
36
|
+
const input = new Array(slots.length).fill(null);
|
|
37
|
+
input[0] = source;
|
|
38
|
+
for (let i = 0; i < joins.length; i++) {
|
|
39
|
+
input[i + 1] = joins[i](input);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (filters.every((f) => f(input))) {
|
|
43
|
+
inputs.push(input);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = view.isAggregated
|
|
48
|
+
? aggregate(columns, inputs)
|
|
49
|
+
: inputs.map((input) => columns.map((c) => c.expression.evaluate(input)));
|
|
50
|
+
const order = view.orderBy.map((name) => columns.findIndex((c) => c.column.name === name)).filter((i) => i >= 0);
|
|
51
|
+
if (order.length > 0) {
|
|
52
|
+
// Array.prototype.sort is stable, like the ordering of the other implementations.
|
|
53
|
+
result.sort((a, b) => {
|
|
54
|
+
for (const index of order) {
|
|
55
|
+
const compared = compare(a[index], b[index]);
|
|
56
|
+
if (compared !== 0) {
|
|
57
|
+
return compared;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return 0;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return new ViewResult(view, true, result, diagnostics.items);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Returns the first table or "table.column" the view needs but the schema lacks, or null. */
|
|
69
|
+
function findMissing(view, schema, slots) {
|
|
70
|
+
for (const table of slots) {
|
|
71
|
+
if (schema.findTable(table) === null) {
|
|
72
|
+
return table;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const join of view.joins.filter((j) => j.on.length === 0)) {
|
|
77
|
+
if (ancestorPath(schema, view.source, join.table) === null) {
|
|
78
|
+
return join.table;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const references = [
|
|
83
|
+
...view.joins.flatMap((j) => j.on.flatMap((c) => [c.left, c.right])),
|
|
84
|
+
...view.parameters.map((p) => p.column),
|
|
85
|
+
...view.columns.filter((c) => c.value !== null).flatMap((c) => [c.value.column, c.value.by]),
|
|
86
|
+
].filter((r) => typeof r === 'string');
|
|
87
|
+
for (const reference of references) {
|
|
88
|
+
const [table, column] = split(reference);
|
|
89
|
+
if (!slots.includes(table) || schema.findTable(table)?.findColumn(column) == null) {
|
|
90
|
+
return reference;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Tables from the parent of `source` up to `ancestor`, or null when it is no ancestor. */
|
|
98
|
+
function ancestorPath(schema, source, ancestor) {
|
|
99
|
+
const path = [];
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
for (let table = schema.findTable(source)?.parent ?? null; table !== null && !seen.has(table); table = schema.findTable(table)?.parent ?? null) {
|
|
102
|
+
seen.add(table);
|
|
103
|
+
path.push(table);
|
|
104
|
+
if (table === ancestor) {
|
|
105
|
+
return path;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function entries(parameters) {
|
|
113
|
+
if (parameters === null || parameters === undefined) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (parameters instanceof Map) {
|
|
118
|
+
return [...parameters];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof parameters !== 'object') {
|
|
122
|
+
throw new TypeError('parameters must be an object or a Map.');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return Object.entries(parameters);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function buildFilters(view, parameters, resolve) {
|
|
129
|
+
const filters = [];
|
|
130
|
+
for (const [name, raw] of entries(parameters)) {
|
|
131
|
+
const parameter = view.parameters.find((p) => p.name === name);
|
|
132
|
+
if (parameter === undefined) {
|
|
133
|
+
throw new TypeError(`View '${view.name}' has no parameter '${name}'.`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (raw === null || raw === undefined) {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const reference = resolve(parameter.column);
|
|
141
|
+
const value = convert(raw, reference.column, parameter.name);
|
|
142
|
+
const test = {
|
|
143
|
+
'=': (c) => c === 0,
|
|
144
|
+
'<': (c) => c < 0,
|
|
145
|
+
'<=': (c) => c <= 0,
|
|
146
|
+
'>': (c) => c > 0,
|
|
147
|
+
'>=': (c) => c >= 0,
|
|
148
|
+
}[parameter.operator];
|
|
149
|
+
if (test === undefined) {
|
|
150
|
+
throw new Error(`View '${view.name}': operator '${parameter.operator}' is not supported.`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
filters.push((input) => {
|
|
154
|
+
const cell = reference.get(input);
|
|
155
|
+
return cell !== null && test(compare(cell, value));
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return filters;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function buildJoins(view, schema, rows, resolve, diagnostics) {
|
|
163
|
+
// One index by id over every table between the source and its joined ancestors.
|
|
164
|
+
const ancestorTables = [...new Set(view.joins.filter((j) => j.on.length === 0).flatMap((j) => ancestorPath(schema, view.source, j.table)))];
|
|
165
|
+
const byId = new Map();
|
|
166
|
+
for (const table of ancestorTables) {
|
|
167
|
+
for (const row of rows(table)) {
|
|
168
|
+
byId.set(row.id, row);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const joins = [];
|
|
173
|
+
for (const join of view.joins) {
|
|
174
|
+
if (join.on.length === 0) {
|
|
175
|
+
joins.push((input) => {
|
|
176
|
+
for (let row = byId.get(input[0].parentId); row !== undefined; row = byId.get(row.parentId)) {
|
|
177
|
+
if (row.table.name === join.table) {
|
|
178
|
+
return row;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null;
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const left = join.on.map((c) => resolve(c.left));
|
|
188
|
+
const right = join.on.map((c) => resolve(c.right));
|
|
189
|
+
const index = new Map();
|
|
190
|
+
const width = Math.max(...right.map((r) => r.slot)) + 1;
|
|
191
|
+
for (const row of rows(join.table)) {
|
|
192
|
+
const cells = new Array(width).fill(null);
|
|
193
|
+
cells[right[0].slot] = row;
|
|
194
|
+
const key = keyOf(right, cells);
|
|
195
|
+
if (key === null) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (index.has(key)) {
|
|
200
|
+
diagnostics.add('XAF-Q002', Location.NONE, { view: view.name, table: join.table, key: key.replaceAll(SEPARATOR, '/') });
|
|
201
|
+
} else {
|
|
202
|
+
index.set(key, row);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
joins.push((input) => {
|
|
207
|
+
const key = keyOf(left, input);
|
|
208
|
+
return key === null ? null : index.get(key) ?? null;
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return joins;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function keyOf(references, input) {
|
|
216
|
+
const parts = [];
|
|
217
|
+
for (const reference of references) {
|
|
218
|
+
const value = reference.get(input);
|
|
219
|
+
if (value === null || value === undefined) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
parts.push(format(value));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return parts.join(SEPARATOR);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function aggregate(columns, inputs) {
|
|
230
|
+
const groups = new Map();
|
|
231
|
+
for (const input of inputs) {
|
|
232
|
+
const values = columns.map((c) => c.expression?.evaluate(input) ?? null);
|
|
233
|
+
const key = columns
|
|
234
|
+
.map((c, i) => (c.column.aggregate === null ? (values[i] !== null ? `=${format(values[i])}` : '') : null))
|
|
235
|
+
.filter((k) => k !== null)
|
|
236
|
+
.join(SEPARATOR);
|
|
237
|
+
|
|
238
|
+
let group = groups.get(key);
|
|
239
|
+
if (group === undefined) {
|
|
240
|
+
group = { values, aggregators: columns.map((c) => (c.column.aggregate === null ? null : new Aggregator(c))) };
|
|
241
|
+
groups.set(key, group);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
group.aggregators.forEach((aggregator, i) => aggregator?.add(values[i]));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// A Map keeps insertion order: groups appear in the order they were first seen.
|
|
248
|
+
return [...groups.values()].map(({ values, aggregators }) => values.map((v, i) => (aggregators[i] === null ? v : aggregators[i].result)));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function split(reference) {
|
|
252
|
+
const dot = reference.indexOf('.');
|
|
253
|
+
return dot < 0 ? [reference, ''] : [reference.slice(0, dot), reference.slice(dot + 1)];
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function convert(value, column, parameter) {
|
|
257
|
+
const fail = () => new TypeError(`Parameter '${parameter}': '${String(value)}' is not a valid ${column.type}.`);
|
|
258
|
+
switch (column.type) {
|
|
259
|
+
case ColumnType.Decimal:
|
|
260
|
+
try {
|
|
261
|
+
return Decimal.from(value);
|
|
262
|
+
} catch {
|
|
263
|
+
throw fail();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
case ColumnType.Integer: {
|
|
267
|
+
const number = typeof value === 'string' && /^[+-]?\d+$/.test(value.trim()) ? Number(value) : value;
|
|
268
|
+
if (typeof number !== 'number' || !Number.isSafeInteger(number)) {
|
|
269
|
+
throw fail();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return number;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
case ColumnType.Date:
|
|
276
|
+
case ColumnType.DateTime: {
|
|
277
|
+
if (value instanceof Date && !Number.isNaN(value.getTime())) {
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (typeof value === 'string') {
|
|
282
|
+
const parsed = tryConvert(value, value.trim().length > 10 ? ColumnType.DateTime : ColumnType.Date);
|
|
283
|
+
if (parsed.ok && parsed.value !== null) {
|
|
284
|
+
return parsed.value;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
throw fail();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
case ColumnType.Time: {
|
|
292
|
+
const parsed = typeof value === 'string' ? tryConvert(value, ColumnType.Time) : { ok: false };
|
|
293
|
+
if (!parsed.ok || parsed.value === null) {
|
|
294
|
+
throw fail();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return parsed.value;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
case ColumnType.Boolean:
|
|
301
|
+
if (typeof value === 'boolean') {
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (value === 'true' || value === 'false') {
|
|
306
|
+
return value === 'true';
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
throw fail();
|
|
310
|
+
|
|
311
|
+
default:
|
|
312
|
+
return format(value);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function format(value) {
|
|
317
|
+
return value instanceof Decimal ? value.toString() : toText(value);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Orders null first, numbers by value, dates by time, strings ordinal. */
|
|
321
|
+
function compare(a, b) {
|
|
322
|
+
if (a === null || a === undefined || b === null || b === undefined) {
|
|
323
|
+
return a === null || a === undefined ? (b === null || b === undefined ? 0 : -1) : 1;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
327
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const numeric = (v) => v instanceof Decimal || typeof v === 'number';
|
|
331
|
+
if (numeric(a) && numeric(b)) {
|
|
332
|
+
return Decimal.from(a).compareTo(Decimal.from(b));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (a instanceof Date && b instanceof Date) {
|
|
336
|
+
return Math.sign(a.getTime() - b.getTime());
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
340
|
+
return Number(a) - Number(b);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const sa = format(a);
|
|
344
|
+
const sb = format(b);
|
|
345
|
+
return sa < sb ? -1 : sa > sb ? 1 : 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** A resolved "table.column": the input slot and the column. */
|
|
349
|
+
class Ref {
|
|
350
|
+
constructor(slot, column) {
|
|
351
|
+
this.slot = slot;
|
|
352
|
+
this.column = column;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
get(input) {
|
|
356
|
+
return input[this.slot]?.values[this.column.index] ?? null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
class Expression {
|
|
361
|
+
#value;
|
|
362
|
+
#column;
|
|
363
|
+
#by;
|
|
364
|
+
|
|
365
|
+
constructor(value, resolve) {
|
|
366
|
+
this.#value = value;
|
|
367
|
+
this.#column = resolve(value.column);
|
|
368
|
+
this.#by = value.by === null ? null : resolve(value.by);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
get type() {
|
|
372
|
+
return this.#column.column.type;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
evaluate(input) {
|
|
376
|
+
const cell = this.#column.get(input);
|
|
377
|
+
if (this.#value.operator === ViewOperator.Column || cell === null) {
|
|
378
|
+
return cell;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const by = this.#by?.get(input) ?? null;
|
|
382
|
+
const listed = by !== null && this.#value.codes.includes(format(by));
|
|
383
|
+
if (this.#value.operator === ViewOperator.Only) {
|
|
384
|
+
return listed ? cell : null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return listed ? negate(cell) : cell;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function negate(value) {
|
|
392
|
+
if (value instanceof Decimal) {
|
|
393
|
+
return value.negate();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return typeof value === 'number' ? -value : value;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
class Aggregator {
|
|
400
|
+
#kind;
|
|
401
|
+
#countRows;
|
|
402
|
+
#count = 0;
|
|
403
|
+
#sum = Decimal.ZERO;
|
|
404
|
+
#decimal;
|
|
405
|
+
#extreme = null;
|
|
406
|
+
|
|
407
|
+
constructor(output) {
|
|
408
|
+
this.#kind = output.column.aggregate;
|
|
409
|
+
this.#countRows = output.expression === null;
|
|
410
|
+
this.#decimal = output.expression?.type === ColumnType.Decimal;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
get result() {
|
|
414
|
+
switch (this.#kind) {
|
|
415
|
+
case ViewAggregate.Count:
|
|
416
|
+
return this.#count;
|
|
417
|
+
case ViewAggregate.Sum:
|
|
418
|
+
return this.#decimal ? this.#sum : this.#sum.toNumber();
|
|
419
|
+
default:
|
|
420
|
+
return this.#extreme;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
add(value) {
|
|
425
|
+
if (value === null && !this.#countRows) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
this.#count++;
|
|
430
|
+
switch (this.#kind) {
|
|
431
|
+
case ViewAggregate.Sum:
|
|
432
|
+
this.#decimal ||= value instanceof Decimal;
|
|
433
|
+
this.#sum = this.#sum.add(Decimal.from(value));
|
|
434
|
+
break;
|
|
435
|
+
case ViewAggregate.Min:
|
|
436
|
+
if (this.#extreme === null || compare(value, this.#extreme) < 0) {
|
|
437
|
+
this.#extreme = value;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
break;
|
|
441
|
+
case ViewAggregate.Max:
|
|
442
|
+
if (this.#extreme === null || compare(value, this.#extreme) > 0) {
|
|
443
|
+
this.#extreme = value;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// The XSD built-in simple types this validator supports, with their lexical rules.
|
|
2
|
+
// A type that is not listed is refused when the schema is compiled.
|
|
3
|
+
|
|
4
|
+
import { Decimal } from '../internal/decimal.js';
|
|
5
|
+
|
|
6
|
+
const DECIMAL = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/;
|
|
7
|
+
const INTEGER = /^[+-]?\d+$/;
|
|
8
|
+
const FLOAT = /^(?:[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?|[+-]?INF|NaN)$/;
|
|
9
|
+
const ZONE = '(?:Z|[+-](?:(?:0\\d|1[0-3]):[0-5]\\d|14:00))?';
|
|
10
|
+
const DATE = new RegExp(`^(-?\\d{4,})-(\\d{2})-(\\d{2})${ZONE}$`);
|
|
11
|
+
const DATE_TIME = new RegExp(`^(-?\\d{4,})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:\\.\\d+)?${ZONE}$`);
|
|
12
|
+
const TIME = new RegExp(`^(\\d{2}):(\\d{2}):(\\d{2})(?:\\.\\d+)?${ZONE}$`);
|
|
13
|
+
const G_YEAR = new RegExp(`^-?\\d{4,}${ZONE}$`);
|
|
14
|
+
const G_YEAR_MONTH = new RegExp(`^-?\\d{4,}-(0[1-9]|1[0-2])${ZONE}$`);
|
|
15
|
+
const NAME_START = 'A-Za-z_\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD';
|
|
16
|
+
const NAME_CHAR = `${NAME_START}\\-.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040`;
|
|
17
|
+
const NC_NAME = new RegExp(`^[${NAME_START}][${NAME_CHAR}]*$`);
|
|
18
|
+
const NAME = new RegExp(`^[${NAME_START}:][${NAME_CHAR}:]*$`);
|
|
19
|
+
const NMTOKEN = new RegExp(`^[${NAME_CHAR}:]+$`);
|
|
20
|
+
const LANGUAGE = /^[a-zA-Z]{1,8}(?:-[a-zA-Z0-9]{1,8})*$/;
|
|
21
|
+
|
|
22
|
+
function daysInMonth(year, month) {
|
|
23
|
+
return month === 2 ? (year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28) : [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function validDate(year, month, day) {
|
|
27
|
+
return month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(Number(year), month);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function validTime(hour, minute, second, text) {
|
|
31
|
+
return (hour < 24 && minute < 60 && second < 60) || (hour === 24 && minute === 0 && second === 0 && !/\.\d*[1-9]/.test(text));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const string = (pattern) => (value) => (pattern === null || pattern.test(value) ? value : null);
|
|
35
|
+
|
|
36
|
+
function decimal(value) {
|
|
37
|
+
return DECIMAL.test(value) ? Decimal.parse(value) ?? bigDecimal(value) : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A decimal outside the range of Decimal.parse (more than 29 digits) is still lexically valid XSD. */
|
|
41
|
+
function bigDecimal(value) {
|
|
42
|
+
const [whole, fraction = ''] = value.replace(/^[+-]/, '').split('.');
|
|
43
|
+
const negative = value.startsWith('-');
|
|
44
|
+
const coefficient = BigInt((whole || '0') + fraction);
|
|
45
|
+
return new Decimal(negative ? -coefficient : coefficient, fraction.length);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const integer = (min, max) => (value) => {
|
|
49
|
+
if (!INTEGER.test(value)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const parsed = BigInt(value);
|
|
54
|
+
return (min !== null && parsed < min) || (max !== null && parsed > max) ? null : new Decimal(parsed, 0);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const range = (bits, signed) => signed
|
|
58
|
+
? integer(-(2n ** BigInt(bits - 1)), 2n ** BigInt(bits - 1) - 1n)
|
|
59
|
+
: integer(0n, 2n ** BigInt(bits) - 1n);
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Built-in types: whitespace handling, whether the value space is numeric (for numeric facets),
|
|
63
|
+
* and a parser that returns the value or null when the text is not valid.
|
|
64
|
+
*/
|
|
65
|
+
export const BUILTINS = new Map([
|
|
66
|
+
['anySimpleType', { whiteSpace: 'preserve', numeric: false, parse: string(null) }],
|
|
67
|
+
['string', { whiteSpace: 'preserve', numeric: false, parse: string(null) }],
|
|
68
|
+
['normalizedString', { whiteSpace: 'replace', numeric: false, parse: string(null) }],
|
|
69
|
+
['token', { whiteSpace: 'collapse', numeric: false, parse: string(null) }],
|
|
70
|
+
['language', { whiteSpace: 'collapse', numeric: false, parse: string(LANGUAGE) }],
|
|
71
|
+
['Name', { whiteSpace: 'collapse', numeric: false, parse: string(NAME) }],
|
|
72
|
+
['NCName', { whiteSpace: 'collapse', numeric: false, parse: string(NC_NAME) }],
|
|
73
|
+
['ID', { whiteSpace: 'collapse', numeric: false, parse: string(NC_NAME), id: true }],
|
|
74
|
+
['IDREF', { whiteSpace: 'collapse', numeric: false, parse: string(NC_NAME) }],
|
|
75
|
+
['NMTOKEN', { whiteSpace: 'collapse', numeric: false, parse: string(NMTOKEN) }],
|
|
76
|
+
['anyURI', { whiteSpace: 'collapse', numeric: false, parse: string(null) }],
|
|
77
|
+
['boolean', { whiteSpace: 'collapse', numeric: false, parse: string(/^(?:true|false|1|0)$/) }],
|
|
78
|
+
['float', { whiteSpace: 'collapse', numeric: false, parse: string(FLOAT) }],
|
|
79
|
+
['double', { whiteSpace: 'collapse', numeric: false, parse: string(FLOAT) }],
|
|
80
|
+
['decimal', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: decimal }],
|
|
81
|
+
['integer', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: integer(null, null) }],
|
|
82
|
+
['nonNegativeInteger', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: integer(0n, null) }],
|
|
83
|
+
['positiveInteger', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: integer(1n, null) }],
|
|
84
|
+
['nonPositiveInteger', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: integer(null, 0n) }],
|
|
85
|
+
['negativeInteger', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: integer(null, -1n) }],
|
|
86
|
+
['long', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: range(64, true) }],
|
|
87
|
+
['int', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: range(32, true) }],
|
|
88
|
+
['short', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: range(16, true) }],
|
|
89
|
+
['byte', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: range(8, true) }],
|
|
90
|
+
['unsignedLong', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: range(64, false) }],
|
|
91
|
+
['unsignedInt', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: range(32, false) }],
|
|
92
|
+
['unsignedShort', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: range(16, false) }],
|
|
93
|
+
['unsignedByte', { whiteSpace: 'collapse', numeric: true, decimal: true, parse: range(8, false) }],
|
|
94
|
+
['date', {
|
|
95
|
+
whiteSpace: 'collapse', numeric: false,
|
|
96
|
+
parse: (v) => {
|
|
97
|
+
const m = DATE.exec(v);
|
|
98
|
+
return m !== null && validDate(m[1], Number(m[2]), Number(m[3])) ? v : null;
|
|
99
|
+
},
|
|
100
|
+
}],
|
|
101
|
+
['dateTime', {
|
|
102
|
+
whiteSpace: 'collapse', numeric: false,
|
|
103
|
+
parse: (v) => {
|
|
104
|
+
const m = DATE_TIME.exec(v);
|
|
105
|
+
return m !== null && validDate(m[1], Number(m[2]), Number(m[3])) && validTime(Number(m[4]), Number(m[5]), Number(m[6]), v) ? v : null;
|
|
106
|
+
},
|
|
107
|
+
}],
|
|
108
|
+
['time', {
|
|
109
|
+
whiteSpace: 'collapse', numeric: false,
|
|
110
|
+
parse: (v) => {
|
|
111
|
+
const m = TIME.exec(v);
|
|
112
|
+
return m !== null && validTime(Number(m[1]), Number(m[2]), Number(m[3]), v) ? v : null;
|
|
113
|
+
},
|
|
114
|
+
}],
|
|
115
|
+
['gYear', { whiteSpace: 'collapse', numeric: false, parse: string(G_YEAR) }],
|
|
116
|
+
['gYearMonth', { whiteSpace: 'collapse', numeric: false, parse: string(G_YEAR_MONTH) }],
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Applies a whiteSpace facet value.
|
|
121
|
+
* @param {string} value
|
|
122
|
+
* @param {'preserve' | 'replace' | 'collapse'} mode
|
|
123
|
+
*/
|
|
124
|
+
export function normalizeWhiteSpace(value, mode) {
|
|
125
|
+
if (mode === 'preserve') {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const replaced = value.replace(/[\t\n\r]/g, ' ');
|
|
130
|
+
return mode === 'replace' ? replaced : replaced.replace(/ {2,}/g, ' ').replace(/^ | $/g, '');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Total and fraction digits of a decimal, as the XSD facets count them.
|
|
135
|
+
* @param {Decimal} value
|
|
136
|
+
*/
|
|
137
|
+
export function digitsOf(value) {
|
|
138
|
+
const normalized = value.normalize();
|
|
139
|
+
const digits = (normalized.coefficient < 0n ? -normalized.coefficient : normalized.coefficient).toString().replace(/^0+/, '');
|
|
140
|
+
return { total: Math.max(digits.length, normalized.scale, 1), fraction: normalized.scale };
|
|
141
|
+
}
|