@sequenceholdings/orm 2.0.0 → 2.1.0
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 +57 -2
- package/dist/authoring.cjs +122 -15
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +20 -3
- package/dist/authoring.d.ts +20 -3
- package/dist/authoring.js +122 -15
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +83 -9
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +47 -1
- package/dist/canonical.d.ts +47 -1
- package/dist/canonical.js +83 -10
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +721 -132
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +723 -134
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +482 -96
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +34 -2
- package/dist/compiler.d.ts +34 -2
- package/dist/compiler.js +483 -97
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +414 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +266 -9
- package/dist/index.d.ts +266 -9
- package/dist/index.js +404 -72
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +146 -12
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +161 -5
- package/dist/ir.d.ts +161 -5
- package/dist/ir.js +144 -13
- package/dist/ir.js.map +1 -1
- package/dist/{naming-CTvWSRTf.d.ts → naming-DCn94LJ4.d.ts} +15 -1
- package/dist/{naming-DRyinU4u.d.cts → naming-DPglwzkB.d.cts} +15 -1
- package/dist/predicate-v2.cjs +130 -11
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.js +130 -11
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +460 -13
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +1 -1
- package/dist/sdl.d.ts +1 -1
- package/dist/sdl.js +459 -14
- package/dist/sdl.js.map +1 -1
- package/package.json +3 -3
package/dist/compiler.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { Kind, print, buildSchema, parse, validate, valueFromASTUntyped } from 'graphql';
|
|
3
|
+
import { Kind, print, visit, buildSchema, parse, validate, valueFromASTUntyped } from 'graphql';
|
|
4
4
|
|
|
5
5
|
// src/compiler-gql/artifact.ts
|
|
6
6
|
|
|
7
7
|
// src/constants.ts
|
|
8
|
+
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
8
9
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
9
10
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
10
11
|
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
@@ -19,6 +20,7 @@ function stableJson(value) {
|
|
|
19
20
|
var CANONICAL_FORMAT = 2;
|
|
20
21
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
21
22
|
var identifier = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
23
|
+
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
22
24
|
var COLUMN_TYPES = [
|
|
23
25
|
"uuid",
|
|
24
26
|
"text",
|
|
@@ -39,18 +41,33 @@ var canonicalEnumSchema = z.object({
|
|
|
39
41
|
var canonicalColumnSchema = z.object({
|
|
40
42
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
41
43
|
name: z.string().min(1).max(63),
|
|
42
|
-
columnName:
|
|
44
|
+
columnName: columnIdentifier,
|
|
43
45
|
type: z.union([columnTypeSchema, z.object({ enum: identifier })]),
|
|
44
46
|
nullable: z.boolean().default(true),
|
|
45
47
|
unique: z.boolean().default(false),
|
|
46
48
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
47
49
|
default: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
|
|
48
50
|
defaultFn: z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
51
|
+
/**
|
|
52
|
+
* Stamp this column from the database clock on UPDATE (`now()`). The
|
|
53
|
+
* column stays insertable (defaults fill gaps) but is omitted from
|
|
54
|
+
* SetInput / settable so callers cannot backdate it.
|
|
55
|
+
*/
|
|
56
|
+
onUpdateFn: z.enum(["now()"]).optional(),
|
|
49
57
|
description: z.string().max(500).optional(),
|
|
50
58
|
/** Migration hints — same semantics as v1. */
|
|
51
|
-
renamedFrom:
|
|
59
|
+
renamedFrom: columnIdentifier.optional(),
|
|
52
60
|
cast: z.string().max(200).optional(),
|
|
53
61
|
deprecated: z.boolean().default(false)
|
|
62
|
+
}).superRefine((column, ctx) => {
|
|
63
|
+
if (column.onUpdateFn === void 0) return;
|
|
64
|
+
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
65
|
+
ctx.addIssue({
|
|
66
|
+
code: "custom",
|
|
67
|
+
path: ["onUpdateFn"],
|
|
68
|
+
message: `onUpdateFn '${column.onUpdateFn}' is only valid on timestamptz/date columns`
|
|
69
|
+
});
|
|
70
|
+
}
|
|
54
71
|
});
|
|
55
72
|
var canonicalLinkSchema = z.object({
|
|
56
73
|
name: z.string().min(1).max(63),
|
|
@@ -58,15 +75,15 @@ var canonicalLinkSchema = z.object({
|
|
|
58
75
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
59
76
|
target: identifier.optional(),
|
|
60
77
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
61
|
-
on:
|
|
78
|
+
on: columnIdentifier,
|
|
62
79
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
63
|
-
references:
|
|
80
|
+
references: columnIdentifier.optional(),
|
|
64
81
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
65
82
|
enforce: z.boolean().default(false),
|
|
66
83
|
description: z.string().max(500).optional()
|
|
67
84
|
});
|
|
68
85
|
var canonicalIndexSchema = z.object({
|
|
69
|
-
columns: z.array(
|
|
86
|
+
columns: z.array(columnIdentifier).min(1),
|
|
70
87
|
unique: z.boolean().default(false),
|
|
71
88
|
name: identifier.optional()
|
|
72
89
|
});
|
|
@@ -157,6 +174,13 @@ var canonicalCowMutationSchema = z.object({
|
|
|
157
174
|
}
|
|
158
175
|
}
|
|
159
176
|
});
|
|
177
|
+
var canonicalForeignKeySchema = z.object({
|
|
178
|
+
name: identifier,
|
|
179
|
+
columns: z.array(columnIdentifier).min(1).max(8),
|
|
180
|
+
target: identifier,
|
|
181
|
+
references: z.array(columnIdentifier).min(1).max(8),
|
|
182
|
+
deferrable: z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
183
|
+
}).strict();
|
|
160
184
|
var canonicalTableSchema = z.object({
|
|
161
185
|
table: identifier,
|
|
162
186
|
description: z.string().max(2e3).optional(),
|
|
@@ -165,7 +189,7 @@ var canonicalTableSchema = z.object({
|
|
|
165
189
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
166
190
|
source: z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
167
191
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
168
|
-
primaryKey: z.array(
|
|
192
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
169
193
|
links: z.array(canonicalLinkSchema).default([]),
|
|
170
194
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
171
195
|
policies: canonicalPoliciesSchema.default({}),
|
|
@@ -176,14 +200,16 @@ var canonicalTableSchema = z.object({
|
|
|
176
200
|
*/
|
|
177
201
|
actions: z.array(canonicalActionSchema).default([]),
|
|
178
202
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
179
|
-
cowMutations: z.array(canonicalCowMutationSchema).optional()
|
|
203
|
+
cowMutations: z.array(canonicalCowMutationSchema).optional(),
|
|
204
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
205
|
+
foreignKeys: z.array(canonicalForeignKeySchema).max(100).optional()
|
|
180
206
|
});
|
|
181
207
|
var canonicalViewSchema = z.object({
|
|
182
208
|
view: identifier,
|
|
183
209
|
description: z.string().max(2e3).optional(),
|
|
184
210
|
columns: z.array(canonicalColumnSchema).min(1),
|
|
185
211
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
186
|
-
primaryKey: z.array(
|
|
212
|
+
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
187
213
|
links: z.array(canonicalLinkSchema).default([]),
|
|
188
214
|
policies: z.object({ read: policyExpressionSchema }).strict(),
|
|
189
215
|
query: z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
|
|
@@ -196,6 +222,9 @@ var canonicalViewSchema = z.object({
|
|
|
196
222
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
197
223
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
198
224
|
}
|
|
225
|
+
if (column.onUpdateFn !== void 0) {
|
|
226
|
+
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare onUpdateFn` });
|
|
227
|
+
}
|
|
199
228
|
if (column.unique) {
|
|
200
229
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
|
|
201
230
|
}
|
|
@@ -229,6 +258,45 @@ var canonicalDefinitionSchema = z.object({
|
|
|
229
258
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
230
259
|
}
|
|
231
260
|
seen.add(table.table);
|
|
261
|
+
if (table.source !== void 0) {
|
|
262
|
+
for (const [columnIndex, column] of table.columns.entries()) {
|
|
263
|
+
if (column.onUpdateFn !== void 0) {
|
|
264
|
+
ctx.addIssue({
|
|
265
|
+
code: "custom",
|
|
266
|
+
path: ["tables", index, "columns", columnIndex],
|
|
267
|
+
message: `synced table '${table.table}' column '${column.name}' cannot declare onUpdateFn`
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
274
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
275
|
+
const names = /* @__PURE__ */ new Set([
|
|
276
|
+
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
277
|
+
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
278
|
+
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
279
|
+
]);
|
|
280
|
+
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
281
|
+
const fail2 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
282
|
+
if (names.has(fk.name)) fail2(`duplicate foreign key '${fk.name}'`);
|
|
283
|
+
names.add(fk.name);
|
|
284
|
+
const target = fkTables.get(fk.target);
|
|
285
|
+
if (table.source || !target || target.source) {
|
|
286
|
+
fail2("foreign keys must connect owned tables in the same namespace");
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (fk.columns.length !== fk.references.length) fail2("foreign key columns and references must have the same number of columns");
|
|
290
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail2("foreign key columns must not repeat");
|
|
291
|
+
for (const [position, name] of fk.columns.entries()) {
|
|
292
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name);
|
|
293
|
+
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
294
|
+
if (!sourceColumn || !targetColumn) fail2("foreign key references an unknown column");
|
|
295
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail2("foreign key column types must match");
|
|
296
|
+
}
|
|
297
|
+
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
298
|
+
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail2("foreign key target columns must form a unique key");
|
|
299
|
+
}
|
|
232
300
|
}
|
|
233
301
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
234
302
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
@@ -324,7 +392,7 @@ var canonicalDefinitionSchema = z.object({
|
|
|
324
392
|
const visiting = /* @__PURE__ */ new Set();
|
|
325
393
|
const visited = /* @__PURE__ */ new Set();
|
|
326
394
|
let cycleReported = false;
|
|
327
|
-
const
|
|
395
|
+
const visit2 = (name, path) => {
|
|
328
396
|
if (visited.has(name) || cycleReported) return;
|
|
329
397
|
if (visiting.has(name)) {
|
|
330
398
|
cycleReported = true;
|
|
@@ -338,12 +406,12 @@ var canonicalDefinitionSchema = z.object({
|
|
|
338
406
|
visiting.add(name);
|
|
339
407
|
const view = viewByName.get(name);
|
|
340
408
|
for (const dependency of view?.dependencies ?? []) {
|
|
341
|
-
if (viewByName.has(dependency))
|
|
409
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name]);
|
|
342
410
|
}
|
|
343
411
|
visiting.delete(name);
|
|
344
412
|
visited.add(name);
|
|
345
413
|
};
|
|
346
|
-
for (const view of views)
|
|
414
|
+
for (const view of views) visit2(view.view, []);
|
|
347
415
|
});
|
|
348
416
|
function canonicalRelations(definition) {
|
|
349
417
|
const views = (definition.views ?? []).map((view) => ({
|
|
@@ -364,12 +432,13 @@ function normalizeCanonical(definition) {
|
|
|
364
432
|
...definition,
|
|
365
433
|
enums: sortByName(definition.enums),
|
|
366
434
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
|
|
367
|
-
const { cowMutations, ...rest } = t;
|
|
435
|
+
const { cowMutations, foreignKeys, ...rest } = t;
|
|
368
436
|
return {
|
|
369
437
|
...rest,
|
|
370
438
|
links: sortByName(t.links),
|
|
371
439
|
indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
372
440
|
actions: sortByName(t.actions),
|
|
441
|
+
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
373
442
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
374
443
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
375
444
|
};
|
|
@@ -386,7 +455,18 @@ function normalizeCanonical(definition) {
|
|
|
386
455
|
}
|
|
387
456
|
|
|
388
457
|
// src/ir/index.ts
|
|
389
|
-
var IR_VERSION =
|
|
458
|
+
var IR_VERSION = 2;
|
|
459
|
+
var LEGACY_IR_VERSION = 1;
|
|
460
|
+
var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(IR_VERSION)]);
|
|
461
|
+
var operationVersionSchema = z.object({ irVersion: irVersionSchema });
|
|
462
|
+
function requiredArtifactIrVersion(operations) {
|
|
463
|
+
let required = LEGACY_IR_VERSION;
|
|
464
|
+
for (const operation of operations) {
|
|
465
|
+
const { irVersion } = operationVersionSchema.parse(operation);
|
|
466
|
+
if (irVersion > required) required = irVersion;
|
|
467
|
+
}
|
|
468
|
+
return required;
|
|
469
|
+
}
|
|
390
470
|
var CLAIMS_CONTRACT = {
|
|
391
471
|
sub: "string",
|
|
392
472
|
org: "string?",
|
|
@@ -513,6 +593,33 @@ var aggregateRootSchema = z.object({
|
|
|
513
593
|
nodes: selectionIrSchema.optional(),
|
|
514
594
|
includeIf: includeIfSchema.optional()
|
|
515
595
|
});
|
|
596
|
+
var DATE_BUCKETS = ["year", "quarter", "month", "week", "day", "hour"];
|
|
597
|
+
var groupByKeyIrSchema = z.object({
|
|
598
|
+
column: z.string(),
|
|
599
|
+
bucket: z.enum(DATE_BUCKETS).optional()
|
|
600
|
+
});
|
|
601
|
+
var aggregateGroupsRootSchema = z.object({
|
|
602
|
+
kind: z.literal("aggregateGroups"),
|
|
603
|
+
alias: z.string(),
|
|
604
|
+
table: z.string(),
|
|
605
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
606
|
+
where: predIrSchema,
|
|
607
|
+
groupBy: z.array(groupByKeyIrSchema).min(1),
|
|
608
|
+
/**
|
|
609
|
+
* The `key { … }` projection: which group columns appear in each entry's key
|
|
610
|
+
* object, in selection order. A subset of `groupBy` (the compiler rejects a
|
|
611
|
+
* key field that is not a group key — it would be indeterminate within the
|
|
612
|
+
* group). Empty when the document selects only `aggregate`.
|
|
613
|
+
*/
|
|
614
|
+
key: z.array(z.object({ alias: z.string(), column: z.string() })).default([]),
|
|
615
|
+
count: z.object({ columns: z.array(z.string()).optional(), distinct: z.boolean().optional() }).optional(),
|
|
616
|
+
sum: z.array(z.string()).optional(),
|
|
617
|
+
avg: z.array(z.string()).optional(),
|
|
618
|
+
/** Bounds the GROUP count, not the scanned rows — see MAX_GROUPS. */
|
|
619
|
+
limit: limitIrSchema,
|
|
620
|
+
offset: z.object({ variable: z.string().optional(), value: z.number().int().optional() }).optional(),
|
|
621
|
+
includeIf: includeIfSchema.optional()
|
|
622
|
+
});
|
|
516
623
|
var writeValuesSchema = z.union([
|
|
517
624
|
z.object({ slot: z.string() }),
|
|
518
625
|
z.object({ entries: z.array(z.object({ column: z.string(), value: valueIrSchema })) })
|
|
@@ -558,12 +665,15 @@ var mutationRootSchema = z.object({
|
|
|
558
665
|
insertCheck: predIrSchema.optional(),
|
|
559
666
|
returning: selectionIrSchema.nullable(),
|
|
560
667
|
affectedRows: z.boolean().default(false),
|
|
668
|
+
/** v2: abort the transaction when the actual write count differs, regardless of selection. */
|
|
669
|
+
assertAffectedRows: z.number().int().min(0).max(2147483647).optional(),
|
|
561
670
|
includeIf: includeIfSchema.optional()
|
|
562
671
|
});
|
|
563
672
|
var rootIrSchema = z.discriminatedUnion("kind", [
|
|
564
673
|
selectRootSchema,
|
|
565
674
|
byPkRootSchema,
|
|
566
675
|
aggregateRootSchema,
|
|
676
|
+
aggregateGroupsRootSchema,
|
|
567
677
|
mutationRootSchema
|
|
568
678
|
]);
|
|
569
679
|
var INPUT_REF_KINDS = [
|
|
@@ -593,7 +703,7 @@ var variableIrSchema = z.object({
|
|
|
593
703
|
defaultValue: variableDefaultIrSchema.optional()
|
|
594
704
|
});
|
|
595
705
|
var compiledOperationSchema = z.object({
|
|
596
|
-
irVersion:
|
|
706
|
+
irVersion: irVersionSchema,
|
|
597
707
|
name: z.string(),
|
|
598
708
|
opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
599
709
|
kind: z.enum(["query", "mutation"]),
|
|
@@ -605,6 +715,13 @@ var compiledOperationSchema = z.object({
|
|
|
605
715
|
/** Every table the op touches — data-token scoping + audit. */
|
|
606
716
|
tableSet: z.array(z.string()).min(1),
|
|
607
717
|
roots: z.array(rootIrSchema).min(1)
|
|
718
|
+
}).superRefine((operation, ctx) => {
|
|
719
|
+
for (const [index, root] of operation.roots.entries()) {
|
|
720
|
+
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
721
|
+
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
722
|
+
ctx.addIssue({ code: "custom", path: ["roots", index], message: "affected-row assertions require unconditional mutation roots and IR v2" });
|
|
723
|
+
}
|
|
724
|
+
}
|
|
608
725
|
});
|
|
609
726
|
var tableMetaIrSchema = z.object({
|
|
610
727
|
table: z.string(),
|
|
@@ -641,18 +758,29 @@ var tableMetaIrSchema = z.object({
|
|
|
641
758
|
insertable: z.array(z.string()),
|
|
642
759
|
settable: z.array(z.string()),
|
|
643
760
|
appendable: z.array(z.string()),
|
|
761
|
+
/**
|
|
762
|
+
* Logical columns stamped with Postgres `now()` on UPDATE (canonical
|
|
763
|
+
* `onUpdateFn`). Omitted when empty so pre-onUpdate artifacts still parse.
|
|
764
|
+
*/
|
|
765
|
+
touchOnUpdate: z.array(z.string()).optional(),
|
|
644
766
|
constraints: z.array(z.object({ name: z.string(), columns: z.array(z.string()) })),
|
|
645
767
|
/** Depth/node caps for dynamic filter trees on this table. */
|
|
646
768
|
dynamicFilterMaxDepth: z.number().int().positive().default(6),
|
|
647
769
|
dynamicFilterMaxNodes: z.number().int().positive().default(64)
|
|
648
770
|
});
|
|
649
771
|
var compiledArtifactSchema = z.object({
|
|
650
|
-
irVersion:
|
|
772
|
+
irVersion: irVersionSchema,
|
|
651
773
|
namespace: z.string(),
|
|
652
774
|
schemaHash: z.string(),
|
|
653
775
|
opsHash: z.string(),
|
|
654
776
|
tables: z.record(z.string(), tableMetaIrSchema),
|
|
655
777
|
operations: z.record(z.string(), compiledOperationSchema)
|
|
778
|
+
}).superRefine((artifact, ctx) => {
|
|
779
|
+
for (const [name, operation] of Object.entries(artifact.operations)) {
|
|
780
|
+
if (operation.irVersion > artifact.irVersion) {
|
|
781
|
+
ctx.addIssue({ code: "custom", path: ["operations", name, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
782
|
+
}
|
|
783
|
+
}
|
|
656
784
|
});
|
|
657
785
|
var IrVersionError = class extends Error {
|
|
658
786
|
constructor(found) {
|
|
@@ -663,19 +791,19 @@ var IrVersionError = class extends Error {
|
|
|
663
791
|
function normalizeIr(raw) {
|
|
664
792
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
665
793
|
const version = raw.irVersion;
|
|
666
|
-
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
794
|
+
if (version !== LEGACY_IR_VERSION && version !== IR_VERSION) throw new IrVersionError(version);
|
|
667
795
|
return compiledArtifactSchema.parse(raw);
|
|
668
796
|
}
|
|
669
797
|
|
|
670
798
|
// src/predicate-v2/ast.ts
|
|
671
799
|
var POLICY_COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
672
|
-
function walkPolicy(node,
|
|
673
|
-
|
|
800
|
+
function walkPolicy(node, visit2) {
|
|
801
|
+
visit2(node);
|
|
674
802
|
if (node.kind === "and" || node.kind === "or") {
|
|
675
|
-
for (const operand of node.operands) walkPolicy(operand,
|
|
803
|
+
for (const operand of node.operands) walkPolicy(operand, visit2);
|
|
676
804
|
}
|
|
677
|
-
if (node.kind === "not") walkPolicy(node.operand,
|
|
678
|
-
if (node.kind === "exists") walkPolicy(node.where,
|
|
805
|
+
if (node.kind === "not") walkPolicy(node.operand, visit2);
|
|
806
|
+
if (node.kind === "exists") walkPolicy(node.where, visit2);
|
|
679
807
|
}
|
|
680
808
|
|
|
681
809
|
// src/predicate-v2/errors.ts
|
|
@@ -1181,21 +1309,21 @@ function andPred(...preds) {
|
|
|
1181
1309
|
if (operands.length === 1) return operands[0];
|
|
1182
1310
|
return { kind: "and", operands };
|
|
1183
1311
|
}
|
|
1184
|
-
function walkPred(pred,
|
|
1185
|
-
|
|
1312
|
+
function walkPred(pred, visit2) {
|
|
1313
|
+
visit2(pred);
|
|
1186
1314
|
switch (pred.kind) {
|
|
1187
1315
|
case "and":
|
|
1188
1316
|
case "or":
|
|
1189
|
-
for (const operand of pred.operands) walkPred(operand,
|
|
1317
|
+
for (const operand of pred.operands) walkPred(operand, visit2);
|
|
1190
1318
|
return;
|
|
1191
1319
|
case "not":
|
|
1192
|
-
walkPred(pred.operand,
|
|
1320
|
+
walkPred(pred.operand, visit2);
|
|
1193
1321
|
return;
|
|
1194
1322
|
case "exists":
|
|
1195
|
-
walkPred(pred.where,
|
|
1323
|
+
walkPred(pred.where, visit2);
|
|
1196
1324
|
return;
|
|
1197
1325
|
case "aggPredicate":
|
|
1198
|
-
if (pred.where) walkPred(pred.where,
|
|
1326
|
+
if (pred.where) walkPred(pred.where, visit2);
|
|
1199
1327
|
return;
|
|
1200
1328
|
default:
|
|
1201
1329
|
return;
|
|
@@ -1220,12 +1348,12 @@ function predContains(haystack, needle) {
|
|
|
1220
1348
|
// src/compiler-gql/inject.ts
|
|
1221
1349
|
function collectPinnedColumns(writePolicy) {
|
|
1222
1350
|
const key = (pin) => `${pin.column}\0${pin.claim}`;
|
|
1223
|
-
const
|
|
1351
|
+
const visit2 = (node) => {
|
|
1224
1352
|
if (node.kind === "and") {
|
|
1225
1353
|
const merged = [];
|
|
1226
1354
|
const seen2 = /* @__PURE__ */ new Set();
|
|
1227
1355
|
for (const operand of node.operands) {
|
|
1228
|
-
for (const pin of
|
|
1356
|
+
for (const pin of visit2(operand)) {
|
|
1229
1357
|
if (seen2.has(key(pin))) continue;
|
|
1230
1358
|
seen2.add(key(pin));
|
|
1231
1359
|
merged.push(pin);
|
|
@@ -1236,9 +1364,9 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1236
1364
|
if (node.kind === "or") {
|
|
1237
1365
|
const [first, ...rest] = node.operands;
|
|
1238
1366
|
if (first === void 0) return [];
|
|
1239
|
-
let common =
|
|
1367
|
+
let common = visit2(first);
|
|
1240
1368
|
for (const operand of rest) {
|
|
1241
|
-
const branch = new Set(
|
|
1369
|
+
const branch = new Set(visit2(operand).map(key));
|
|
1242
1370
|
common = common.filter((pin) => branch.has(key(pin)));
|
|
1243
1371
|
if (common.length === 0) break;
|
|
1244
1372
|
}
|
|
@@ -1250,7 +1378,7 @@ function collectPinnedColumns(writePolicy) {
|
|
|
1250
1378
|
return [];
|
|
1251
1379
|
};
|
|
1252
1380
|
const seen = /* @__PURE__ */ new Set();
|
|
1253
|
-
return
|
|
1381
|
+
return visit2(writePolicy).filter((pin) => {
|
|
1254
1382
|
if (seen.has(pin.column)) return false;
|
|
1255
1383
|
seen.add(pin.column);
|
|
1256
1384
|
return true;
|
|
@@ -1344,6 +1472,11 @@ var PolicyInjector = class {
|
|
|
1344
1472
|
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table)),
|
|
1345
1473
|
...root.nodes ? { nodes: this.injectSelection(root.nodes, root.table) } : {}
|
|
1346
1474
|
};
|
|
1475
|
+
case "aggregateGroups":
|
|
1476
|
+
return {
|
|
1477
|
+
...root,
|
|
1478
|
+
where: andPred(this.read(root.table), this.injectDocPred(root.where, root.table))
|
|
1479
|
+
};
|
|
1347
1480
|
case "mutation":
|
|
1348
1481
|
return this.injectMutation(root);
|
|
1349
1482
|
}
|
|
@@ -1436,20 +1569,33 @@ function collectTableSet(roots, ctx) {
|
|
|
1436
1569
|
};
|
|
1437
1570
|
for (const root of roots) {
|
|
1438
1571
|
tables.add(root.table);
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1572
|
+
switch (root.kind) {
|
|
1573
|
+
case "select":
|
|
1574
|
+
case "byPk":
|
|
1575
|
+
visitPred(root.where, root.table);
|
|
1576
|
+
visitSelection(root.selection, root.table);
|
|
1577
|
+
break;
|
|
1578
|
+
case "aggregate":
|
|
1579
|
+
visitPred(root.where, root.table);
|
|
1580
|
+
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
1581
|
+
break;
|
|
1582
|
+
case "aggregateGroups":
|
|
1583
|
+
visitPred(root.where, root.table);
|
|
1584
|
+
break;
|
|
1585
|
+
case "mutation":
|
|
1586
|
+
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
1587
|
+
if (root.where) visitPred(root.where, root.table);
|
|
1588
|
+
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
1589
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
1590
|
+
if (root.returning) visitSelection(root.returning, root.table);
|
|
1591
|
+
break;
|
|
1592
|
+
default: {
|
|
1593
|
+
const exhausted = root;
|
|
1594
|
+
throw new DocumentCompileError(
|
|
1595
|
+
`internal: unhandled root kind in tableSet collection ${JSON.stringify(exhausted)}`,
|
|
1596
|
+
"internal"
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1453
1599
|
}
|
|
1454
1600
|
}
|
|
1455
1601
|
return [...tables].sort();
|
|
@@ -1548,7 +1694,12 @@ function tableNames(table) {
|
|
|
1548
1694
|
sumFields: `${type}SumFields`,
|
|
1549
1695
|
avgFields: `${type}AvgFields`,
|
|
1550
1696
|
aggregateBoolExp: `${type}AggregateBoolExp`,
|
|
1551
|
-
aggregateBoolExpCount: `${type}AggregateBoolExpCount
|
|
1697
|
+
aggregateBoolExpCount: `${type}AggregateBoolExpCount`,
|
|
1698
|
+
aggregateGroupsRoot: `${root}AggregateGroups`,
|
|
1699
|
+
groupByColumnEnum: `${type}GroupByColumn`,
|
|
1700
|
+
groupByInput: `${type}GroupBy`,
|
|
1701
|
+
groupKey: `${type}GroupKey`,
|
|
1702
|
+
aggregateGroup: `${type}AggregateGroup`
|
|
1552
1703
|
};
|
|
1553
1704
|
}
|
|
1554
1705
|
|
|
@@ -1559,7 +1710,12 @@ function typeNameForTable(table) {
|
|
|
1559
1710
|
}
|
|
1560
1711
|
function queryRootNames(table) {
|
|
1561
1712
|
const names = tableNames(table);
|
|
1562
|
-
return {
|
|
1713
|
+
return {
|
|
1714
|
+
select: names.selectRoot,
|
|
1715
|
+
byPk: names.byPkRoot,
|
|
1716
|
+
aggregate: names.aggregateRoot,
|
|
1717
|
+
aggregateGroups: names.aggregateGroupsRoot
|
|
1718
|
+
};
|
|
1563
1719
|
}
|
|
1564
1720
|
function mutationRootNames(table) {
|
|
1565
1721
|
const names = tableNames(table);
|
|
@@ -1652,7 +1808,14 @@ function resolveColumnEntries(table, enums, diagnostics) {
|
|
|
1652
1808
|
type = { enum: enumDef.name, values: [...enumDef.values] };
|
|
1653
1809
|
}
|
|
1654
1810
|
}
|
|
1655
|
-
return {
|
|
1811
|
+
return {
|
|
1812
|
+
name: column.name,
|
|
1813
|
+
columnName: column.columnName,
|
|
1814
|
+
type,
|
|
1815
|
+
nullable: column.nullable,
|
|
1816
|
+
deprecated: column.deprecated,
|
|
1817
|
+
onUpdateFn: column.onUpdateFn !== void 0
|
|
1818
|
+
};
|
|
1656
1819
|
});
|
|
1657
1820
|
}
|
|
1658
1821
|
function logicalColumn(entries, ref) {
|
|
@@ -1701,6 +1864,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1701
1864
|
primaryKey.push(resolved);
|
|
1702
1865
|
}
|
|
1703
1866
|
const active = entries.filter((c) => !c.deprecated);
|
|
1867
|
+
const touchOnUpdate = isView ? [] : active.filter((c) => c.onUpdateFn).map((c) => c.name);
|
|
1704
1868
|
const constraints = [];
|
|
1705
1869
|
if (primaryKey.length > 0) constraints.push({ name: `${table.table}_pkey`, columns: [...primaryKey] });
|
|
1706
1870
|
for (const column of table.columns) {
|
|
@@ -1721,14 +1885,15 @@ function buildCompileContext(rawCanonical) {
|
|
|
1721
1885
|
// Synced twins (and views reading them, resolved transitively below)
|
|
1722
1886
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
1723
1887
|
claimsGated: relationIsClaimsGated(table.table),
|
|
1724
|
-
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
1888
|
+
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
1725
1889
|
primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1726
1890
|
links: [],
|
|
1727
1891
|
filterable: entries.map((c) => c.name),
|
|
1728
1892
|
orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
1729
1893
|
insertable: isView ? [] : active.map((c) => c.name),
|
|
1730
|
-
settable: isView ? [] : active.map((c) => c.name),
|
|
1894
|
+
settable: isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
1731
1895
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
1896
|
+
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
1732
1897
|
constraints: isView ? [] : constraints,
|
|
1733
1898
|
dynamicFilterMaxDepth: 6,
|
|
1734
1899
|
dynamicFilterMaxNodes: 64
|
|
@@ -1749,7 +1914,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1749
1914
|
});
|
|
1750
1915
|
continue;
|
|
1751
1916
|
}
|
|
1752
|
-
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false })) : columnEntries.get(targetName);
|
|
1917
|
+
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
1753
1918
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
1754
1919
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
1755
1920
|
const on = logicalColumn(onEntries, link.on);
|
|
@@ -1846,6 +2011,9 @@ function buildCompileContext(rawCanonical) {
|
|
|
1846
2011
|
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
1847
2012
|
}
|
|
1848
2013
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
2014
|
+
if (table.columns.some((column) => column.type !== "jsonb")) {
|
|
2015
|
+
queryRoots.set(roots.aggregateGroups, { kind: "aggregateGroups", table: table.table });
|
|
2016
|
+
}
|
|
1849
2017
|
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
1850
2018
|
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
1851
2019
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
@@ -1981,6 +2149,16 @@ function computeOpsHash(opHashes) {
|
|
|
1981
2149
|
}
|
|
1982
2150
|
var DEFAULT_LIMIT = 100;
|
|
1983
2151
|
var MAX_LIMIT = 1e3;
|
|
2152
|
+
var MAX_GROUPS = 1e3;
|
|
2153
|
+
var DATE_BUCKET_BY_ENUM = {
|
|
2154
|
+
YEAR: "year",
|
|
2155
|
+
QUARTER: "quarter",
|
|
2156
|
+
MONTH: "month",
|
|
2157
|
+
WEEK: "week",
|
|
2158
|
+
DAY: "day",
|
|
2159
|
+
HOUR: "hour"
|
|
2160
|
+
};
|
|
2161
|
+
var BUCKETABLE_TYPES = /* @__PURE__ */ new Set(["timestamptz", "date"]);
|
|
1984
2162
|
var BOOLEXP_OPS = {
|
|
1985
2163
|
_eq: "eq",
|
|
1986
2164
|
_neq: "neq",
|
|
@@ -2012,6 +2190,14 @@ function fail(message, code, candidates = []) {
|
|
|
2012
2190
|
function argsByName(field) {
|
|
2013
2191
|
return new Map((field.arguments ?? []).map((arg) => [arg.name.value, arg]));
|
|
2014
2192
|
}
|
|
2193
|
+
function* aggregateBlockFields(selectionSet) {
|
|
2194
|
+
for (const node of selectionSet.selections) {
|
|
2195
|
+
if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2196
|
+
if (node.name.value === "__typename") continue;
|
|
2197
|
+
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
2198
|
+
yield node;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2015
2201
|
function assertKnownArgs(field, allowed) {
|
|
2016
2202
|
for (const arg of field.arguments ?? []) {
|
|
2017
2203
|
if (!allowed.includes(arg.name.value)) {
|
|
@@ -2312,11 +2498,8 @@ var OperationLowerer = class {
|
|
|
2312
2498
|
lowerAggregateSelection(selectionSet, table) {
|
|
2313
2499
|
const result = {};
|
|
2314
2500
|
let sawAggregate = false;
|
|
2315
|
-
for (const node of selectionSet
|
|
2316
|
-
if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2501
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2317
2502
|
const name = node.name.value;
|
|
2318
|
-
if (name === "__typename") continue;
|
|
2319
|
-
if (node.alias) fail(`aliases are not supported inside aggregate blocks ('${node.alias.value}')`, "invalid-aggregate");
|
|
2320
2503
|
if (name === "nodes") {
|
|
2321
2504
|
if (!node.selectionSet) fail("'nodes' needs a selection set", "invalid-aggregate");
|
|
2322
2505
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
@@ -2326,48 +2509,116 @@ var OperationLowerer = class {
|
|
|
2326
2509
|
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2327
2510
|
sawAggregate = true;
|
|
2328
2511
|
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2512
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
2513
|
+
}
|
|
2514
|
+
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
2515
|
+
return result;
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* The `{ key {…} aggregate {…} }` block of a grouped aggregate root. Same
|
|
2519
|
+
* `aggregate` sub-block as the ungrouped shape (shared via
|
|
2520
|
+
* `lowerAggregateFields`), but `key` replaces `nodes` — see the IR comment on
|
|
2521
|
+
* `aggregateGroupsRootSchema` for why grouped roots have no `nodes`.
|
|
2522
|
+
*/
|
|
2523
|
+
lowerGroupedAggregateSelection(selectionSet, table, groupBy) {
|
|
2524
|
+
const groupColumns = new Set(groupBy.map((entry) => entry.column));
|
|
2525
|
+
const result = { key: [] };
|
|
2526
|
+
let sawAggregate = false;
|
|
2527
|
+
let sawKey = false;
|
|
2528
|
+
for (const node of aggregateBlockFields(selectionSet)) {
|
|
2529
|
+
const name = node.name.value;
|
|
2530
|
+
if (name === "key") {
|
|
2531
|
+
if (sawKey) fail("one key block per grouped aggregate", "invalid-aggregate");
|
|
2532
|
+
sawKey = true;
|
|
2533
|
+
if (!node.selectionSet) fail("'key' needs a selection set", "invalid-aggregate");
|
|
2534
|
+
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
if (name === "nodes") {
|
|
2538
|
+
fail("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
2539
|
+
}
|
|
2540
|
+
if (name !== "aggregate") fail(`unknown grouped-aggregate field '${name}'`, "invalid-aggregate", ["key", "aggregate"]);
|
|
2541
|
+
if (sawAggregate) fail("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
2542
|
+
sawAggregate = true;
|
|
2543
|
+
if (!node.selectionSet) fail("'aggregate' needs a selection set", "invalid-aggregate");
|
|
2544
|
+
Object.assign(result, this.lowerAggregateFields(node.selectionSet, table));
|
|
2545
|
+
}
|
|
2546
|
+
const rendersAggregate = result.count !== void 0 || (result.sum?.length ?? 0) > 0 || (result.avg?.length ?? 0) > 0;
|
|
2547
|
+
if (result.key.length === 0 && !rendersAggregate) {
|
|
2548
|
+
fail(
|
|
2549
|
+
"grouped aggregate needs at least one key column or an aggregate \u2014 a block selecting only __typename produces nothing",
|
|
2550
|
+
"invalid-aggregate",
|
|
2551
|
+
["key", "aggregate"]
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
return result;
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* `key { month officer }` → the projected group columns. Every field must be
|
|
2558
|
+
* one of the query's own group keys: any other column is indeterminate
|
|
2559
|
+
* within a group, so selecting it is a document error rather than an
|
|
2560
|
+
* arbitrary pick.
|
|
2561
|
+
*/
|
|
2562
|
+
lowerGroupKeySelection(selectionSet, table, groupColumns) {
|
|
2563
|
+
const key = [];
|
|
2564
|
+
for (const node of selectionSet.selections) {
|
|
2565
|
+
if (node.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2566
|
+
const name = node.name.value;
|
|
2567
|
+
if (name === "__typename") continue;
|
|
2568
|
+
if (!this.column(table, name)) {
|
|
2569
|
+
fail(`unknown key column '${name}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
2570
|
+
}
|
|
2571
|
+
if (!groupColumns.has(name)) {
|
|
2572
|
+
fail(
|
|
2573
|
+
`key column '${name}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
2574
|
+
"invalid-group-key",
|
|
2575
|
+
[...groupColumns]
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
key.push({ alias: node.alias?.value ?? name, column: name });
|
|
2579
|
+
}
|
|
2580
|
+
return key;
|
|
2581
|
+
}
|
|
2582
|
+
/** The `count` / `sum {…}` / `avg {…}` fields inside an `aggregate { … }` block. */
|
|
2583
|
+
lowerAggregateFields(selectionSet, table) {
|
|
2584
|
+
const result = {};
|
|
2585
|
+
for (const aggField of aggregateBlockFields(selectionSet)) {
|
|
2586
|
+
const aggName = aggField.name.value;
|
|
2587
|
+
if (aggName === "count") {
|
|
2588
|
+
assertKnownArgs(aggField, ["columns", "distinct"]);
|
|
2589
|
+
const args = argsByName(aggField);
|
|
2590
|
+
const count = {};
|
|
2591
|
+
const columns = args.get("columns");
|
|
2592
|
+
if (columns) count.columns = this.lowerDistinctOn(columns.value, table);
|
|
2593
|
+
const distinct = args.get("distinct");
|
|
2594
|
+
if (distinct) {
|
|
2595
|
+
if (distinct.value.kind !== Kind.BOOLEAN) fail("count(distinct:) must be a boolean literal", "invalid-aggregate");
|
|
2596
|
+
count.distinct = distinct.value.value;
|
|
2347
2597
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2598
|
+
result.count = count;
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
if (aggName === "sum" || aggName === "avg") {
|
|
2602
|
+
if (!aggField.selectionSet) fail(`'${aggName}' needs a column selection`, "invalid-aggregate");
|
|
2603
|
+
const columns = [];
|
|
2604
|
+
for (const columnField of aggField.selectionSet.selections) {
|
|
2605
|
+
if (columnField.kind !== Kind.FIELD) fail("fragments must be flattened before lowering", "internal");
|
|
2606
|
+
if (columnField.name.value === "__typename") continue;
|
|
2607
|
+
const column = this.column(table, columnField.name.value);
|
|
2608
|
+
if (!column || typeof column.type !== "string" || !NUMERIC_TYPES.has(column.type)) {
|
|
2609
|
+
fail(
|
|
2610
|
+
`'${aggName}' supports numeric columns only, got '${columnField.name.value}' on '${table}'`,
|
|
2611
|
+
"invalid-aggregate",
|
|
2612
|
+
this.meta(table).columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type)).map((c) => c.name)
|
|
2613
|
+
);
|
|
2363
2614
|
}
|
|
2364
|
-
|
|
2365
|
-
continue;
|
|
2615
|
+
columns.push(columnField.name.value);
|
|
2366
2616
|
}
|
|
2367
|
-
|
|
2617
|
+
result[aggName] = columns;
|
|
2618
|
+
continue;
|
|
2368
2619
|
}
|
|
2620
|
+
fail(`aggregate '${aggName}' is not supported in v2.0 (count/sum/avg only)`, "invalid-aggregate", ["count", "sum", "avg"]);
|
|
2369
2621
|
}
|
|
2370
|
-
if (!sawAggregate && !result.nodes) fail("aggregate selection needs an aggregate or nodes block", "invalid-aggregate");
|
|
2371
2622
|
return result;
|
|
2372
2623
|
}
|
|
2373
2624
|
// -------------------------------------------------------------------------
|
|
@@ -2377,19 +2628,49 @@ var OperationLowerer = class {
|
|
|
2377
2628
|
const roots = [];
|
|
2378
2629
|
for (const node of operation.selectionSet.selections) {
|
|
2379
2630
|
if (node.kind !== Kind.FIELD) fail("fragments are not supported at the operation root", "invalid-root");
|
|
2631
|
+
if (node.selectionSet) visit(node.selectionSet, {
|
|
2632
|
+
Directive(directive) {
|
|
2633
|
+
if (directive.name.value === "assertAffectedRows") {
|
|
2634
|
+
fail("@assertAffectedRows is supported on mutation roots only", "invalid-affected-row-assertion");
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
});
|
|
2638
|
+
const assertion = this.parseAffectedRowAssertion(node, kind);
|
|
2380
2639
|
const gate = this.parseRootDirectives(node);
|
|
2381
2640
|
if (gate.omit) continue;
|
|
2382
2641
|
const root = kind === "query" ? this.lowerQueryRoot(node) : this.lowerMutationRoot(node);
|
|
2383
|
-
|
|
2642
|
+
const guarded = root.kind === "mutation" && assertion !== void 0 ? { ...root, assertAffectedRows: assertion } : root;
|
|
2643
|
+
roots.push(gate.includeIf ? { ...guarded, includeIf: gate.includeIf } : guarded);
|
|
2384
2644
|
}
|
|
2385
2645
|
if (roots.length === 0) fail("operation has no roots (all statically skipped?)", "invalid-root");
|
|
2386
2646
|
return roots;
|
|
2387
2647
|
}
|
|
2648
|
+
parseAffectedRowAssertion(node, kind) {
|
|
2649
|
+
const assertions = node.directives?.filter((directive) => directive.name.value === "assertAffectedRows") ?? [];
|
|
2650
|
+
if (!assertions.length) return void 0;
|
|
2651
|
+
if (kind !== "mutation" || assertions.length !== 1) {
|
|
2652
|
+
fail("@assertAffectedRows is supported once per mutation root", "invalid-affected-row-assertion");
|
|
2653
|
+
}
|
|
2654
|
+
if (node.directives?.some((directive) => ["include", "skip"].includes(directive.name.value))) {
|
|
2655
|
+
fail("an @assertAffectedRows root cannot use @include or @skip", "invalid-affected-row-assertion");
|
|
2656
|
+
}
|
|
2657
|
+
const args = assertions[0].arguments;
|
|
2658
|
+
const arg = args?.[0];
|
|
2659
|
+
if (args?.length !== 1 || arg?.name.value !== "exactly" || arg.value.kind !== Kind.INT) {
|
|
2660
|
+
fail("@assertAffectedRows(exactly:) requires a document-authored nonnegative Int literal", "invalid-affected-row-assertion");
|
|
2661
|
+
}
|
|
2662
|
+
const count = Number(arg.value.value);
|
|
2663
|
+
if (!Number.isInteger(count) || count < 0 || count > 2147483647) {
|
|
2664
|
+
fail("@assertAffectedRows(exactly:) must be between 0 and 2147483647", "invalid-affected-row-assertion");
|
|
2665
|
+
}
|
|
2666
|
+
return count;
|
|
2667
|
+
}
|
|
2388
2668
|
parseRootDirectives(node) {
|
|
2389
2669
|
let includeIf;
|
|
2390
2670
|
let omit = false;
|
|
2391
2671
|
for (const directive of node.directives ?? []) {
|
|
2392
2672
|
const name = directive.name.value;
|
|
2673
|
+
if (name === "assertAffectedRows") continue;
|
|
2393
2674
|
if (name !== "include" && name !== "skip") {
|
|
2394
2675
|
fail(`directive @${name} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
2395
2676
|
}
|
|
@@ -2438,6 +2719,9 @@ var OperationLowerer = class {
|
|
|
2438
2719
|
const pk = this.lowerPkArgs(node, table);
|
|
2439
2720
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
2440
2721
|
}
|
|
2722
|
+
if (rootInfo.kind === "aggregateGroups") {
|
|
2723
|
+
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
2724
|
+
}
|
|
2441
2725
|
assertKnownArgs(node, ["where"]);
|
|
2442
2726
|
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
2443
2727
|
const whereArg = args.get("where");
|
|
@@ -2450,6 +2734,108 @@ var OperationLowerer = class {
|
|
|
2450
2734
|
...aggregate
|
|
2451
2735
|
};
|
|
2452
2736
|
}
|
|
2737
|
+
/** `<t>AggregateGroups(where:, groupBy:, limit:, offset:)` → an aggregateGroups root. */
|
|
2738
|
+
lowerAggregateGroupsRoot(node, { alias, table }) {
|
|
2739
|
+
assertKnownArgs(node, ["where", "groupBy", "limit", "offset"]);
|
|
2740
|
+
if (!node.selectionSet) fail(`'${node.name.value}' needs a selection set`, "invalid-selection");
|
|
2741
|
+
const args = argsByName(node);
|
|
2742
|
+
const groupByArg = args.get("groupBy");
|
|
2743
|
+
if (!groupByArg) fail(`'${node.name.value}' requires a 'groupBy' argument`, "missing-group-by");
|
|
2744
|
+
const whereArg = args.get("where");
|
|
2745
|
+
const groupBy = this.lowerGroupBy(groupByArg.value, table);
|
|
2746
|
+
const aggregate = this.lowerGroupedAggregateSelection(node.selectionSet, table, groupBy);
|
|
2747
|
+
const offset = this.lowerOffset(args.get("offset")?.value);
|
|
2748
|
+
return {
|
|
2749
|
+
kind: "aggregateGroups",
|
|
2750
|
+
alias,
|
|
2751
|
+
table,
|
|
2752
|
+
where: whereArg ? this.lowerBoolExp(whereArg.value, table) : CONST_TRUE,
|
|
2753
|
+
groupBy,
|
|
2754
|
+
key: aggregate.key,
|
|
2755
|
+
...aggregate.count ? { count: aggregate.count } : {},
|
|
2756
|
+
...aggregate.sum ? { sum: aggregate.sum } : {},
|
|
2757
|
+
...aggregate.avg ? { avg: aggregate.avg } : {},
|
|
2758
|
+
limit: this.lowerGroupLimit(args.get("limit")?.value),
|
|
2759
|
+
...offset ? { offset } : {}
|
|
2760
|
+
};
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* `groupBy: [{ column: startedAt, bucket: MONTH }, …]` → GroupByKeyIr[].
|
|
2764
|
+
*
|
|
2765
|
+
* Document-authored only: a whole-argument variable is rejected rather than
|
|
2766
|
+
* becoming a §4f slot. Group keys choose which expressions reach GROUP BY,
|
|
2767
|
+
* and dynamic-input.ts refuses aggregation shapes from client input on the
|
|
2768
|
+
* same reasoning — `where` stays dynamic, which is what runtime filtering
|
|
2769
|
+
* actually needs.
|
|
2770
|
+
*/
|
|
2771
|
+
lowerGroupBy(node, table) {
|
|
2772
|
+
if (node.kind === Kind.VARIABLE) {
|
|
2773
|
+
fail(
|
|
2774
|
+
`groupBy on '${table}' must be written in the document \u2014 it cannot come from a variable`,
|
|
2775
|
+
"dynamic-group-by"
|
|
2776
|
+
);
|
|
2777
|
+
}
|
|
2778
|
+
const items = node.kind === Kind.LIST ? node.values : [node];
|
|
2779
|
+
if (items.length === 0) fail(`groupBy on '${table}' needs at least one key`, "invalid-group-by");
|
|
2780
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2781
|
+
return items.map((item) => {
|
|
2782
|
+
if (item.kind !== Kind.OBJECT) {
|
|
2783
|
+
fail(`groupBy entries on '${table}' must be objects like { column: foo }`, "invalid-group-by");
|
|
2784
|
+
}
|
|
2785
|
+
const fields = new Map(item.fields.map((field) => [field.name.value, field.value]));
|
|
2786
|
+
for (const key of fields.keys()) {
|
|
2787
|
+
if (key !== "column" && key !== "bucket") {
|
|
2788
|
+
fail(`unknown groupBy field '${key}' on '${table}'`, "invalid-group-by", ["column", "bucket"]);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
const columnNode = fields.get("column");
|
|
2792
|
+
if (!columnNode) fail(`groupBy entries on '${table}' need a 'column'`, "invalid-group-by");
|
|
2793
|
+
if (columnNode.kind !== Kind.ENUM) {
|
|
2794
|
+
fail(`groupBy column on '${table}' must be a group-by column enum value`, "invalid-group-by");
|
|
2795
|
+
}
|
|
2796
|
+
const column = this.column(table, columnNode.value);
|
|
2797
|
+
if (!column) {
|
|
2798
|
+
fail(`unknown groupBy column '${columnNode.value}' on '${table}'`, "unknown-column", this.meta(table).columns.map((c) => c.name));
|
|
2799
|
+
}
|
|
2800
|
+
if (column.type === "jsonb") {
|
|
2801
|
+
fail(`groupBy column '${table}.${columnNode.value}' is jsonb and cannot be a group key`, "invalid-group-by");
|
|
2802
|
+
}
|
|
2803
|
+
const bucketNode = fields.get("bucket");
|
|
2804
|
+
let bucket;
|
|
2805
|
+
if (bucketNode) {
|
|
2806
|
+
if (bucketNode.kind !== Kind.ENUM) {
|
|
2807
|
+
fail(`groupBy bucket on '${table}.${columnNode.value}' must be a DateBucket enum value`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
2808
|
+
}
|
|
2809
|
+
bucket = DATE_BUCKET_BY_ENUM[bucketNode.value];
|
|
2810
|
+
if (!bucket) {
|
|
2811
|
+
fail(`unknown DateBucket '${bucketNode.value}'`, "invalid-group-by", Object.keys(DATE_BUCKET_BY_ENUM));
|
|
2812
|
+
}
|
|
2813
|
+
if (typeof column.type !== "string" || !BUCKETABLE_TYPES.has(column.type)) {
|
|
2814
|
+
fail(
|
|
2815
|
+
`groupBy bucket is only valid on date/timestamptz columns \u2014 '${table}.${columnNode.value}' is ${typeof column.type === "string" ? column.type : "an enum"}`,
|
|
2816
|
+
"invalid-group-by"
|
|
2817
|
+
);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
if (seen.has(columnNode.value)) {
|
|
2821
|
+
fail(`duplicate groupBy column '${columnNode.value}' on '${table}'`, "invalid-group-by");
|
|
2822
|
+
}
|
|
2823
|
+
seen.add(columnNode.value);
|
|
2824
|
+
return bucket ? { column: columnNode.value, bucket } : { column: columnNode.value };
|
|
2825
|
+
});
|
|
2826
|
+
}
|
|
2827
|
+
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
2828
|
+
lowerGroupLimit(node) {
|
|
2829
|
+
const base = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
2830
|
+
if (!node) return base;
|
|
2831
|
+
if (node.kind === Kind.VARIABLE) return { ...base, variable: node.name.value };
|
|
2832
|
+
if (node.kind === Kind.INT) {
|
|
2833
|
+
const value = Number(node.value);
|
|
2834
|
+
if (value < 1) fail("limit must be a positive integer", "invalid-limit");
|
|
2835
|
+
return { ...base, value: Math.min(value, MAX_GROUPS) };
|
|
2836
|
+
}
|
|
2837
|
+
fail("limit must be an integer literal or a variable", "invalid-limit");
|
|
2838
|
+
}
|
|
2453
2839
|
/** byPk-style pk arguments: one argument per primary-key column, exactly. */
|
|
2454
2840
|
lowerPkArgs(node, table) {
|
|
2455
2841
|
const meta = this.meta(table);
|
|
@@ -2959,7 +3345,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2959
3345
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
2960
3346
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
2961
3347
|
operations[restricted.name] = {
|
|
2962
|
-
irVersion: IR_VERSION,
|
|
3348
|
+
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? IR_VERSION : LEGACY_IR_VERSION,
|
|
2963
3349
|
name: restricted.name,
|
|
2964
3350
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
2965
3351
|
kind: restricted.kind,
|
|
@@ -2986,7 +3372,7 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
2986
3372
|
}
|
|
2987
3373
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
2988
3374
|
const artifact = {
|
|
2989
|
-
irVersion:
|
|
3375
|
+
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
2990
3376
|
namespace: ctx.namespace,
|
|
2991
3377
|
schemaHash,
|
|
2992
3378
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|