@sequenceholdings/orm 2.9.0 → 2.11.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.
Files changed (50) hide show
  1. package/README.md +71 -0
  2. package/dist/authoring.cjs +796 -7
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +11 -2
  5. package/dist/authoring.d.ts +11 -2
  6. package/dist/authoring.js +796 -7
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +777 -6
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +43 -43
  11. package/dist/canonical.d.ts +43 -43
  12. package/dist/canonical.js +777 -6
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +7585 -7512
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.d.cts +10 -1
  17. package/dist/cli.d.ts +10 -1
  18. package/dist/cli.js +7584 -7513
  19. package/dist/cli.js.map +1 -1
  20. package/dist/compiler.cjs +1059 -50
  21. package/dist/compiler.cjs.map +1 -1
  22. package/dist/compiler.d.cts +182 -2
  23. package/dist/compiler.d.ts +182 -2
  24. package/dist/compiler.js +1054 -51
  25. package/dist/compiler.js.map +1 -1
  26. package/dist/index.cjs +3720 -3447
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +96 -31
  29. package/dist/index.d.ts +96 -31
  30. package/dist/index.js +3714 -3446
  31. package/dist/index.js.map +1 -1
  32. package/dist/ir.cjs +787 -14
  33. package/dist/ir.cjs.map +1 -1
  34. package/dist/ir.d.cts +70 -59
  35. package/dist/ir.d.ts +70 -59
  36. package/dist/ir.js +787 -15
  37. package/dist/ir.js.map +1 -1
  38. package/dist/predicate-v2.cjs +802 -30
  39. package/dist/predicate-v2.cjs.map +1 -1
  40. package/dist/predicate-v2.d.cts +2 -0
  41. package/dist/predicate-v2.d.ts +2 -0
  42. package/dist/predicate-v2.js +802 -30
  43. package/dist/predicate-v2.js.map +1 -1
  44. package/dist/sdl.cjs +794 -24
  45. package/dist/sdl.cjs.map +1 -1
  46. package/dist/sdl.js +794 -24
  47. package/dist/sdl.js.map +1 -1
  48. package/package.json +1 -1
  49. package/dist/{definition-B99BsOAO.d.cts → definition-CevES7lM.d.cts} +2 -2
  50. package/dist/{definition-B99BsOAO.d.ts → definition-CevES7lM.d.ts} +2 -2
