@sequenceholdings/orm 2.4.0 → 2.6.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 +23 -0
- package/dist/authoring.cjs +53 -7
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +10 -6
- package/dist/authoring.d.ts +10 -6
- package/dist/authoring.js +53 -7
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +48 -2
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +1 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/canonical.js +48 -2
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +260 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +260 -31
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +11 -6
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +38 -31
- package/dist/client.d.ts +38 -31
- package/dist/client.js +11 -7
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +513 -23
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +39 -3
- package/dist/compiler.d.ts +39 -3
- package/dist/compiler.js +510 -24
- package/dist/compiler.js.map +1 -1
- package/dist/{constants-C6EG6Wq7.d.cts → constants-Dou1RkTo.d.cts} +7 -2
- package/dist/{constants-C6EG6Wq7.d.ts → constants-Dou1RkTo.d.ts} +7 -2
- package/dist/dynamic-input.d.cts +1 -1
- package/dist/dynamic-input.d.ts +1 -1
- package/dist/index.cjs +461 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +460 -141
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +55 -4
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +1 -1
- package/dist/ir.d.ts +1 -1
- package/dist/ir.js +55 -4
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +55 -4
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +1 -1
- package/dist/predicate-v2.d.ts +1 -1
- package/dist/predicate-v2.js +55 -4
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +60 -4
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +3 -1
- package/dist/sdl.d.ts +3 -1
- package/dist/sdl.js +60 -4
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -56,18 +56,22 @@ var init_rehearsal_poll = __esm({
|
|
|
56
56
|
});
|
|
57
57
|
|
|
58
58
|
// src/constants.ts
|
|
59
|
-
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
59
|
+
var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
60
60
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
61
61
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
62
62
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
63
63
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
64
|
+
var MAX_PERSISTED_READ_ROWS = 50001;
|
|
64
65
|
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
65
66
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
66
67
|
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
67
68
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
68
69
|
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
69
|
-
var ormSchemaName = (namespace) =>
|
|
70
|
-
|
|
70
|
+
var ormSchemaName = (namespace) => {
|
|
71
|
+
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(`invalid or reserved ORM namespace '${namespace}'`);
|
|
72
|
+
return `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
73
|
+
};
|
|
74
|
+
var consumerRoleName = (namespace) => `${ormSchemaName(namespace)}${CONSUMER_ROLE_SUFFIX}`;
|
|
71
75
|
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
72
76
|
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
73
77
|
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
@@ -535,6 +539,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
535
539
|
function validateSource({ scope, source, table }) {
|
|
536
540
|
const { fail: fail4 } = scope;
|
|
537
541
|
if (source.kind === "etl" && table.source === void 0) fail4(`etl source '${source.name}' must reference a synced table`);
|
|
542
|
+
if (source.kind === "etl" && table.primaryKey?.length === 0) fail4(`etl source '${source.name}' cannot use an unkeyed synced table for resolved row identity`);
|
|
538
543
|
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
539
544
|
fail4(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
540
545
|
}
|
|
@@ -983,7 +988,7 @@ var canonicalTableSchema = z.object({
|
|
|
983
988
|
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
984
989
|
source: z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
985
990
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
986
|
-
primaryKey: z.array(columnIdentifier).
|
|
991
|
+
primaryKey: z.array(columnIdentifier).max(8).optional(),
|
|
987
992
|
links: z.array(canonicalLinkSchema).default([]),
|
|
988
993
|
indexes: z.array(canonicalIndexSchema).default([]),
|
|
989
994
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
@@ -1019,6 +1024,29 @@ var canonicalTableSchema = z.object({
|
|
|
1019
1024
|
* data plane. Omitted otherwise.
|
|
1020
1025
|
*/
|
|
1021
1026
|
materializes: identifier2.optional()
|
|
1027
|
+
}).superRefine((table, ctx) => {
|
|
1028
|
+
if (table.primaryKey?.length !== 0) return;
|
|
1029
|
+
const fail4 = (message, path = ["primaryKey"]) => ctx.addIssue({ code: "custom", path, message: `unkeyed table '${table.table}' ${message}` });
|
|
1030
|
+
if (table.source === void 0 || table.columns.length === 0) {
|
|
1031
|
+
fail4("requires a typed synced source");
|
|
1032
|
+
}
|
|
1033
|
+
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
1034
|
+
fail4("is read-only and cannot declare write policies", ["policies"]);
|
|
1035
|
+
}
|
|
1036
|
+
const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
1037
|
+
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
1038
|
+
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
1039
|
+
fail4("cannot declare owned storage, constraints or identity operations");
|
|
1040
|
+
}
|
|
1041
|
+
for (const [index3, column] of table.columns.entries()) {
|
|
1042
|
+
const ownedColumnMetadata = [column.default, column.defaultFn, column.onUpdateFn, column.renamedFrom, column.cast, column.cell];
|
|
1043
|
+
if (column.unique || ownedColumnMetadata.some((value) => value !== void 0)) {
|
|
1044
|
+
fail4("cannot declare column uniqueness, mutation defaults or migration metadata", ["columns", index3]);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
for (const [index3, link] of table.links.entries()) {
|
|
1048
|
+
if (link.enforce || link.kind === "toMany") fail4("cannot provide an identity for this link", ["links", index3]);
|
|
1049
|
+
}
|
|
1022
1050
|
});
|
|
1023
1051
|
var MAX_VIEW_QUERY = 1e5;
|
|
1024
1052
|
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
@@ -1075,6 +1103,26 @@ var canonicalViewSchema = z.object({
|
|
|
1075
1103
|
}
|
|
1076
1104
|
}
|
|
1077
1105
|
});
|
|
1106
|
+
function validateUnkeyedLinkIdentities({ definition, ctx }) {
|
|
1107
|
+
const tables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1108
|
+
const relations = [
|
|
1109
|
+
...definition.tables.map((table, index3) => ({ kind: "tables", index: index3, links: table.links })),
|
|
1110
|
+
...(definition.views ?? []).map((view, index3) => ({ kind: "views", index: index3, links: view.links }))
|
|
1111
|
+
];
|
|
1112
|
+
for (const relation of relations) {
|
|
1113
|
+
for (const [linkIndex, link] of relation.links.entries()) {
|
|
1114
|
+
if (link.kind !== "toOne" || link.namespace !== void 0 && link.namespace !== definition.namespace) continue;
|
|
1115
|
+
const target = link.target === void 0 ? void 0 : tables.get(link.target);
|
|
1116
|
+
if (target?.source !== void 0 && target.primaryKey?.length === 0) {
|
|
1117
|
+
ctx.addIssue({
|
|
1118
|
+
code: "custom",
|
|
1119
|
+
path: [relation.kind, relation.index, "links", linkIndex],
|
|
1120
|
+
message: `link '${link.name}' cannot use unkeyed table '${target.table}' as a to-one identity`
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1078
1126
|
var CELL_PART_TYPES = {
|
|
1079
1127
|
at: "timestamptz",
|
|
1080
1128
|
by: "text",
|
|
@@ -1138,7 +1186,7 @@ var exportGateSchema = z.union([z.literal(true), z.object({ capability: z.string
|
|
|
1138
1186
|
var relationExportSchema = z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
1139
1187
|
var localDefinitionSchema = z.object({
|
|
1140
1188
|
definition_format: z.literal(CANONICAL_FORMAT),
|
|
1141
|
-
namespace: identifier2.max(41),
|
|
1189
|
+
namespace: identifier2.max(41).regex(NAMESPACE_PATTERN, "invalid or reserved ORM namespace"),
|
|
1142
1190
|
description: z.string().max(2e3).optional(),
|
|
1143
1191
|
enums: z.array(canonicalEnumSchema).default([]),
|
|
1144
1192
|
tables: z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -1172,6 +1220,7 @@ var localDefinitionSchema = z.object({
|
|
|
1172
1220
|
}
|
|
1173
1221
|
}
|
|
1174
1222
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1223
|
+
validateUnkeyedLinkIdentities({ definition, ctx });
|
|
1175
1224
|
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
1176
1225
|
for (const name2 of Object.keys(definition.exports ?? {})) {
|
|
1177
1226
|
if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
|
|
@@ -1406,6 +1455,9 @@ function normalizeCanonical(definition) {
|
|
|
1406
1455
|
return normalized;
|
|
1407
1456
|
}
|
|
1408
1457
|
|
|
1458
|
+
// src/client/sql.ts
|
|
1459
|
+
var BUILTIN_SQL_READ_HASH = "sha256:a50c477c952a5e0fe68ed158a4530e4aab573c09bcbb1a8ac454dc88a39a6258";
|
|
1460
|
+
|
|
1409
1461
|
// src/ir/index.ts
|
|
1410
1462
|
var IR_VERSION = 5;
|
|
1411
1463
|
var LEGACY_IR_VERSION = 1;
|
|
@@ -1494,7 +1546,7 @@ var limitIrSchema = z.object({
|
|
|
1494
1546
|
variable: z.string().optional(),
|
|
1495
1547
|
value: z.number().int().positive().optional(),
|
|
1496
1548
|
default: z.number().int().positive(),
|
|
1497
|
-
max: z.number().int().positive()
|
|
1549
|
+
max: z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
|
|
1498
1550
|
});
|
|
1499
1551
|
var selectionIrSchema = z.lazy(
|
|
1500
1552
|
() => z.object({
|
|
@@ -1739,7 +1791,8 @@ var compiledOperationSchema = z.object({
|
|
|
1739
1791
|
}).superRefine((operation, ctx) => {
|
|
1740
1792
|
if (operation.language === "sql") {
|
|
1741
1793
|
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
1742
|
-
|
|
1794
|
+
const builtin = operation.name === "BuiltinSqlRead" && operation.opHash === BUILTIN_SQL_READ_HASH && operation.requires === null;
|
|
1795
|
+
if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !builtin && !operation.requires?.capability) {
|
|
1743
1796
|
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
1744
1797
|
}
|
|
1745
1798
|
} else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
|
|
@@ -3118,6 +3171,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3118
3171
|
const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
|
|
3119
3172
|
if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
|
|
3120
3173
|
const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
|
|
3174
|
+
const unkeyedSyncedTableNames = new Set(relations.filter((table) => table.source !== void 0 && table.primaryKey?.length === 0).map((table) => table.table));
|
|
3121
3175
|
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
3122
3176
|
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
3123
3177
|
const relationIsClaimsGated = (name2) => {
|
|
@@ -3131,6 +3185,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3131
3185
|
};
|
|
3132
3186
|
for (const table of relations) {
|
|
3133
3187
|
const isView = viewNames.has(table.table);
|
|
3188
|
+
const readOnly = isView || unkeyedSyncedTableNames.has(table.table);
|
|
3134
3189
|
const owner = owners.get(table.table);
|
|
3135
3190
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
3136
3191
|
columnEntries.set(table.table, entries);
|
|
@@ -3178,13 +3233,13 @@ function buildCompileContext(rawCanonical) {
|
|
|
3178
3233
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
3179
3234
|
claimsGated: relationIsClaimsGated(table.table),
|
|
3180
3235
|
columns: entries.map(({ deprecated: _deprecated, onUpdateFn: _onUpdateFn, ...column }) => column),
|
|
3181
|
-
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
3236
|
+
primaryKey: isView || unkeyedSyncedTableNames.has(table.table) ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
3182
3237
|
links: [],
|
|
3183
3238
|
filterable: data.map((c) => c.name),
|
|
3184
3239
|
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
3185
|
-
insertable: resolved ? resolvedWritable(data) :
|
|
3186
|
-
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) :
|
|
3187
|
-
appendable:
|
|
3240
|
+
insertable: resolved ? resolvedWritable(data) : readOnly ? [] : active.map((c) => c.name),
|
|
3241
|
+
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey2.includes(name2)) : readOnly ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
3242
|
+
appendable: readOnly ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
3188
3243
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
3189
3244
|
...cells ? { cells } : {},
|
|
3190
3245
|
...resolved ? { resolved: resolvedMeta({ resolved, pk: primaryKey2[0] }) } : {},
|
|
@@ -3208,6 +3263,16 @@ function buildCompileContext(rawCanonical) {
|
|
|
3208
3263
|
});
|
|
3209
3264
|
continue;
|
|
3210
3265
|
}
|
|
3266
|
+
const identityTable = link.kind === "toOne" ? targetName : link.kind === "toMany" ? table.table : void 0;
|
|
3267
|
+
if (identityTable !== void 0 && unkeyedSyncedTableNames.has(identityTable)) {
|
|
3268
|
+
diagnostics.push({
|
|
3269
|
+
severity: "error",
|
|
3270
|
+
code: "unkeyed-link-identity",
|
|
3271
|
+
table: table.table,
|
|
3272
|
+
message: `link '${link.name}' cannot use unkeyed table '${identityTable}' as a row identity`
|
|
3273
|
+
});
|
|
3274
|
+
continue;
|
|
3275
|
+
}
|
|
3211
3276
|
const targetEntries = targetName === PLATFORM_USER_TABLE ? target.columns.map((c) => ({ ...c, deprecated: false, onUpdateFn: false })) : columnEntries.get(targetName);
|
|
3212
3277
|
const onEntries = link.kind === "toMany" ? targetEntries : entries;
|
|
3213
3278
|
const referencesEntries = link.kind === "toMany" ? entries : targetEntries;
|
|
@@ -3273,7 +3338,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3273
3338
|
compilePolicyFor("write", writePolicies);
|
|
3274
3339
|
compilePolicyFor("insert", insertPolicies);
|
|
3275
3340
|
compilePolicyFor("update", updatePolicies);
|
|
3276
|
-
if (isView && table.resolved === void 0) {
|
|
3341
|
+
if (isView && table.resolved === void 0 || unkeyedSyncedTableNames.has(table.table)) {
|
|
3277
3342
|
writePolicies.set(table.table, CONST_FALSE);
|
|
3278
3343
|
insertPolicies.set(table.table, CONST_FALSE);
|
|
3279
3344
|
updatePolicies.set(table.table, CONST_FALSE);
|
|
@@ -3348,7 +3413,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
3348
3413
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
3349
3414
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
3350
3415
|
for (const table of relations) {
|
|
3351
|
-
if (table.internal === true) continue;
|
|
3416
|
+
if (table.internal === true || unkeyedSyncedTableNames.has(table.table)) continue;
|
|
3352
3417
|
const roots = queryRootNames(table.table);
|
|
3353
3418
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
3354
3419
|
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
@@ -3728,8 +3793,10 @@ var OperationLowerer = class {
|
|
|
3728
3793
|
constructor(ctx, restricted) {
|
|
3729
3794
|
this.ctx = ctx;
|
|
3730
3795
|
this.variables = new Map(restricted.variables.map((v) => [v.name, v]));
|
|
3796
|
+
this.maxRows = restricted.readBudget ?? MAX_LIMIT;
|
|
3731
3797
|
}
|
|
3732
3798
|
variables;
|
|
3799
|
+
maxRows;
|
|
3733
3800
|
meta(table) {
|
|
3734
3801
|
return this.ctx.tables[table];
|
|
3735
3802
|
}
|
|
@@ -3933,13 +4000,13 @@ var OperationLowerer = class {
|
|
|
3933
4000
|
});
|
|
3934
4001
|
}
|
|
3935
4002
|
lowerLimit(node) {
|
|
3936
|
-
const base2 = { default: DEFAULT_LIMIT, max:
|
|
4003
|
+
const base2 = { default: DEFAULT_LIMIT, max: this.maxRows };
|
|
3937
4004
|
if (!node) return base2;
|
|
3938
4005
|
if (node.kind === Kind.VARIABLE) return { ...base2, variable: node.name.value };
|
|
3939
4006
|
if (node.kind === Kind.INT) {
|
|
3940
4007
|
const value = Number(node.value);
|
|
3941
4008
|
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3942
|
-
return { ...base2, value: Math.min(value,
|
|
4009
|
+
return { ...base2, value: Math.min(value, this.maxRows) };
|
|
3943
4010
|
}
|
|
3944
4011
|
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3945
4012
|
}
|
|
@@ -4738,7 +4805,13 @@ var OperationLowerer = class {
|
|
|
4738
4805
|
};
|
|
4739
4806
|
function lowerOperation({ restricted, ctx }) {
|
|
4740
4807
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
4741
|
-
|
|
4808
|
+
const roots = lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
4809
|
+
if (restricted.readBudget === void 0) return roots;
|
|
4810
|
+
const root = roots[0];
|
|
4811
|
+
if (restricted.kind !== "query" || restricted.definition.selectionSet.selections.length !== 1 || roots.length !== 1 || root?.kind !== "select" || root.selection.relations.length > 0 || root.selection.aggregates.length > 0 || root.selection.cells !== void 0 || root.prefetch?.length) {
|
|
4812
|
+
fail2("@readBudget requires one flat select root without related selections, aggregates, cells, or prefetch", "invalid-read-budget");
|
|
4813
|
+
}
|
|
4814
|
+
return roots;
|
|
4742
4815
|
}
|
|
4743
4816
|
function isCellColumn(meta, name2) {
|
|
4744
4817
|
if (!meta.cells) return false;
|
|
@@ -4797,6 +4870,19 @@ function parseDocuments({
|
|
|
4797
4870
|
}
|
|
4798
4871
|
return { documents: parsed, diagnostics };
|
|
4799
4872
|
}
|
|
4873
|
+
function parseReadBudget(operation) {
|
|
4874
|
+
const directives = operation.directives?.filter((directive) => directive.name.value === "readBudget") ?? [];
|
|
4875
|
+
if (directives.length === 0) return void 0;
|
|
4876
|
+
const value = directives[0]?.arguments?.find((argument) => argument.name.value === "maxRows")?.value;
|
|
4877
|
+
if (operation.operation !== "query" || directives.length !== 1 || value?.kind !== Kind.INT) {
|
|
4878
|
+
throw new DocumentCompileError("@readBudget requires a literal maxRows on a query", "invalid-read-budget");
|
|
4879
|
+
}
|
|
4880
|
+
const maxRows = Number(value.value);
|
|
4881
|
+
if (!Number.isSafeInteger(maxRows) || maxRows < 1 || maxRows > MAX_PERSISTED_READ_ROWS) {
|
|
4882
|
+
throw new DocumentCompileError(`@readBudget(maxRows:) must be between 1 and ${MAX_PERSISTED_READ_ROWS}`, "invalid-read-budget");
|
|
4883
|
+
}
|
|
4884
|
+
return maxRows;
|
|
4885
|
+
}
|
|
4800
4886
|
function unwrapType(node, variable) {
|
|
4801
4887
|
let nullable = true;
|
|
4802
4888
|
let list2 = false;
|
|
@@ -4957,6 +5043,7 @@ function restrictOperation({
|
|
|
4957
5043
|
name: name2,
|
|
4958
5044
|
kind: operation.operation === "mutation" ? "mutation" : "query",
|
|
4959
5045
|
requires: parseRequires(operation),
|
|
5046
|
+
readBudget: parseReadBudget(operation),
|
|
4960
5047
|
variables,
|
|
4961
5048
|
definition: operation
|
|
4962
5049
|
};
|
|
@@ -5087,9 +5174,9 @@ function compilePredIrToSql(pred, options = {}) {
|
|
|
5087
5174
|
case "cmp":
|
|
5088
5175
|
return compileCmp(pred.op, pred.column, pred.value, options);
|
|
5089
5176
|
case "capability":
|
|
5090
|
-
return `(${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5177
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5091
5178
|
case "role":
|
|
5092
|
-
return `(${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5179
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5093
5180
|
case "exists":
|
|
5094
5181
|
if (options.exists) return options.exists(pred);
|
|
5095
5182
|
throw new OrmDefinitionError("synced view read policy cannot use 'exists' without a relationship resolver");
|
|
@@ -5197,7 +5284,8 @@ function createSqlCatalog({ canonical, publishedObjects, executionTarget }) {
|
|
|
5197
5284
|
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
5198
5285
|
if (diagnostics.some((d) => d.severity === "error")) throw new Error(diagnostics.map((d) => d.message).join("; "));
|
|
5199
5286
|
const identity = sqlHash(`ontology-sql-v1\0${computeSchemaHash(canonical)}\0${executionTarget}`);
|
|
5200
|
-
const
|
|
5287
|
+
const logicalNames = new Map(Object.entries(ctx.tables).map(([key, meta]) => [meta, key]));
|
|
5288
|
+
const refOf = (meta) => `${meta.namespace ?? canonical.namespace}/${logicalNames.get(meta)}`;
|
|
5201
5289
|
const definitions = [canonical, ...canonical.imports ?? []];
|
|
5202
5290
|
const internal = new Set(definitions.flatMap((owner) => [
|
|
5203
5291
|
...owner.tables.filter((t2) => t2.internal).map((t2) => `${owner.namespace}/${t2.table}`),
|
|
@@ -5273,15 +5361,22 @@ function only(node, keys) {
|
|
|
5273
5361
|
var parser;
|
|
5274
5362
|
var SQL_TYPES = ["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"];
|
|
5275
5363
|
var pgType = (type) => type === "double" ? "double precision" : SQL_TYPES.some((t2) => t2 === type) ? type : fail3("unsupported SQL type");
|
|
5276
|
-
async function
|
|
5277
|
-
if (
|
|
5364
|
+
async function parseSelect(sql4) {
|
|
5365
|
+
if (Buffer.byteLength(sql4) > 65536) fail3("SQL limit exceeded");
|
|
5278
5366
|
parser ??= new PgParser({ version: 17 });
|
|
5279
5367
|
const parsed = await parser.parse(sql4);
|
|
5280
5368
|
if (parsed.error || !parsed.tree) fail3("invalid SQL");
|
|
5281
5369
|
const statements = list(obj(parsed.tree).stmts);
|
|
5282
5370
|
if (statements.length !== 1) fail3("one SQL SELECT is required");
|
|
5371
|
+
const statement = obj(obj(statements[0]).stmt);
|
|
5372
|
+
if (!statement.SelectStmt) fail3("one SQL SELECT is required");
|
|
5373
|
+
return statement;
|
|
5374
|
+
}
|
|
5375
|
+
async function compileOntologySql({ catalog, sql: sql4, parameterTypes = [], maxRows = 1e3 }) {
|
|
5376
|
+
if (!Number.isInteger(maxRows) || maxRows < 1 || maxRows > 1e4 || Buffer.byteLength(sql4) > 65536) fail3("SQL limit exceeded");
|
|
5377
|
+
const statement = await parseSelect(sql4);
|
|
5283
5378
|
const emitter = new SqlEmitter({ catalog, parameterTypes });
|
|
5284
|
-
const result = emitter.select(
|
|
5379
|
+
const result = emitter.select(statement, { relations: [], ctes: /* @__PURE__ */ new Map() });
|
|
5285
5380
|
if (emitter.used.size !== parameterTypes.length) fail3("SQL parameters must be used exactly as declared");
|
|
5286
5381
|
if (!emitter.read.size) fail3("SQL operation must read an ontology object");
|
|
5287
5382
|
return {
|
|
@@ -5614,6 +5709,118 @@ function bindNamedParameters({ sql: sql4, names }) {
|
|
|
5614
5709
|
}
|
|
5615
5710
|
return result;
|
|
5616
5711
|
}
|
|
5712
|
+
var parser2;
|
|
5713
|
+
var functions = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
5714
|
+
var scalarTypes = /* @__PURE__ */ new Set([
|
|
5715
|
+
"bool",
|
|
5716
|
+
"boolean",
|
|
5717
|
+
"int2",
|
|
5718
|
+
"int4",
|
|
5719
|
+
"int8",
|
|
5720
|
+
"integer",
|
|
5721
|
+
"bigint",
|
|
5722
|
+
"smallint",
|
|
5723
|
+
"numeric",
|
|
5724
|
+
"decimal",
|
|
5725
|
+
"float4",
|
|
5726
|
+
"float8",
|
|
5727
|
+
"real",
|
|
5728
|
+
"text",
|
|
5729
|
+
"varchar",
|
|
5730
|
+
"bpchar",
|
|
5731
|
+
"uuid",
|
|
5732
|
+
"date",
|
|
5733
|
+
"timestamp",
|
|
5734
|
+
"timestamptz",
|
|
5735
|
+
"time",
|
|
5736
|
+
"timetz",
|
|
5737
|
+
"interval",
|
|
5738
|
+
"json",
|
|
5739
|
+
"jsonb",
|
|
5740
|
+
"bytea"
|
|
5741
|
+
]);
|
|
5742
|
+
var sqlValues = /* @__PURE__ */ new Set([
|
|
5743
|
+
"SVFOP_CURRENT_DATE",
|
|
5744
|
+
"SVFOP_CURRENT_TIME",
|
|
5745
|
+
"SVFOP_CURRENT_TIME_N",
|
|
5746
|
+
"SVFOP_CURRENT_TIMESTAMP",
|
|
5747
|
+
"SVFOP_CURRENT_TIMESTAMP_N",
|
|
5748
|
+
"SVFOP_LOCALTIME",
|
|
5749
|
+
"SVFOP_LOCALTIME_N",
|
|
5750
|
+
"SVFOP_LOCALTIMESTAMP",
|
|
5751
|
+
"SVFOP_LOCALTIMESTAMP_N",
|
|
5752
|
+
"SVFOP_CURRENT_ROLE",
|
|
5753
|
+
"SVFOP_CURRENT_USER",
|
|
5754
|
+
"SVFOP_USER",
|
|
5755
|
+
"SVFOP_SESSION_USER",
|
|
5756
|
+
"SVFOP_CURRENT_CATALOG",
|
|
5757
|
+
"SVFOP_CURRENT_SCHEMA"
|
|
5758
|
+
]);
|
|
5759
|
+
async function validateManagedViewQueries({ canonical }) {
|
|
5760
|
+
for (const definition of [canonical, ...canonical.imports ?? []]) {
|
|
5761
|
+
for (const view of definition.views ?? []) {
|
|
5762
|
+
const fail4 = (message) => {
|
|
5763
|
+
throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
|
|
5764
|
+
};
|
|
5765
|
+
parser2 ??= new PgParser({ version: 17 });
|
|
5766
|
+
const parsed = await parser2.parse(view.query);
|
|
5767
|
+
if (parsed.error || !parsed.tree) fail4("invalid SELECT query");
|
|
5768
|
+
const statements = list(obj(parsed.tree).stmts);
|
|
5769
|
+
if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt) fail4("one SELECT query is required");
|
|
5770
|
+
validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail: fail4 });
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
function pgCatalogName(parts) {
|
|
5775
|
+
if (parts.length === 1) return parts[0];
|
|
5776
|
+
if (parts.length === 2 && parts[0] === "pg_catalog") return parts[1];
|
|
5777
|
+
return void 0;
|
|
5778
|
+
}
|
|
5779
|
+
function reviewedFunctionName({ parts, format }) {
|
|
5780
|
+
return format === "COERCE_SQL_SYNTAX" && parts.length === 2 && parts[0] === "pg_catalog" && parts[1] === "position" ? "strpos" : pgCatalogName(parts);
|
|
5781
|
+
}
|
|
5782
|
+
function validateTree({ tree, ...scope }) {
|
|
5783
|
+
const pending = [tree];
|
|
5784
|
+
while (pending.length) {
|
|
5785
|
+
const value = pending.pop();
|
|
5786
|
+
if (Array.isArray(value)) {
|
|
5787
|
+
pending.push(...value);
|
|
5788
|
+
continue;
|
|
5789
|
+
}
|
|
5790
|
+
if (!value || typeof value !== "object") continue;
|
|
5791
|
+
const node = obj(value);
|
|
5792
|
+
for (const [kind, child] of Object.entries(node)) {
|
|
5793
|
+
validateNode({ kind, child, scope });
|
|
5794
|
+
pending.push(child);
|
|
5795
|
+
}
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
function validateNode({ kind, child, scope }) {
|
|
5799
|
+
const { fail: fail4 } = scope;
|
|
5800
|
+
if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail4("only SELECT statements are permitted");
|
|
5801
|
+
if (kind === "FuncCall") {
|
|
5802
|
+
const call = obj(child);
|
|
5803
|
+
const parts = strings(call.funcname);
|
|
5804
|
+
const name2 = reviewedFunctionName({ parts, format: call.funcformat });
|
|
5805
|
+
if (!name2 || !functions.has(name2)) fail4(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
|
|
5806
|
+
}
|
|
5807
|
+
if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
|
|
5808
|
+
if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0) fail4("only pg_catalog operators are permitted");
|
|
5809
|
+
if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0) fail4("only pg_catalog sort operators are permitted");
|
|
5810
|
+
if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op))) fail4("unsupported SQL value expression");
|
|
5811
|
+
if (kind === "ParamRef") fail4("view queries cannot contain parameters");
|
|
5812
|
+
if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
|
|
5813
|
+
fail4("composite field notation is not permitted in managed views");
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5816
|
+
function validateType({ node, schema, enums, fail: fail4 }) {
|
|
5817
|
+
const parts = strings(node.names);
|
|
5818
|
+
const builtin = pgCatalogName(parts);
|
|
5819
|
+
const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
|
|
5820
|
+
if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
|
|
5821
|
+
fail4(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5617
5824
|
|
|
5618
5825
|
// src/sql/operations.ts
|
|
5619
5826
|
function isSqlOperationDocument(path, contents) {
|
|
@@ -5629,7 +5836,13 @@ function isSqlOperationDocument(path, contents) {
|
|
|
5629
5836
|
async function compileOperationDocuments(input) {
|
|
5630
5837
|
const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
|
|
5631
5838
|
const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
|
|
5632
|
-
if (!result.ok
|
|
5839
|
+
if (!result.ok) return result;
|
|
5840
|
+
const viewDiagnostics = await managedViewDiagnostics(input.canonical);
|
|
5841
|
+
if (viewDiagnostics.length) return { ok: false, diagnostics: [...result.diagnostics, ...viewDiagnostics] };
|
|
5842
|
+
if (!sqlDocuments.length) return result;
|
|
5843
|
+
return compileSqlDocuments({ input, result, sqlDocuments });
|
|
5844
|
+
}
|
|
5845
|
+
async function compileSqlDocuments({ input, result, sqlDocuments }) {
|
|
5633
5846
|
for (const document of sqlDocuments) {
|
|
5634
5847
|
try {
|
|
5635
5848
|
const definition = defineOperation(JSON.parse(document.contents));
|
|
@@ -5723,6 +5936,18 @@ async function compileOperationDocuments(input) {
|
|
|
5723
5936
|
result.manifest.opsHash = result.artifact.opsHash;
|
|
5724
5937
|
return { ...result, artifact: normalizeIr(result.artifact) };
|
|
5725
5938
|
}
|
|
5939
|
+
async function managedViewDiagnostics(canonical) {
|
|
5940
|
+
try {
|
|
5941
|
+
await validateManagedViewQueries({ canonical });
|
|
5942
|
+
return [];
|
|
5943
|
+
} catch (error) {
|
|
5944
|
+
return [{
|
|
5945
|
+
severity: "error",
|
|
5946
|
+
code: "invalid-view-query",
|
|
5947
|
+
message: error instanceof Error ? error.message : "invalid managed view query"
|
|
5948
|
+
}];
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5726
5951
|
|
|
5727
5952
|
// src/sql/codegen.ts
|
|
5728
5953
|
var typeOf = (type) => ["integer", "double"].includes(type) ? "number" : type === "boolean" ? "boolean" : "string";
|
|
@@ -7570,19 +7795,19 @@ function compileComparison(op, left, right, columnTypes) {
|
|
|
7570
7795
|
}
|
|
7571
7796
|
return `${compileOperand(left, right, columnTypes)} ${SQL_COMPARE_OPS[op]} ${compileOperand(right, left, columnTypes)}`;
|
|
7572
7797
|
}
|
|
7573
|
-
function compilePredicate(predicate, columnTypes) {
|
|
7798
|
+
function compilePredicate(predicate, columnTypes, options = {}) {
|
|
7574
7799
|
switch (predicate.kind) {
|
|
7575
7800
|
case "and":
|
|
7576
7801
|
case "or": {
|
|
7577
|
-
const joined = predicate.operands.map((operand) => compilePredicate(operand, columnTypes)).join(` ${predicate.kind.toUpperCase()} `);
|
|
7802
|
+
const joined = predicate.operands.map((operand) => compilePredicate(operand, columnTypes, options)).join(` ${predicate.kind.toUpperCase()} `);
|
|
7578
7803
|
return `(${joined})`;
|
|
7579
7804
|
}
|
|
7580
7805
|
case "not":
|
|
7581
|
-
return `(NOT ${compilePredicate(predicate.operand, columnTypes)})`;
|
|
7806
|
+
return `(NOT ${compilePredicate(predicate.operand, columnTypes, options)})`;
|
|
7582
7807
|
case "compare":
|
|
7583
7808
|
return `(${compileComparison(predicate.op, predicate.left, predicate.right, columnTypes)})`;
|
|
7584
7809
|
case "capability":
|
|
7585
|
-
return `(${capabilityExpression(predicate.key)})`;
|
|
7810
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(predicate.key)})`;
|
|
7586
7811
|
}
|
|
7587
7812
|
}
|
|
7588
7813
|
|
|
@@ -7818,7 +8043,7 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
7818
8043
|
// src/engine/materialized-refresh.ts
|
|
7819
8044
|
function materializedRefreshFunctions(canonical) {
|
|
7820
8045
|
const schema = ormSchemaName(canonical.namespace);
|
|
7821
|
-
const
|
|
8046
|
+
const functions2 = /* @__PURE__ */ new Map();
|
|
7822
8047
|
for (const view of canonical.views ?? []) {
|
|
7823
8048
|
const materialized = view.resolved?.materialized;
|
|
7824
8049
|
if (!materialized) continue;
|
|
@@ -7827,7 +8052,7 @@ function materializedRefreshFunctions(canonical) {
|
|
|
7827
8052
|
if (!table || pk.length === 0) {
|
|
7828
8053
|
throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
|
|
7829
8054
|
}
|
|
7830
|
-
|
|
8055
|
+
functions2.set(view.view, refreshFunctionSql({
|
|
7831
8056
|
schema,
|
|
7832
8057
|
name: view.view,
|
|
7833
8058
|
table: materialized.table,
|
|
@@ -7836,7 +8061,7 @@ function materializedRefreshFunctions(canonical) {
|
|
|
7836
8061
|
pk
|
|
7837
8062
|
}));
|
|
7838
8063
|
}
|
|
7839
|
-
return
|
|
8064
|
+
return functions2;
|
|
7840
8065
|
}
|
|
7841
8066
|
function refreshFunctionSql({ schema, name: name2, table, compute, columns, pk }) {
|
|
7842
8067
|
const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name2))}`;
|
|
@@ -8480,6 +8705,7 @@ async function planWithViewRecreation({
|
|
|
8480
8705
|
previousSnapshot,
|
|
8481
8706
|
viewRecreation
|
|
8482
8707
|
}) {
|
|
8708
|
+
await validateManagedViewQueries({ canonical });
|
|
8483
8709
|
const schema = ormSchemaName(canonical.namespace);
|
|
8484
8710
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
8485
8711
|
assertValidViewDependencies(canonical);
|
|
@@ -9770,6 +9996,9 @@ function generateSdl(canonical, options = {}) {
|
|
|
9770
9996
|
blocks.push(
|
|
9771
9997
|
'"""Gates an operation behind a platform capability or custom role \u2014 checked by the data plane before execution."""\ndirective @requires(capability: String, role: String) on QUERY | MUTATION'
|
|
9772
9998
|
);
|
|
9999
|
+
if (!options.omitReadBudgetDirective) blocks.push(
|
|
10000
|
+
'"""Sets a document-authored ceiling of 1\u201350001 rows for one flat select root. Runtime variables cannot raise it."""\ndirective @readBudget(maxRows: Int!) on QUERY'
|
|
10001
|
+
);
|
|
9773
10002
|
blocks.push("directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT");
|
|
9774
10003
|
blocks.push(
|
|
9775
10004
|
'"""Abort the entire mutation unless this root affects exactly the document-authored number of rows. Mutation roots only; cannot be conditional."""\ndirective @assertAffectedRows(exactly: Int!) on FIELD'
|
|
@@ -11974,7 +12203,7 @@ function emitSyncedTable(table) {
|
|
|
11974
12203
|
lines.push(" },", " {");
|
|
11975
12204
|
lines.push(` source: ${quote(table.source ?? "")},`);
|
|
11976
12205
|
if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
|
|
11977
|
-
if (singlePrimaryKey === void 0 && table.primaryKey
|
|
12206
|
+
if (singlePrimaryKey === void 0 && table.primaryKey !== void 0) {
|
|
11978
12207
|
lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
|
|
11979
12208
|
}
|
|
11980
12209
|
if (table.links.length > 0) {
|