@@ -30,11 +30,16 @@ var PolicyCompileError = class extends Error {
30
30
 
31
31
  // src/constants.ts
32
32
  var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
33
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
33
34
  var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
34
35
  var MAX_IDENTIFIER_LENGTH = 63;
35
36
  var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
36
37
  var MAX_PERSISTED_READ_ROWS = 50001;
38
+ var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
37
39
  var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
40
+ var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
41
+ var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
42
+ var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
38
43
  var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
39
44
  var CELL_PART_SUFFIX = {
40
45
  at: "__at",
@@ -247,6 +252,741 @@ var Parser = class {
247
252
  function compileCheckExpression(source, columns) {
248
253
  return new Parser(tokenize(source), columns).parse();
249
254
  }
255
+
256
+ // src/predicate/ast.ts
257
+ var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
258
+ function walkPredicate(predicate, visit) {
259
+ visit(predicate);
260
+ if (predicate.kind === "and" || predicate.kind === "or") {
261
+ for (const operand of predicate.operands) walkPredicate(operand, visit);
262
+ }
263
+ if (predicate.kind === "not") walkPredicate(predicate.operand, visit);
264
+ }
265
+ function predicateOperands(predicate) {
266
+ const operands = [];
267
+ walkPredicate(predicate, (node) => {
268
+ if (node.kind === "compare") operands.push(node.left, node.right);
269
+ });
270
+ return operands;
271
+ }
272
+ function referencedColumns(predicate) {
273
+ const names = predicateOperands(predicate).flatMap(
274
+ (operand) => operand.kind === "column" ? [operand.name] : []
275
+ );
276
+ return [...new Set(names)];
277
+ }
278
+ var identifierSchema = z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
279
+ var namespaceSchema = z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
280
+ var tableReferenceSchema = z.string().refine(
281
+ (value) => IDENTIFIER_PATTERN.test(value) || QUALIFIED_TABLE_PATTERN.test(value),
282
+ "must be a table name or a schema-qualified table reference"
283
+ );
284
+
285
+ // src/schema/columns.ts
286
+ var COLUMN_TYPES = [
287
+ "uuid",
288
+ "text",
289
+ "integer",
290
+ "bigint",
291
+ "numeric",
292
+ "double",
293
+ "boolean",
294
+ "timestamptz",
295
+ "date",
296
+ "jsonb"
297
+ ];
298
+ var columnTypeSchema = z.enum(COLUMN_TYPES);
299
+ var FUNCTION_DEFAULTS = {
300
+ "gen_random_uuid()": ["uuid"],
301
+ "now()": ["timestamptz", "date"]
302
+ };
303
+ var CAST_PATTERN = /^([a-z][a-z0-9_]*)::([a-z]+)$/;
304
+ function defaultIssue(value, type) {
305
+ if (typeof value === "boolean") return type === "boolean" ? null : `boolean default is invalid for type '${type}'`;
306
+ if (typeof value === "number") {
307
+ if (!["integer", "bigint", "numeric", "double"].includes(type)) {
308
+ return `numeric default is invalid for type '${type}'`;
309
+ }
310
+ if (["integer", "bigint"].includes(type)) {
311
+ if (!Number.isInteger(value)) return `default for type '${type}' must be an integer`;
312
+ if (!Number.isSafeInteger(value)) {
313
+ return `default for type '${type}' exceeds the safe integer range \u2014 YAML numbers lose precision past 2^53`;
314
+ }
315
+ }
316
+ if (type === "numeric" && Math.abs(value) > Number.MAX_SAFE_INTEGER) {
317
+ return "numeric default exceeds the safe float range \u2014 write it as a string (e.g. '9007199254740993') to keep exact precision";
318
+ }
319
+ return null;
320
+ }
321
+ const allowedTypes = FUNCTION_DEFAULTS[value];
322
+ if (allowedTypes) return allowedTypes.includes(type) ? null : `default '${value}' is invalid for type '${type}'`;
323
+ return null;
324
+ }
325
+ var columnSchema = z.strictObject({
326
+ type: columnTypeSchema,
327
+ primary_key: z.boolean().optional(),
328
+ nullable: z.boolean().optional(),
329
+ unique: z.boolean().optional(),
330
+ default: z.union([z.string(), z.number(), z.boolean()]).optional(),
331
+ description: z.string().optional(),
332
+ renamed_from: identifierSchema.optional(),
333
+ cast: z.string().optional(),
334
+ deprecated: z.boolean().optional()
335
+ }).superRefine((column, ctx) => {
336
+ if (column.default !== void 0) {
337
+ const issue = defaultIssue(column.default, column.type);
338
+ if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["default"] });
339
+ }
340
+ if (column.primary_key && column.nullable) {
341
+ ctx.addIssue({ code: "custom", message: "a primary key column cannot be nullable", path: ["nullable"] });
342
+ }
343
+ if (column.primary_key && column.deprecated) {
344
+ ctx.addIssue({ code: "custom", message: "a primary key column cannot be deprecated", path: ["deprecated"] });
345
+ }
346
+ if (column.cast && !CAST_PATTERN.test(column.cast)) {
347
+ ctx.addIssue({ code: "custom", message: "cast must have the shape '<column>::<type>'", path: ["cast"] });
348
+ }
349
+ });
350
+ function castParts(cast) {
351
+ const match = CAST_PATTERN.exec(cast);
352
+ if (!match) return null;
353
+ return { column: match[1], type: match[2] };
354
+ }
355
+
356
+ // src/errors.ts
357
+ var PredicateParseError = class extends Error {
358
+ constructor(message, position) {
359
+ super(`${message} (at position ${position})`);
360
+ this.position = position;
361
+ }
362
+ name = "PredicateParseError";
363
+ };
364
+
365
+ // src/predicate/parse.ts
366
+ var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability"]);
367
+ var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
368
+ var IDENTIFIER_START = /[a-z_]/;
369
+ var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
370
+ var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
371
+ function tokenize2(source) {
372
+ const tokens = [];
373
+ let position = 0;
374
+ while (position < source.length) {
375
+ const char = source[position];
376
+ if (/\s/.test(char)) {
377
+ position += 1;
378
+ continue;
379
+ }
380
+ if (char === "(") {
381
+ tokens.push({ kind: "lparen", value: char, position });
382
+ position += 1;
383
+ continue;
384
+ }
385
+ if (char === ")") {
386
+ tokens.push({ kind: "rparen", value: char, position });
387
+ position += 1;
388
+ continue;
389
+ }
390
+ if (char === ",") {
391
+ tokens.push({ kind: "comma", value: char, position });
392
+ position += 1;
393
+ continue;
394
+ }
395
+ if (char === "'") {
396
+ const { value, end } = readString(source, position);
397
+ tokens.push({ kind: "string", value, position });
398
+ position = end;
399
+ continue;
400
+ }
401
+ const operator = OPERATOR_PATTERN.exec(source.slice(position));
402
+ if (operator) {
403
+ const value = operator[0] === "<>" ? "!=" : operator[0];
404
+ tokens.push({ kind: "operator", value, position });
405
+ position += operator[0].length;
406
+ continue;
407
+ }
408
+ const number = NUMBER_PATTERN.exec(source.slice(position));
409
+ if (number && !IDENTIFIER_START.test(char)) {
410
+ tokens.push({ kind: "number", value: number[0], position });
411
+ position += number[0].length;
412
+ continue;
413
+ }
414
+ if (IDENTIFIER_START.test(char)) {
415
+ let end = position;
416
+ while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
417
+ tokens.push({ kind: "identifier", value: source.slice(position, end), position });
418
+ position = end;
419
+ continue;
420
+ }
421
+ throw new PredicateParseError(`unexpected character '${char}'`, position);
422
+ }
423
+ tokens.push({ kind: "end", value: "", position: source.length });
424
+ return tokens;
425
+ }
426
+ function readString(source, start) {
427
+ let value = "";
428
+ let position = start + 1;
429
+ while (position < source.length) {
430
+ const char = source[position];
431
+ if (char !== "'") {
432
+ value += char;
433
+ position += 1;
434
+ continue;
435
+ }
436
+ if (source[position + 1] === "'") {
437
+ value += "'";
438
+ position += 2;
439
+ continue;
440
+ }
441
+ return { value, end: position + 1 };
442
+ }
443
+ throw new PredicateParseError("unterminated string literal", start);
444
+ }
445
+ var Parser2 = class {
446
+ constructor(tokens) {
447
+ this.tokens = tokens;
448
+ }
449
+ index = 0;
450
+ parse() {
451
+ const predicate = this.parseOr();
452
+ this.expect("end", "end of expression");
453
+ return predicate;
454
+ }
455
+ parseOr() {
456
+ const operands = [this.parseAnd()];
457
+ while (this.matchKeyword("or")) operands.push(this.parseAnd());
458
+ return operands.length === 1 ? operands[0] : { kind: "or", operands };
459
+ }
460
+ parseAnd() {
461
+ const operands = [this.parseNot()];
462
+ while (this.matchKeyword("and")) operands.push(this.parseNot());
463
+ return operands.length === 1 ? operands[0] : { kind: "and", operands };
464
+ }
465
+ parseNot() {
466
+ if (this.matchKeyword("not")) return { kind: "not", operand: this.parseNot() };
467
+ return this.parsePrimary();
468
+ }
469
+ parsePrimary() {
470
+ if (this.peek().kind === "lparen") {
471
+ this.advance();
472
+ const inner = this.parseOr();
473
+ this.expect("rparen", "')'");
474
+ return inner;
475
+ }
476
+ if (this.peekKeyword("capability")) {
477
+ const capability = this.parseCapability();
478
+ const next = this.peek();
479
+ if (next.kind === "operator") {
480
+ throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", next.position);
481
+ }
482
+ return capability;
483
+ }
484
+ return this.parseComparison();
485
+ }
486
+ parseCapability() {
487
+ this.advance();
488
+ const key = this.parseCallArgument("capability");
489
+ if (!CAPABILITY_KEY_PATTERN.test(key)) {
490
+ throw new PredicateParseError(`invalid capability key '${key}'`, this.peek().position);
491
+ }
492
+ return { kind: "capability", key };
493
+ }
494
+ parseComparison() {
495
+ const left = this.parseOperand();
496
+ const operator = this.expect("operator", "a comparison operator");
497
+ if (!COMPARE_OPS.includes(operator.value)) {
498
+ throw new PredicateParseError(`unsupported operator '${operator.value}'`, operator.position);
499
+ }
500
+ const right = this.parseOperand();
501
+ return { kind: "compare", op: operator.value, left, right };
502
+ }
503
+ parseOperand() {
504
+ const token = this.peek();
505
+ if (token.kind === "string") {
506
+ this.advance();
507
+ return { kind: "literal", value: token.value };
508
+ }
509
+ if (token.kind === "number") {
510
+ this.advance();
511
+ const value = Number(token.value);
512
+ if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
513
+ throw new PredicateParseError(
514
+ `numeric literal '${token.value}' exceeds safe integer precision \u2014 quote it as a string`,
515
+ token.position
516
+ );
517
+ }
518
+ return { kind: "literal", value };
519
+ }
520
+ if (token.kind !== "identifier") {
521
+ throw new PredicateParseError(`expected an operand, got '${token.value || "end"}'`, token.position);
522
+ }
523
+ this.advance();
524
+ if (token.value === "true" || token.value === "false") {
525
+ return { kind: "literal", value: token.value === "true" };
526
+ }
527
+ if (token.value === "null") return { kind: "literal", value: null };
528
+ if (token.value === "claim") {
529
+ const key = this.parseCallArgument("claim");
530
+ if (!CLAIM_KEY_PATTERN.test(key)) {
531
+ throw new PredicateParseError(`invalid claim key '${key}'`, token.position);
532
+ }
533
+ return { kind: "claim", key };
534
+ }
535
+ if (token.value === "capability") {
536
+ throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", token.position);
537
+ }
538
+ if (KEYWORDS.has(token.value) || !IDENTIFIER_PATTERN.test(token.value)) {
539
+ throw new PredicateParseError(`invalid column reference '${token.value}'`, token.position);
540
+ }
541
+ return { kind: "column", name: token.value };
542
+ }
543
+ parseCallArgument(fn) {
544
+ this.expect("lparen", `'(' after ${fn}`);
545
+ const argument = this.expect("string", `a quoted key in ${fn}(...)`);
546
+ this.expect("rparen", `')' after ${fn} key`);
547
+ return argument.value;
548
+ }
549
+ peek() {
550
+ return this.tokens[this.index];
551
+ }
552
+ peekKeyword(keyword) {
553
+ const token = this.peek();
554
+ return token.kind === "identifier" && token.value === keyword;
555
+ }
556
+ matchKeyword(keyword) {
557
+ if (!this.peekKeyword(keyword)) return false;
558
+ this.advance();
559
+ return true;
560
+ }
561
+ advance() {
562
+ const token = this.peek();
563
+ if (token.kind !== "end") this.index += 1;
564
+ return token;
565
+ }
566
+ expect(kind, description) {
567
+ const token = this.peek();
568
+ if (token.kind !== kind) {
569
+ throw new PredicateParseError(`expected ${description}, got '${token.value || "end"}'`, token.position);
570
+ }
571
+ return this.advance();
572
+ }
573
+ };
574
+ function parsePredicate(source) {
575
+ const trimmed = source.trim();
576
+ if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
577
+ return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
578
+ }
579
+
580
+ // src/schema/predicates.ts
581
+ var parsedPredicateSchema = z.string().transform((source, ctx) => {
582
+ try {
583
+ return parsePredicate(source);
584
+ } catch (error) {
585
+ ctx.addIssue({ code: "custom", message: error instanceof Error ? error.message : String(error) });
586
+ return z.NEVER;
587
+ }
588
+ });
589
+
590
+ // src/schema/actions.ts
591
+ var EXECUTES_VERB_PATTERN = /^\s*(insert|update|delete)\b/i;
592
+ var DOLLAR_QUOTE_PATTERN = /\$[a-zA-Z0-9_]*\$/;
593
+ var RETURNING_PATTERN = /\breturning\b/i;
594
+ var ESCAPE_STRING_PATTERN = /(?<![a-zA-Z0-9_])[Ee]'/;
595
+ var MASKED_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\(\s*''\s*\)/g;
596
+ var BARE_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
597
+ var EXISTS_ENTRY_PATTERN = /^exists\(\s*'([^']+)'\s*,\s*([a-z][a-z0-9_]*)\s*\)$/;
598
+ var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
599
+ var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
600
+ var SQL_COMMENT_PATTERN = /--|\/\*/;
601
+ var actionParamSchema = z.strictObject({
602
+ type: columnTypeSchema,
603
+ optional: z.boolean().optional(),
604
+ description: z.string().optional()
605
+ });
606
+ var validatesEntrySchema = z.string().transform((source, ctx) => {
607
+ const match = EXISTS_ENTRY_PATTERN.exec(source.trim());
608
+ if (!match) {
609
+ ctx.addIssue({ code: "custom", message: "validates entries must have the shape exists('<table>', <param>)" });
610
+ return z.NEVER;
611
+ }
612
+ const table = tableReferenceSchema.safeParse(match[1]);
613
+ if (!table.success) {
614
+ ctx.addIssue({ code: "custom", message: `invalid table reference '${match[1]}'` });
615
+ return z.NEVER;
616
+ }
617
+ return { source: source.trim(), table: table.data, param: match[2] };
618
+ });
619
+ var AUDIT_ACTIONS = ["create", "update", "delete"];
620
+ var auditSchema = z.strictObject({
621
+ action: z.enum(AUDIT_ACTIONS),
622
+ resource: identifierSchema.optional()
623
+ });
624
+ var actionSchema = z.strictObject({
625
+ description: z.string().optional(),
626
+ requires: parsedPredicateSchema.optional(),
627
+ params: z.record(identifierSchema, actionParamSchema).optional(),
628
+ validates: z.array(validatesEntrySchema).optional(),
629
+ // One statement, or an ordered list run atomically in the action function.
630
+ executes: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]),
631
+ // Declares the columns the action's RETURNING yields. Present → the
632
+ // function returns those rows (typed in the client); absent → the action
633
+ // reports rows_affected only. The column names/types must match the
634
+ // RETURNING list, which Postgres enforces at CREATE FUNCTION.
635
+ returns: z.record(identifierSchema, columnTypeSchema).optional(),
636
+ audit: auditSchema.optional()
637
+ }).superRefine((action, ctx) => {
638
+ const params = Object.keys(action.params ?? {});
639
+ if (action.requires && referencedColumns(action.requires.ast).length > 0) {
640
+ ctx.addIssue({
641
+ code: "custom",
642
+ message: "requires may reference claims and capabilities only, not columns",
643
+ path: ["requires"]
644
+ });
645
+ }
646
+ for (const issue of executesIssues(action.executes, new Set(params), action.returns !== void 0)) {
647
+ ctx.addIssue({ code: "custom", message: issue, path: ["executes"] });
648
+ }
649
+ if (action.returns && Object.keys(action.returns).length === 0) {
650
+ ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
651
+ }
652
+ for (const [index, entry] of (action.validates ?? []).entries()) {
653
+ if (!params.includes(entry.param)) {
654
+ ctx.addIssue({
655
+ code: "custom",
656
+ message: `validates references undeclared param '${entry.param}'`,
657
+ path: ["validates", index]
658
+ });
659
+ }
660
+ }
661
+ });
662
+ function extractNamedParams(sql) {
663
+ const withoutStrings = sql.replace(QUOTED_STRING_PATTERN, "''");
664
+ const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
665
+ return [...new Set(names)];
666
+ }
667
+ function actionStatementSources(executes) {
668
+ return (Array.isArray(executes) ? executes : [executes]).map((statement) => statement.trim());
669
+ }
670
+ function executesIssues(executes, declaredParams, hasReturns) {
671
+ const issues = [];
672
+ const statements = actionStatementSources(executes);
673
+ statements.forEach((statement, index) => {
674
+ const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
675
+ const at = statements.length > 1 ? ` (statement ${index + 1})` : "";
676
+ if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
677
+ if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
678
+ if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
679
+ if (DOLLAR_QUOTE_PATTERN.test(withoutStrings)) issues.push(`executes must not use dollar-quoted strings${at}`);
680
+ if (ESCAPE_STRING_PATTERN.test(withoutStrings)) issues.push(`executes must not use escape strings (E'\u2026')${at}`);
681
+ if (withoutStrings.includes('"')) issues.push(`executes must not use double-quoted identifiers${at}`);
682
+ if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
683
+ issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
684
+ }
685
+ if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index === statements.length - 1)) {
686
+ issues.push(
687
+ hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
688
+ );
689
+ }
690
+ });
691
+ const last = statements.at(-1);
692
+ if (hasReturns && last && !RETURNING_PATTERN.test(last.replace(QUOTED_STRING_PATTERN, "''"))) {
693
+ issues.push("the last executes statement must use RETURNING to populate the declared returns columns");
694
+ }
695
+ const undeclared = extractNamedParams(statements.join("\n")).filter((name) => !declaredParams.has(name));
696
+ if (undeclared.length > 0) issues.push(`executes references undeclared params: ${undeclared.join(", ")}`);
697
+ return issues;
698
+ }
699
+
700
+ // src/schema/table.ts
701
+ var linkSchema = z.strictObject({
702
+ to: tableReferenceSchema,
703
+ on: identifierSchema,
704
+ enforce: z.boolean().optional(),
705
+ description: z.string().optional()
706
+ });
707
+ var indexSchema = z.union([
708
+ z.array(identifierSchema).min(1),
709
+ z.strictObject({
710
+ columns: z.array(identifierSchema).min(1),
711
+ unique: z.boolean().optional(),
712
+ name: identifierSchema.optional()
713
+ })
714
+ ]).transform((index) => Array.isArray(index) ? { columns: index } : index);
715
+ var policiesSchema = z.strictObject({
716
+ read: parsedPredicateSchema.optional(),
717
+ write: parsedPredicateSchema.optional(),
718
+ insert: parsedPredicateSchema.optional(),
719
+ update: parsedPredicateSchema.optional()
720
+ });
721
+ z.strictObject({
722
+ namespace: namespaceSchema,
723
+ table: identifierSchema,
724
+ description: z.string().optional(),
725
+ columns: z.record(identifierSchema, columnSchema).optional(),
726
+ source: z.string().regex(SYNCED_SOURCE_PATTERN, "source must be a qualified lowercase schema.table identifier").optional(),
727
+ links: z.record(identifierSchema, linkSchema).optional(),
728
+ indexes: z.array(indexSchema).optional(),
729
+ policies: policiesSchema.optional(),
730
+ actions: z.record(identifierSchema, actionSchema).optional()
731
+ }).superRefine((table, ctx) => {
732
+ if (RESERVED_TABLE_NAMES.has(table.table)) {
733
+ ctx.addIssue({ code: "custom", message: `'${table.table}' is a reserved table name`, path: ["table"] });
734
+ }
735
+ for (const name of Object.keys(table.columns ?? {})) {
736
+ if (RESERVED_COLUMN_NAMES.has(name)) {
737
+ ctx.addIssue({
738
+ code: "custom",
739
+ message: `column '${name}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
740
+ path: ["columns", name]
741
+ });
742
+ }
743
+ }
744
+ if (table.source) validateSyncedTable(table, ctx);
745
+ else validateColumnTable(table, ctx);
746
+ validateLinks(table, ctx);
747
+ validatePolicyColumns(table, ctx);
748
+ validateDerivedNameLengths(table, ctx);
749
+ });
750
+ var indexName = (table, definition) => definition.name ?? `ix_${table}_${definition.columns.join("_")}`;
751
+ var policyName = (table, suffix) => `${table}_${suffix}`;
752
+ var foreignKeyName = (table, link) => `fk_${table}_${link}`;
753
+ function validateSyncedTable(table, ctx) {
754
+ const forbidden = [
755
+ ["actions", "synced tables are read-only; actions are not allowed"],
756
+ ["indexes", "v1 synced tables cannot declare indexes; declare them with defineSyncedTable({ indexes }) in a v2 definition"]
757
+ ];
758
+ for (const [field, message] of forbidden) {
759
+ if (table[field] !== void 0) ctx.addIssue({ code: "custom", message, path: [field] });
760
+ }
761
+ if (table.policies?.write) {
762
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
763
+ }
764
+ if (table.policies?.insert) {
765
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
766
+ }
767
+ if (table.policies?.update) {
768
+ ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
769
+ }
770
+ const columns = table.columns ?? {};
771
+ const names = Object.keys(columns);
772
+ if (names.length === 0) return;
773
+ const primaryKeys = names.filter((name) => columns[name]?.primary_key);
774
+ if (primaryKeys.length !== 1) {
775
+ ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
776
+ }
777
+ validateRenames(columns, names, ctx);
778
+ for (const name of names) {
779
+ const column = columns[name];
780
+ if (!column) continue;
781
+ const cast = column.cast ? castParts(column.cast) : null;
782
+ if (column.cast && cast?.column !== name) {
783
+ ctx.addIssue({
784
+ code: "custom",
785
+ message: "cast must reference the column it is declared on",
786
+ path: ["columns", name, "cast"]
787
+ });
788
+ }
789
+ if (cast && cast.type !== column.type) {
790
+ ctx.addIssue({
791
+ code: "custom",
792
+ message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
793
+ path: ["columns", name, "cast"]
794
+ });
795
+ }
796
+ }
797
+ }
798
+ function validateRenames(columns, names, ctx) {
799
+ const renameSources = /* @__PURE__ */ new Map();
800
+ for (const name of names) {
801
+ const renamedFrom = columns[name]?.renamed_from;
802
+ if (!renamedFrom) continue;
803
+ if (renamedFrom === name || names.includes(renamedFrom)) {
804
+ ctx.addIssue({
805
+ code: "custom",
806
+ message: "renamed_from must name a column that no longer exists",
807
+ path: ["columns", name, "renamed_from"]
808
+ });
809
+ }
810
+ const claimed = renameSources.get(renamedFrom);
811
+ if (claimed) {
812
+ ctx.addIssue({
813
+ code: "custom",
814
+ message: `renamed_from '${renamedFrom}' is already claimed by '${claimed}'`,
815
+ path: ["columns", name, "renamed_from"]
816
+ });
817
+ }
818
+ renameSources.set(renamedFrom, name);
819
+ }
820
+ }
821
+ function validateColumnTable(table, ctx) {
822
+ const columns = table.columns ?? {};
823
+ const names = Object.keys(columns);
824
+ if (names.length === 0) {
825
+ ctx.addIssue({ code: "custom", message: "a table must declare columns or a source", path: ["columns"] });
826
+ return;
827
+ }
828
+ const primaryKeys = names.filter((name) => columns[name]?.primary_key);
829
+ if (primaryKeys.length !== 1) {
830
+ ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
831
+ }
832
+ validateRenames(columns, names, ctx);
833
+ for (const name of names) {
834
+ const column = columns[name];
835
+ if (!column) continue;
836
+ const cast = column.cast ? castParts(column.cast) : null;
837
+ if (column.cast && cast?.column !== name) {
838
+ ctx.addIssue({
839
+ code: "custom",
840
+ message: "cast must reference the column it is declared on",
841
+ path: ["columns", name, "cast"]
842
+ });
843
+ }
844
+ if (cast && cast.type !== column.type) {
845
+ ctx.addIssue({
846
+ code: "custom",
847
+ message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
848
+ path: ["columns", name, "cast"]
849
+ });
850
+ }
851
+ }
852
+ for (const [position, index] of (table.indexes ?? []).entries()) {
853
+ if (new Set(index.columns).size !== index.columns.length) {
854
+ ctx.addIssue({
855
+ code: "custom",
856
+ message: "index names a column more than once",
857
+ path: ["indexes", position]
858
+ });
859
+ }
860
+ for (const column of index.columns) {
861
+ if (!names.includes(column)) {
862
+ ctx.addIssue({
863
+ code: "custom",
864
+ message: `index references unknown column '${column}'`,
865
+ path: ["indexes", position]
866
+ });
867
+ }
868
+ }
869
+ }
870
+ }
871
+ function validateLinks(table, ctx) {
872
+ const columnNames = Object.keys(table.columns ?? {});
873
+ for (const [name, link] of Object.entries(table.links ?? {})) {
874
+ if (columnNames.length > 0 && !columnNames.includes(link.on)) {
875
+ ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
876
+ }
877
+ if (link.enforce && link.to.includes(".")) {
878
+ ctx.addIssue({
879
+ code: "custom",
880
+ message: "enforce is only valid for links inside the namespace; cross-boundary links stay soft",
881
+ path: ["links", name, "enforce"]
882
+ });
883
+ }
884
+ if (link.enforce && table.source) {
885
+ ctx.addIssue({
886
+ code: "custom",
887
+ message: "a synced table cannot enforce foreign keys \u2014 its DDL is owned by the sync",
888
+ path: ["links", name, "enforce"]
889
+ });
890
+ }
891
+ }
892
+ }
893
+ function validateDerivedNameLengths(table, ctx) {
894
+ const worstPolicy = policyName(table.table, "insert");
895
+ if (!table.source && worstPolicy.length > MAX_IDENTIFIER_LENGTH) {
896
+ ctx.addIssue({
897
+ code: "custom",
898
+ message: `table name is too long \u2014 the derived policy name '${worstPolicy}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit`,
899
+ path: ["table"]
900
+ });
901
+ }
902
+ for (const [position, index] of (table.indexes ?? []).entries()) {
903
+ const name = indexName(table.table, index);
904
+ if (name.length > MAX_IDENTIFIER_LENGTH) {
905
+ ctx.addIssue({
906
+ code: "custom",
907
+ message: `derived index name '${name}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`,
908
+ path: ["indexes", position]
909
+ });
910
+ }
911
+ }
912
+ for (const [name, link] of Object.entries(table.links ?? {})) {
913
+ const constraint = foreignKeyName(table.table, name);
914
+ if (link.enforce && constraint.length > MAX_IDENTIFIER_LENGTH) {
915
+ ctx.addIssue({
916
+ code: "custom",
917
+ message: `derived foreign-key name '${constraint}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the link or table name`,
918
+ path: ["links", name]
919
+ });
920
+ }
921
+ }
922
+ for (const name of Object.keys(table.actions ?? {})) {
923
+ const fnName = `action_${name}`;
924
+ if (fnName.length > MAX_IDENTIFIER_LENGTH) {
925
+ ctx.addIssue({
926
+ code: "custom",
927
+ message: `derived action function name '${fnName}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the action name`,
928
+ path: ["actions", name]
929
+ });
930
+ }
931
+ }
932
+ }
933
+ var TYPE_GROUPS = {
934
+ integer: "numeric",
935
+ bigint: "numeric",
936
+ numeric: "numeric",
937
+ double: "numeric",
938
+ text: "text",
939
+ uuid: "uuid",
940
+ date: "temporal",
941
+ timestamptz: "temporal",
942
+ boolean: "boolean",
943
+ jsonb: "jsonb"
944
+ };
945
+ function validatePolicyColumns(table, ctx) {
946
+ const columns = table.columns ?? {};
947
+ const columnNames = Object.keys(columns);
948
+ if (columnNames.length === 0) return;
949
+ for (const kind of ["read", "write", "insert", "update"]) {
950
+ const predicate = table.policies?.[kind];
951
+ if (!predicate) continue;
952
+ for (const column of referencedColumns(predicate.ast)) {
953
+ if (!columnNames.includes(column)) {
954
+ ctx.addIssue({
955
+ code: "custom",
956
+ message: `policy references unknown column '${column}'`,
957
+ path: ["policies", kind]
958
+ });
959
+ }
960
+ }
961
+ walkPredicate(predicate.ast, (node) => {
962
+ if (node.kind !== "compare") return;
963
+ const issue = comparisonTypeIssue(node.left, node.right, columns);
964
+ if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["policies", kind] });
965
+ });
966
+ }
967
+ }
968
+ function comparisonTypeIssue(left, right, columns) {
969
+ const groupOf = (operand) => operand.kind === "column" ? columns[operand.name] ? TYPE_GROUPS[columns[operand.name].type] : null : null;
970
+ if (left.kind === "column" && right.kind === "column") {
971
+ const leftGroup = groupOf(left);
972
+ const rightGroup = groupOf(right);
973
+ if (leftGroup && rightGroup && leftGroup !== rightGroup) {
974
+ return `policy compares '${left.name}' (${columns[left.name].type}) with '${right.name}' (${columns[right.name].type}) \u2014 incomparable types`;
975
+ }
976
+ return null;
977
+ }
978
+ const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
979
+ const literal = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
980
+ if (!column || !literal || literal.value === null) return null;
981
+ const columnGroup = groupOf(column);
982
+ if (!columnGroup) return null;
983
+ if (typeof literal.value === "string") return null;
984
+ const literalGroup = typeof literal.value === "number" ? "numeric" : "boolean";
985
+ if (literalGroup !== columnGroup) {
986
+ return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal.value} literal \u2014 incomparable types`;
987
+ }
988
+ return null;
989
+ }
250
990
  var policyVariantSchema = z.object({
251
991
  /** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
252
992
  when: z.string().min(1).max(1e3),
@@ -555,7 +1295,7 @@ var CANONICAL_FORMAT = 2;
555
1295
  var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
556
1296
  var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
557
1297
  var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
558
- var COLUMN_TYPES = [
1298
+ var COLUMN_TYPES2 = [
559
1299
  "uuid",
560
1300
  "text",
561
1301
  "integer",
@@ -567,7 +1307,7 @@ var COLUMN_TYPES = [
567
1307
  "date",
568
1308
  "jsonb"
569
1309
  ];
570
- var columnTypeSchema = z.enum(COLUMN_TYPES);
1310
+ var columnTypeSchema2 = z.enum(COLUMN_TYPES2);
571
1311
  var canonicalEnumSchema = z.object({
572
1312
  name: identifier2,
573
1313
  values: z.array(z.string().min(1).max(63)).min(1).max(100)
@@ -576,7 +1316,7 @@ var canonicalColumnSchema = z.object({
576
1316
  /** Property name in TS (camelCase); columnName is the Postgres name. */
577
1317
  name: z.string().min(1).max(63),
578
1318
  columnName: columnIdentifier,
579
- type: z.union([columnTypeSchema, z.object({ enum: identifier2 })]),
1319
+ type: z.union([columnTypeSchema2, z.object({ enum: identifier2 })]),
580
1320
  nullable: z.boolean().default(true),
581
1321
  unique: z.boolean().default(false),
582
1322
  /** Literal default, or one of the allowlisted SQL functions. */
@@ -669,7 +1409,7 @@ var canonicalActionSchema = z.object({
669
1409
  name: identifier2,
670
1410
  requires: z.string().max(1e3).optional(),
671
1411
  params: z.record(z.string(), z.object({
672
- type: columnTypeSchema,
1412
+ type: columnTypeSchema2,
673
1413
  nullable: z.boolean().default(false)
674
1414
  })).default({}),
675
1415
  executes: z.string().min(1).max(1e4),
@@ -770,7 +1510,8 @@ var canonicalTableSchema = z.object({
770
1510
  if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
771
1511
  fail("is read-only and cannot declare write policies", ["policies"]);
772
1512
  }
773
- const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
1513
+ const ownedIndexes = table.source === void 0 ? table.indexes : table.indexes.filter((index) => index.unique);
1514
+ const ownedCollections = [ownedIndexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
774
1515
  const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
775
1516
  if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
776
1517
  fail("cannot declare owned storage, constraints or identity operations");
@@ -872,6 +1613,23 @@ function trackedCellColumns(table) {
872
1613
  return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
873
1614
  }
874
1615
  var SUFFIXES = Object.values(CELL_PART_SUFFIX);
1616
+ function validateSyncedIndexes(table, tableIndex, ctx) {
1617
+ if (table.source === void 0) return;
1618
+ const columns = new Set(table.columns.map((column) => column.columnName));
1619
+ for (const [position, index] of table.indexes.entries()) {
1620
+ const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "indexes", position], message: `synced table '${table.table}' ${message}` });
1621
+ if (columns.size === 0) fail("must declare columns to declare indexes");
1622
+ if (index.unique) fail("cannot declare a unique index");
1623
+ if (new Set(index.columns).size !== index.columns.length) fail("index names a column more than once");
1624
+ for (const column of index.columns) {
1625
+ if (columns.size > 0 && !columns.has(column)) fail(`index references unknown column '${column}'`);
1626
+ }
1627
+ const name = indexName(table.table, index);
1628
+ if (name.length > MAX_IDENTIFIER_LENGTH) {
1629
+ fail(`derived index name '${name}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`);
1630
+ }
1631
+ }
1632
+ }
875
1633
  function validateCellMetadata(table, index, ctx) {
876
1634
  const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
877
1635
  const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
@@ -938,12 +1696,24 @@ var localDefinitionSchema = z.object({
938
1696
  }).default({ uses: [] })
939
1697
  }).superRefine((definition, ctx) => {
940
1698
  const seen = /* @__PURE__ */ new Set();
1699
+ const syncedIndexOwners = /* @__PURE__ */ new Map();
941
1700
  for (const [index, table] of definition.tables.entries()) {
942
1701
  if (seen.has(table.table)) {
943
1702
  ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
944
1703
  }
945
1704
  seen.add(table.table);
946
1705
  validateCellMetadata(table, index, ctx);
1706
+ validateSyncedIndexes(table, index, ctx);
1707
+ if (table.source !== void 0) {
1708
+ for (const [position, entry] of table.indexes.entries()) {
1709
+ const name = indexName(table.table, entry);
1710
+ const owner = syncedIndexOwners.get(name);
1711
+ if (owner !== void 0) {
1712
+ ctx.addIssue({ code: "custom", path: ["tables", index, "indexes", position], message: `synced index '${name}' is also declared on '${owner}'` });
1713
+ }
1714
+ syncedIndexOwners.set(name, table.table);
1715
+ }
1716
+ }
947
1717
  if (table.source !== void 0) {
948
1718
  for (const [columnIndex, column] of table.columns.entries()) {
949
1719
  if (column.onUpdateFn !== void 0) {
@@ -1426,7 +2196,7 @@ var variableDefaultIrSchema = z.object({
1426
2196
  var variableIrSchema = z.object({
1427
2197
  name: z.string(),
1428
2198
  type: z.union([
1429
- z.object({ kind: z.literal("scalar"), scalar: z.enum(COLUMN_TYPES) }),
2199
+ z.object({ kind: z.literal("scalar"), scalar: z.enum(COLUMN_TYPES2) }),
1430
2200
  /** `values` is the declared membership whitelist — the executor validates against it. */
1431
2201
  z.object({ kind: z.literal("enum"), name: z.string(), values: z.array(z.string()).min(1) }),
1432
2202
  /** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
@@ -1438,19 +2208,20 @@ var variableIrSchema = z.object({
1438
2208
  defaultValue: variableDefaultIrSchema.optional()
1439
2209
  });
1440
2210
  var sqlColumnSchema = z.object({ name: z.string(), type: z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz", "jsonb"]), nullable: z.boolean() });
2211
+ var sqlObjectIrSchema = z.object({
2212
+ ref: z.string(),
2213
+ table: z.string(),
2214
+ columns: z.array(sqlColumnSchema.extend({ type: z.string() })).min(1),
2215
+ scan: z.string().min(1),
2216
+ read: z.array(z.string()).min(1)
2217
+ }).strict();
1441
2218
  var compiledOperationSchema = z.object({
1442
2219
  language: z.literal("sql").optional(),
1443
2220
  /** Server-compiled object catalog and permission ceiling, never caller SQL. */
1444
2221
  sqlQuery: z.object({
1445
2222
  catalog: z.object({
1446
2223
  identity: z.string(),
1447
- objects: z.array(z.object({
1448
- ref: z.string(),
1449
- table: z.string(),
1450
- columns: z.array(sqlColumnSchema.extend({ type: z.string() })).min(1),
1451
- scan: z.string().min(1),
1452
- read: z.array(z.string()).min(1)
1453
- }).strict()).min(1).max(256)
2224
+ objects: z.array(sqlObjectIrSchema).min(1).max(256)
1454
2225
  }).strict(),
1455
2226
  read: z.array(z.string()).min(1).max(256),
1456
2227
  maxRows: z.number().int().min(1).max(1e4)
@@ -1558,7 +2329,7 @@ var tableMetaIrSchema = z.object({
1558
2329
  columns: z.array(z.object({
1559
2330
  name: z.string(),
1560
2331
  columnName: z.string(),
1561
- type: z.union([z.enum(COLUMN_TYPES), z.object({ enum: z.string(), values: z.array(z.string()) })]),
2332
+ type: z.union([z.enum(COLUMN_TYPES2), z.object({ enum: z.string(), values: z.array(z.string()) })]),
1562
2333
  nullable: z.boolean()
1563
2334
  })),
1564
2335
  /** Empty for keyless read-only views; keyed tables and views list logical columns. */
@@ -1629,14 +2400,14 @@ z.object({
1629
2400
  });
1630
2401
 
1631
2402
  // src/predicate-v2/parse.ts
1632
- var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability", "role", "exists"]);
1633
- var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
1634
- var IDENTIFIER_START = /[a-zA-Z_]/;
1635
- var IDENTIFIER_CHAR = /[a-zA-Z0-9_]/;
1636
- var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
2403
+ var KEYWORDS2 = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability", "role", "exists"]);
2404
+ var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
2405
+ var IDENTIFIER_START2 = /[a-zA-Z_]/;
2406
+ var IDENTIFIER_CHAR2 = /[a-zA-Z0-9_]/;
2407
+ var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
1637
2408
  var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
1638
2409
  var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
1639
- function tokenize2(source) {
2410
+ function tokenize3(source) {
1640
2411
  const tokens = [];
1641
2412
  let position = 0;
1642
2413
  while (position < source.length) {
@@ -1666,27 +2437,27 @@ function tokenize2(source) {
1666
2437
  continue;
1667
2438
  }
1668
2439
  if (char === "'") {
1669
- const { value, end } = readString(source, position);
2440
+ const { value, end } = readString2(source, position);
1670
2441
  tokens.push({ kind: "string", value, position });
1671
2442
  position = end;
1672
2443
  continue;
1673
2444
  }
1674
- const operator = OPERATOR_PATTERN.exec(source.slice(position));
2445
+ const operator = OPERATOR_PATTERN2.exec(source.slice(position));
1675
2446
  if (operator) {
1676
2447
  const value = operator[0] === "<>" ? "!=" : operator[0];
1677
2448
  tokens.push({ kind: "operator", value, position });
1678
2449
  position += operator[0].length;
1679
2450
  continue;
1680
2451
  }
1681
- const number = NUMBER_PATTERN.exec(source.slice(position));
1682
- if (number && !IDENTIFIER_START.test(char)) {
2452
+ const number = NUMBER_PATTERN2.exec(source.slice(position));
2453
+ if (number && !IDENTIFIER_START2.test(char)) {
1683
2454
  tokens.push({ kind: "number", value: number[0], position });
1684
2455
  position += number[0].length;
1685
2456
  continue;
1686
2457
  }
1687
- if (IDENTIFIER_START.test(char)) {
2458
+ if (IDENTIFIER_START2.test(char)) {
1688
2459
  let end = position;
1689
- while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
2460
+ while (end < source.length && IDENTIFIER_CHAR2.test(source[end])) end += 1;
1690
2461
  tokens.push({ kind: "identifier", value: source.slice(position, end), position });
1691
2462
  position = end;
1692
2463
  continue;
@@ -1696,7 +2467,7 @@ function tokenize2(source) {
1696
2467
  tokens.push({ kind: "end", value: "", position: source.length });
1697
2468
  return tokens;
1698
2469
  }
1699
- function readString(source, start) {
2470
+ function readString2(source, start) {
1700
2471
  let value = "";
1701
2472
  let position = start + 1;
1702
2473
  while (position < source.length) {
@@ -1715,7 +2486,7 @@ function readString(source, start) {
1715
2486
  }
1716
2487
  throw new PolicyParseError("unterminated string literal", start);
1717
2488
  }
1718
- var Parser2 = class {
2489
+ var Parser3 = class {
1719
2490
  constructor(tokens) {
1720
2491
  this.tokens = tokens;
1721
2492
  }
@@ -1857,7 +2628,7 @@ var Parser2 = class {
1857
2628
  /** `ident(.ident)*` — first segment must not be a keyword; later segments may be (e.g. `author.role`). */
1858
2629
  parsePath(description) {
1859
2630
  const first = this.peek();
1860
- if (first.kind !== "identifier" || KEYWORDS.has(first.value)) {
2631
+ if (first.kind !== "identifier" || KEYWORDS2.has(first.value)) {
1861
2632
  throw new PolicyParseError(`expected ${description}, got '${first.value || "end"}'`, first.position);
1862
2633
  }
1863
2634
  this.advance();
@@ -1903,7 +2674,7 @@ var Parser2 = class {
1903
2674
  function parsePolicy(source) {
1904
2675
  const trimmed = source.trim();
1905
2676
  if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
1906
- return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
2677
+ return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
1907
2678
  }
1908
2679
 
1909
2680
  // src/predicate-v2/compile.ts
@@ -1943,6 +2714,7 @@ var FLIPPED = {
1943
2714
  ">=": "<="
1944
2715
  };
1945
2716
  function resolveTable(resolver, table) {
2717
+ resolver.onTable?.(table);
1946
2718
  const info = resolver.tables[table];
1947
2719
  if (!info) {
1948
2720
  throw new PolicyCompileError(`unknown table '${table}'`, "unknown-table", Object.keys(resolver.tables));