@distrohelena/canton-typescript-sdk 0.1.36 → 0.1.38

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 (116) hide show
  1. package/README.md +143 -34
  2. package/dist/cjs/core/types/requests/{submit-command-request.js → submit-commands-request.js} +10 -6
  3. package/dist/cjs/index.js +10 -8
  4. package/dist/cjs/query/canonical/in-memory-query-evaluator.js +291 -0
  5. package/dist/cjs/query/canonical/public-identity.js +41 -0
  6. package/dist/cjs/query/canonical/query-ast.js +2 -0
  7. package/dist/cjs/query/canonical/query-dataset.js +351 -0
  8. package/dist/cjs/query/canonical/query-normalizer.js +657 -0
  9. package/dist/cjs/query/canonical/query-schema.js +86 -0
  10. package/dist/cjs/query/canton-manager.js +14 -5
  11. package/dist/cjs/query/errors/query-snapshot-incomplete-error.js +19 -0
  12. package/dist/cjs/query/grpc/grpc-contract-cache.js +484 -0
  13. package/dist/cjs/query/grpc/grpc-package-relation-reader.js +218 -0
  14. package/dist/cjs/query/grpc/grpc-query-client.js +249 -0
  15. package/dist/cjs/query/grpc/grpc-query-snapshot-reader.js +238 -0
  16. package/dist/cjs/query/grpc/grpc-query-value-mapper.js +204 -0
  17. package/dist/cjs/query/grpc/grpc-relation-mapper.js +794 -0
  18. package/dist/cjs/query/pqs/pqs-query-client.js +151 -582
  19. package/dist/cjs/query/pqs/pqs-relational-sql-compiler.js +138 -95
  20. package/dist/cjs/query/pqs/pqs-result-shape.js +28 -0
  21. package/dist/cjs/query/pqs/pqs-schema-profile.js +49 -48
  22. package/dist/cjs/query/pqs/pqs-sql-compiler.js +343 -289
  23. package/dist/cjs/query/pqs/pqs-sql-syntax.js +10 -0
  24. package/dist/cjs/services/command/command-service-client.js +16 -3
  25. package/dist/cjs/services/commands/command-payload-builder.js +1 -1
  26. package/dist/cjs/services/commands/command-submission-pipeline.js +21 -6
  27. package/dist/cjs/testing/runtime/declarative-action-executor.js +3 -3
  28. package/dist/cjs/transports/grpc/grpc-transport.js +2 -2
  29. package/dist/cjs/transports/grpc/mappers/commands-mapper.js +6 -6
  30. package/dist/cjs/transports/grpc/mappers/interactive-command-mapper.js +1 -1
  31. package/dist/cjs/transports/json/json-transport.js +1 -1
  32. package/dist/cjs/transports/json/mappers/commands-mapper.js +3 -3
  33. package/dist/core/transports/transport.interface.d.ts +4 -4
  34. package/dist/core/types/prepared-command-submission.d.ts +3 -3
  35. package/dist/core/types/requests/{submit-command-request.d.ts → submit-commands-request.d.ts} +8 -4
  36. package/dist/core/types/requests/{submit-command-request.js → submit-commands-request.js} +8 -4
  37. package/dist/index.d.ts +5 -2
  38. package/dist/index.js +2 -1
  39. package/dist/query/canonical/in-memory-query-evaluator.d.ts +22 -0
  40. package/dist/query/canonical/in-memory-query-evaluator.js +287 -0
  41. package/dist/query/canonical/public-identity.d.ts +14 -0
  42. package/dist/query/canonical/public-identity.js +35 -0
  43. package/dist/query/canonical/query-ast.d.ts +104 -0
  44. package/dist/query/canonical/query-ast.js +1 -0
  45. package/dist/query/canonical/query-dataset.d.ts +63 -0
  46. package/dist/query/canonical/query-dataset.js +343 -0
  47. package/dist/query/canonical/query-normalizer.d.ts +10 -0
  48. package/dist/query/canonical/query-normalizer.js +648 -0
  49. package/dist/query/canonical/query-schema.d.ts +25 -0
  50. package/dist/query/canonical/query-schema.js +83 -0
  51. package/dist/query/canton-manager.js +14 -5
  52. package/dist/query/errors/query-snapshot-incomplete-error.d.ts +14 -0
  53. package/dist/query/errors/query-snapshot-incomplete-error.js +15 -0
  54. package/dist/query/grpc/grpc-contract-cache.d.ts +27 -0
  55. package/dist/query/grpc/grpc-contract-cache.js +479 -0
  56. package/dist/query/grpc/grpc-package-relation-reader.d.ts +43 -0
  57. package/dist/query/grpc/grpc-package-relation-reader.js +213 -0
  58. package/dist/query/grpc/grpc-query-client.d.ts +42 -0
  59. package/dist/query/grpc/grpc-query-client.js +245 -0
  60. package/dist/query/grpc/grpc-query-snapshot-reader.d.ts +33 -0
  61. package/dist/query/grpc/grpc-query-snapshot-reader.js +233 -0
  62. package/dist/query/grpc/grpc-query-value-mapper.d.ts +15 -0
  63. package/dist/query/grpc/grpc-query-value-mapper.js +195 -0
  64. package/dist/query/grpc/grpc-relation-mapper.d.ts +67 -0
  65. package/dist/query/grpc/grpc-relation-mapper.js +789 -0
  66. package/dist/query/model-types.d.ts +2 -0
  67. package/dist/query/pqs/pqs-query-client.d.ts +10 -20
  68. package/dist/query/pqs/pqs-query-client.js +154 -585
  69. package/dist/query/pqs/pqs-relational-sql-compiler.d.ts +9 -17
  70. package/dist/query/pqs/pqs-relational-sql-compiler.js +136 -95
  71. package/dist/query/pqs/pqs-result-shape.d.ts +25 -0
  72. package/dist/query/pqs/pqs-result-shape.js +25 -0
  73. package/dist/query/pqs/pqs-schema-profile.js +49 -48
  74. package/dist/query/pqs/pqs-sql-compiler.d.ts +18 -3
  75. package/dist/query/pqs/pqs-sql-compiler.js +337 -288
  76. package/dist/query/pqs/pqs-sql-syntax.d.ts +2 -0
  77. package/dist/query/pqs/pqs-sql-syntax.js +6 -0
  78. package/dist/query/query-client.d.ts +16 -0
  79. package/dist/services/command/command-service-client.d.ts +9 -4
  80. package/dist/services/command/command-service-client.js +16 -3
  81. package/dist/services/command-submission/command-submission-service-client.d.ts +2 -2
  82. package/dist/services/commands/command-payload-builder.d.ts +2 -2
  83. package/dist/services/commands/command-payload-builder.js +1 -1
  84. package/dist/services/commands/command-submission-pipeline.d.ts +5 -4
  85. package/dist/services/commands/command-submission-pipeline.js +21 -6
  86. package/dist/testing/runtime/declarative-action-executor.js +3 -3
  87. package/dist/transports/grpc/grpc-transport.d.ts +4 -4
  88. package/dist/transports/grpc/grpc-transport.js +3 -3
  89. package/dist/transports/grpc/mappers/commands-mapper.d.ts +3 -3
  90. package/dist/transports/grpc/mappers/commands-mapper.js +4 -4
  91. package/dist/transports/grpc/mappers/interactive-command-mapper.d.ts +3 -3
  92. package/dist/transports/grpc/mappers/interactive-command-mapper.js +1 -1
  93. package/dist/transports/json/json-transport.d.ts +2 -2
  94. package/dist/transports/json/json-transport.js +2 -2
  95. package/dist/transports/json/mappers/commands-mapper.d.ts +2 -2
  96. package/dist/transports/json/mappers/commands-mapper.js +2 -2
  97. package/node/start-local-splice-0.6.12.sh +6 -0
  98. package/node/start-local-splice-0.6.13.sh +6 -0
  99. package/node/start-local-splice-0.6.14.sh +6 -0
  100. package/node/start-local-splice-0.7.0.sh +6 -0
  101. package/node/stop-local-splice-0.6.12.sh +6 -0
  102. package/node/stop-local-splice-0.6.13.sh +6 -0
  103. package/node/stop-local-splice-0.6.14.sh +6 -0
  104. package/node/stop-local-splice-0.7.0.sh +6 -0
  105. package/node/test-start-local-splice-0.6.12.sh +28 -0
  106. package/node/test-start-local-splice-0.6.13.sh +28 -0
  107. package/node/test-start-local-splice-0.6.14.sh +28 -0
  108. package/node/test-start-local-splice-0.7.0.sh +28 -0
  109. package/node/test-stop-local-splice-0.6.12.sh +28 -0
  110. package/node/test-stop-local-splice-0.6.13.sh +28 -0
  111. package/node/test-stop-local-splice-0.6.14.sh +28 -0
  112. package/node/test-stop-local-splice-0.7.0.sh +28 -0
  113. package/package.json +29 -4
  114. package/dist/cjs/query/grpc/grpc-contract-query-client.js +0 -178
  115. package/dist/query/grpc/grpc-contract-query-client.d.ts +0 -33
  116. package/dist/query/grpc/grpc-contract-query-client.js +0 -174
@@ -1,363 +1,412 @@
1
- import { assertQueryPageArgs, } from "../model-types.js";
2
- import { PqsSchemaProfileV1 } from "./pqs-schema-profile.js";
3
1
  import { pqsRelationEdges, pqsRelationMetadata } from "./pqs-schema-profile.js";
4
- export function compileContractFindMany(args, profile) {
5
- assertQueryPageArgs(args);
2
+ import { quotePqsIdentifier, quotePqsString } from "./pqs-sql-syntax.js";
3
+ import { compilePqsResultShape } from "./pqs-result-shape.js";
4
+ import { compileCanonicalPublicNumericIdentityPartsSql, compileCanonicalPublicNumericIdentitySql } from "../canonical/public-identity.js";
5
+ export function compileCanonicalPhysicalField(relation, field, alias, profile) {
6
+ const column = pqsRelationMetadata[relation].fields[field];
7
+ if (column === undefined)
8
+ throw new Error(`${field} is not a field of ${relation}`);
9
+ const physical = qualified(alias, column);
10
+ if (relation === "__events" && field === "eventId") {
11
+ return eventIdExpression(physical);
12
+ }
13
+ if (relation === "__events" && field === "pk") {
14
+ return compileCanonicalPublicNumericIdentitySql(eventIdExpression(qualified(alias, "event_id")));
15
+ }
16
+ if (relation === "__events" && field === "txIx") {
17
+ return transactionOffsetExpression(profile, physical);
18
+ }
19
+ if (relation === "__events" && field === "type") {
20
+ return `case ${physical}::text when 'create' then 'created' when 'exercise' then 'exercised' else ${physical}::text end`;
21
+ }
22
+ if (relation === "__transactions" && field === "workflowId") {
23
+ return `nullif(${physical}, '')`;
24
+ }
25
+ if (relation === "__transactions" && field === "traceContext") {
26
+ return `case when ${physical} is null then null else jsonb_strip_nulls(jsonb_build_object('traceparent', nullif(to_jsonb(${physical})->>'trace_parent', ''), 'tracestate', nullif(to_jsonb(${physical})->>'trace_state', ''))) end`;
27
+ }
28
+ if ((relation === "__transactions" || relation === "__watermark") && (field === "ix" || field === "offset")) {
29
+ return qualified(alias, "offset");
30
+ }
31
+ if (relation === "__packages" && field === "pk") {
32
+ return compileCanonicalPublicNumericIdentitySql(qualified(alias, "id"));
33
+ }
34
+ if (relation === "__contract_tpe" && field === "pk") {
35
+ return compileCanonicalPublicNumericIdentitySql(compileCanonicalPublicNumericIdentityPartsSql([qualified(alias, "payload_type"), qualified(alias, "template_fqn")]));
36
+ }
37
+ if (relation === "__exercise_tpe" && field === "pk") {
38
+ return compileCanonicalPublicNumericIdentitySql(qualified(alias, "choice_fqn"));
39
+ }
40
+ if (relation === "__exercises" && field === "tpePk") {
41
+ return exerciseTypePublicKeyExpression(profile, physical);
42
+ }
43
+ if (relation === "__exercises" && field === "contractTpePk") {
44
+ return contractTypePublicKeyExpression(profile, physical);
45
+ }
46
+ if (relation === "__exercises" && field === "exerciseEventPk") {
47
+ return eventPublicKeyExpression(profile, physical);
48
+ }
49
+ if (relation === "__exercises" && field === "exercisedAtIx") {
50
+ return transactionOffsetExpression(profile, physical);
51
+ }
52
+ if (relation === "__exercises" && field === "packagePk") {
53
+ return packagePublicKeyExpression(profile, physical);
54
+ }
55
+ return physical;
56
+ }
57
+ function eventIdExpression(physical) {
58
+ return `(to_jsonb(${physical})->>'offset') || ':' || (to_jsonb(${physical})->>'node_id')`;
59
+ }
60
+ function transactionOffsetExpression(profile, physicalIx) {
61
+ return `(select "canonical_transaction"."offset" from ${profile.relation("__transactions")} "canonical_transaction" where "canonical_transaction"."ix" = ${physicalIx})`;
62
+ }
63
+ function eventPublicKeyExpression(profile, physicalPk) {
64
+ const eventId = eventIdExpression('"canonical_event"."event_id"');
65
+ return `(select ${compileCanonicalPublicNumericIdentitySql(eventId)} from ${profile.relation("__events")} "canonical_event" where "canonical_event"."pk" = ${physicalPk})`;
66
+ }
67
+ function packagePublicKeyExpression(profile, physicalPk) {
68
+ return `(select ${compileCanonicalPublicNumericIdentitySql('"canonical_package"."id"')} from ${profile.relation("__packages")} "canonical_package" where "canonical_package"."pk" = ${physicalPk})`;
69
+ }
70
+ function contractTypePublicKeyExpression(profile, physicalPk) {
71
+ const identity = compileCanonicalPublicNumericIdentityPartsSql(['"canonical_contract_type"."payload_type"', '"canonical_contract_type"."template_fqn"']);
72
+ return `(select ${compileCanonicalPublicNumericIdentitySql(identity)} from ${profile.relation("__contract_tpe")} "canonical_contract_type" where "canonical_contract_type"."pk" = ${physicalPk})`;
73
+ }
74
+ function exerciseTypePublicKeyExpression(profile, physicalPk) {
75
+ return `(select ${compileCanonicalPublicNumericIdentitySql('"canonical_exercise_type"."choice_fqn"')} from ${profile.relation("__exercise_tpe")} "canonical_exercise_type" where "canonical_exercise_type"."pk" = ${physicalPk})`;
76
+ }
77
+ export function compileContractFindMany(query, profile) {
78
+ if (query.relation !== "contracts")
79
+ throw new Error("compileContractFindMany requires a contracts query");
6
80
  const values = [];
7
81
  const addValue = (value) => {
8
82
  values.push(value);
9
83
  return `$${values.length}`;
10
84
  };
11
85
  const conditions = [];
12
- const where = args.where === undefined ? undefined : compileWhere(args.where, addValue, profile);
86
+ const where = query.predicate === undefined ? undefined : compileCanonicalContractPredicate(query.predicate, addValue, profile, "contract_row");
13
87
  if (where !== undefined)
14
88
  conditions.push(where);
15
- if (args.parties !== undefined) {
16
- conditions.push(`contract_row.witnesses && ${addValue(args.parties)}::text[]`);
89
+ if (query.parties !== undefined) {
90
+ conditions.push(`contract_row.witnesses && ${addValue(query.parties)}::text[]`);
17
91
  }
18
- const orderBy = compileOrderBy(args.orderBy);
92
+ const orderBy = compileCanonicalContractOrderBy(query.orderBy);
19
93
  const whereSql = conditions.length === 0 ? "" : `where ${conditions.join(" and ")}`;
20
- const limitSql = args.take === undefined ? "" : `limit ${addValue(args.take)}`;
21
- const offsetSql = args.skip === undefined ? "" : `offset ${addValue(args.skip)}`;
22
- const included = compileProfileIncludes("__contracts", "contract_row", args.include, profile, addValue);
23
- const jsonProjection = Object.entries(args.select?.json ?? {}).map(([name, projection]) => {
24
- if (projection.field !== "payload")
25
- throw new Error(`${projection.field} is not a JSON field of contracts`);
26
- if (projection.path.length === 0 || projection.path.some((segment) => segment.length === 0))
27
- throw new Error(`${name}.path must be a non-empty JSON path`);
94
+ const limitSql = query.take === undefined ? "" : `limit ${addValue(query.take)}`;
95
+ const offsetSql = query.skip === 0 ? "" : `offset ${addValue(query.skip)}`;
96
+ const included = compileCanonicalPhysicalIncludes("__contracts", "contract_row", query.includes, profile, addValue);
97
+ const jsonProjection = (query.select?.json ?? []).map((projection) => {
28
98
  const text = `contract_row.payload #>> ${addValue(projection.path)}::text[]`;
29
99
  const expression = projection.as === "text" ? text : projection.as === "numeric" ? `(${text})::numeric::text` : projection.as === "boolean" ? `(${text})::boolean` : `(${text})::timestamptz`;
30
- return `${expression} as "${name}"`;
100
+ return `${expression} as ${quotePqsIdentifier(projection.name)}`;
31
101
  });
32
102
  return {
33
103
  text: `select
34
104
  contract_row.contract_id as contract_id,
35
- contract_row.creation_package_id as package_id,
105
+ ${contractPackageIdExpression("contract_row", profile)} as package_id,
36
106
  contract_row.payload,
37
107
  contract_row.witnesses,
38
- contract_row.created_at_ix::text as created_event_offset,
108
+ created_tx.offset::text as created_event_offset,
39
109
  created_tx.effective_at as created_at,
40
- contract_row.archived_at_ix::text as archived_event_offset,
110
+ archived_tx.offset::text as archived_event_offset,
41
111
  archived_tx.effective_at as archived_at,
42
112
  contract_row.archived_at_ix is null as active,
43
- contract_row.creation_package_id as template_package_id,
113
+ ${contractPackageIdExpression("contract_row", profile)} as template_package_id,
44
114
  contract_tpe_row.module_name as template_module_name,
45
- contract_tpe_row.entity_name as template_entity_name${[...included, ...jsonProjection].length === 0 ? "" : `,\n ${[...included, ...jsonProjection].join(",\n ")}`}
115
+ contract_tpe_row.entity_name as template_entity_name${[...included.map((include) => include.selection), ...jsonProjection].length === 0 ? "" : `,\n ${[...included.map((include) => include.selection), ...jsonProjection].join(",\n ")}`}
46
116
  from ${profile.relation("__contracts")} contract_row
47
117
  join ${profile.relation("__contract_tpe")} contract_tpe_row on contract_tpe_row.pk = contract_row.tpe_pk
48
118
  left join ${profile.relation("__transactions")} created_tx on created_tx.ix = contract_row.created_at_ix
49
119
  left join ${profile.relation("__transactions")} archived_tx on archived_tx.ix = contract_row.archived_at_ix
120
+ left join ${profile.relation("__packages")} contract_package on contract_package.pk = contract_row.package_pk
50
121
  ${whereSql}
51
122
  ${orderBy}
52
123
  ${limitSql}
53
124
  ${offsetSql}`,
54
125
  values,
126
+ resultShape: compilePqsResultShape("__contracts", "many", query.select, query.includes),
55
127
  };
56
128
  }
57
- function compileProfileIncludes(source, parentAlias, include, profile, addValue) {
129
+ export function compileContractCount(query, profile) {
130
+ if (query.relation !== "contracts")
131
+ throw new Error("compileContractCount requires a contracts query");
132
+ const values = [];
133
+ const add = (value) => { values.push(value); return `$${values.length}`; };
134
+ const where = compileContractWhere(query.predicate, query.parties, profile, add);
135
+ return { text: `select count(*)::text as count ${contractFrom(profile)}${where}`, values };
136
+ }
137
+ export function compileContractAggregate(query, profile) {
138
+ if (query.relation !== "contracts")
139
+ throw new Error("compileContractAggregate requires a contracts query");
140
+ const values = [];
141
+ const add = (value) => { values.push(value); return `$${values.length}`; };
58
142
  const selected = [];
59
- for (const [name, option] of Object.entries(include ?? {})) {
60
- const edge = pqsRelationEdges[source]?.[name];
61
- if (edge === undefined)
62
- throw new Error(`${name} is not a relation of ${source}`);
63
- const settings = option === true ? {} : option;
64
- if (settings === null || typeof settings !== "object" || Array.isArray(settings))
65
- throw new Error(`${name} must be an include option`);
66
- const options = settings;
67
- if (edge.cardinality === "many" && (typeof options.take !== "number" || !Number.isInteger(options.take) || options.take < 0))
68
- throw new Error(`${name} is a to-many relation and requires a non-negative take`);
69
- const alias = `"${name}"`;
70
- const nested = compileProfileIncludes(edge.target, alias, options.include, profile, addValue);
71
- const metadata = pqsRelationMetadata[edge.target];
72
- const requested = compileIncludedFields(edge.target, options.select, alias, profile);
73
- const json = options.select?.json ?? {};
74
- const jsonSelections = Object.entries(json).map(([projectionName, projection]) => {
75
- if (!PqsSchemaProfileV1.jsonField(edge.target, projection.field))
76
- throw new Error(`${projection.field} is not a JSON field of ${edge.target}`);
77
- if (projection.path.length === 0 || projection.path.some((segment) => segment.length === 0))
78
- throw new Error(`${projectionName}.path must be a non-empty JSON path`);
79
- const column = edge.target === "__contracts"
80
- ? projection.field === "payload" ? "payload" : undefined
81
- : metadata.fields[projection.field];
82
- if (column === undefined)
83
- throw new Error(`${projection.field} is not a field of ${edge.target}`);
84
- const text = `${alias}."${column}" #>> ${addValue(projection.path)}::text[]`;
143
+ if (query.aggregates.count)
144
+ selected.push("count(*)::text as count");
145
+ const fields = { createdEventOffset: "created_tx.offset", archivedEventOffset: "archived_tx.offset" };
146
+ for (const [operation, requested] of [["min", query.aggregates.min], ["max", query.aggregates.max], ["sum", query.aggregates.sum]]) {
147
+ for (const field of requested) {
148
+ const expression = fields[field];
149
+ if (expression === undefined)
150
+ throw new Error(`${field} is not a numeric aggregate field of contracts`);
151
+ selected.push(`${operation}(${expression})::text as ${quotePqsIdentifier(`${operation}_${field}`)}`);
152
+ }
153
+ }
154
+ if (selected.length === 0)
155
+ throw new Error("aggregate must request at least one result");
156
+ return { text: `select ${selected.join(", ")} ${contractFrom(profile)}${compileContractWhere(query.predicate, undefined, profile, add)}`, values };
157
+ }
158
+ function contractFrom(profile) {
159
+ return `from ${profile.relation("__contracts")} contract_row join ${profile.relation("__contract_tpe")} contract_tpe_row on contract_tpe_row.pk = contract_row.tpe_pk left join ${profile.relation("__transactions")} created_tx on created_tx.ix = contract_row.created_at_ix left join ${profile.relation("__transactions")} archived_tx on archived_tx.ix = contract_row.archived_at_ix left join ${profile.relation("__packages")} contract_package on contract_package.pk = contract_row.package_pk`;
160
+ }
161
+ function compileContractWhere(predicate, parties, profile, add) {
162
+ const conditions = [
163
+ ...(predicate === undefined ? [] : [compileCanonicalContractPredicate(predicate, add, profile, "contract_row")]),
164
+ ...(parties === undefined ? [] : [`contract_row.witnesses && ${add(parties)}::text[]`]),
165
+ ];
166
+ return conditions.length === 0 ? "" : ` where ${conditions.join(" and ")}`;
167
+ }
168
+ export function compileCanonicalPhysicalIncludes(source, parentAlias, includes, profile, add) {
169
+ return includes.map((include) => {
170
+ const edge = pqsRelationEdges[source]?.[include.edge];
171
+ if (edge === undefined || edge.target !== pqsRelation(include.relation))
172
+ throw new Error(`Invalid canonical include ${include.edge}`);
173
+ const alias = quotePqsIdentifier(include.edge);
174
+ const fields = compileCanonicalIncludedFields(edge.target, include.select, alias, profile);
175
+ const json = (include.select?.json ?? []).flatMap((projection) => {
176
+ const base = edge.target === "__contracts"
177
+ ? projection.field === "payload" ? `${alias}."payload"` : undefined
178
+ : pqsRelationMetadata[edge.target].fields[projection.field] === undefined ? undefined : compileCanonicalPhysicalField(edge.target, projection.field, alias, profile);
179
+ if (base === undefined)
180
+ throw new Error(`Invalid canonical JSON projection ${projection.field}`);
181
+ const text = `${base} #>> ${add(projection.path)}::text[]`;
85
182
  const expression = projection.as === "text" ? text : projection.as === "numeric" ? `(${text})::numeric::text` : projection.as === "boolean" ? `(${text})::boolean` : `(${text})::timestamptz`;
86
- return [`'${projectionName}'`, expression];
183
+ return [quotePqsString(projection.name), expression];
87
184
  });
88
- if (requested.length === 0 && jsonSelections.length === 0)
89
- throw new Error(`Nested ${name}.select must include at least one field`);
90
- const fields = requested.flatMap(([field, expression]) => [`'${field}'`, expression]);
91
- const nestedFields = nested.flatMap((selection) => {
92
- const match = /^(.*) as "([^"]+)"$/.exec(selection);
93
- return match === null ? [] : [`'${match[2]}'`, match[1]];
94
- });
95
- const object = `jsonb_build_object(${[...fields, ...jsonSelections.flat(), ...nestedFields].join(", ")})`;
96
- const relationFilter = options.where === undefined ? "true" : compilePhysicalWhere(edge.target, options.where, alias, profile, addValue);
97
- const condition = `${alias}."${edge.targetColumn}" = ${parentAlias}."${edge.sourceColumn}" and (${relationFilter})`;
98
- const orderBy = compilePhysicalOrderBy(edge.target, options.orderBy, alias);
99
- const expression = edge.cardinality === "one"
185
+ const nested = compileCanonicalPhysicalIncludes(edge.target, alias, include.includes, profile, add);
186
+ const nestedFields = nested.flatMap((selection) => [quotePqsString(selection.key), selection.expression]);
187
+ const object = `jsonb_build_object(${[...fields.flatMap(([field, expression]) => [`'${field}'`, expression]), ...json.flat(), ...nestedFields].join(", ")})`;
188
+ const filter = include.predicate === undefined ? "true" : compileCanonicalPhysicalPredicate(edge.target, include.predicate, alias, profile, add);
189
+ const condition = `${compileCanonicalPhysicalEdgeJoin(source, include.edge, parentAlias, alias, profile)} and (${filter})`;
190
+ const orderBy = compileCanonicalPhysicalOrderBy(edge.target, include.orderBy, alias, profile);
191
+ const limit = include.cardinality === "many" ? ` limit ${add(include.take)}` : "";
192
+ const offset = include.cardinality === "many" && include.skip > 0 ? ` offset ${add(include.skip)}` : "";
193
+ const expression = include.cardinality === "one"
100
194
  ? `(select ${object} from ${profile.relation(edge.target)} ${alias} where ${condition})`
101
- : `(select coalesce(jsonb_agg("${name}_limited".value), '[]'::jsonb) from (select ${object} as value from ${profile.relation(edge.target)} ${alias} where ${condition}${orderBy} limit ${addValue(options.take)}) "${name}_limited")`;
102
- selected.push(`${expression} as "${name}"`);
103
- }
104
- return selected;
195
+ : `(select coalesce(jsonb_agg("${include.edge}_limited".value), '[]'::jsonb) from (select ${object} as value from ${profile.relation(edge.target)} ${alias} where ${condition}${orderBy}${limit}${offset}) "${include.edge}_limited")`;
196
+ return { key: include.edge, expression, selection: `${expression} as ${quotePqsIdentifier(include.edge)}` };
197
+ });
105
198
  }
106
- function compileIncludedFields(target, select, alias, profile) {
199
+ function compileCanonicalIncludedFields(target, select, alias, profile) {
107
200
  if (target !== "__contracts") {
108
201
  const metadata = pqsRelationMetadata[target];
109
- const selected = select === undefined
110
- ? Object.entries(metadata.fields)
111
- : Object.entries(select).filter(([field, enabled]) => field !== "json" && enabled === true).map(([field]) => [field, metadata.fields[field]]);
112
- if (selected.some(([, column]) => column === undefined))
113
- throw new Error("Nested relation.select references an unknown field");
114
- return selected.map(([field, column]) => [field, `${alias}."${column}"`]);
202
+ const fields = select === undefined ? Object.keys(metadata.fields) : select.fields;
203
+ return fields.map((field) => {
204
+ const expression = compileCanonicalPhysicalField(target, field, alias, profile);
205
+ return [field, metadata.numericFields.includes(field) ? `${expression}::text` : expression];
206
+ });
115
207
  }
116
- const selected = select === undefined
117
- ? ["contractId", "templateId", "packageId", "payload", "witnesses", "createdEventOffset", "createdAt", "archivedEventOffset", "archivedAt", "active"]
118
- : Object.entries(select).filter(([field, enabled]) => field !== "json" && enabled === true).map(([field]) => field);
208
+ const fields = select === undefined ? ["contractId", "templateId", "packageId", "payload", "witnesses", "createdEventOffset", "createdAt", "archivedEventOffset", "archivedAt", "active"] : select.fields;
119
209
  const expressions = {
120
- contractId: `${alias}."contract_id"`,
121
- templateId: `jsonb_build_object('packageId', ${alias}."creation_package_id", 'moduleName', (select contract_type."module_name" from ${profile.relation("__contract_tpe")} contract_type where contract_type."pk" = ${alias}."tpe_pk"), 'entityName', (select contract_type."entity_name" from ${profile.relation("__contract_tpe")} contract_type where contract_type."pk" = ${alias}."tpe_pk"))`,
122
- packageId: `${alias}."creation_package_id"`,
123
- payload: `${alias}."payload"`,
124
- witnesses: `${alias}."witnesses"`,
125
- createdEventOffset: `${alias}."created_at_ix"::text`,
126
- createdAt: `(select created_transaction."effective_at" from ${profile.relation("__transactions")} created_transaction where created_transaction."ix" = ${alias}."created_at_ix")`,
127
- archivedEventOffset: `${alias}."archived_at_ix"::text`,
128
- archivedAt: `(select archived_transaction."effective_at" from ${profile.relation("__transactions")} archived_transaction where archived_transaction."ix" = ${alias}."archived_at_ix")`,
129
- active: `${alias}."archived_at_ix" is null`,
210
+ contractId: `${alias}."contract_id"`, templateId: `jsonb_build_object('packageId', ${contractPackageIdExpression(alias, profile)}, 'moduleName', ${contractTypeFieldExpression(alias, "module_name", profile)}, 'entityName', ${contractTypeFieldExpression(alias, "entity_name", profile)})`, packageId: contractPackageIdExpression(alias, profile), payload: `${alias}."payload"`, witnesses: `${alias}."witnesses"`, createdEventOffset: `${contractTransactionFieldExpression(alias, "created", "offset", profile)}::text`, createdAt: contractTransactionFieldExpression(alias, "created", "effective_at", profile), archivedEventOffset: `${contractTransactionFieldExpression(alias, "archived", "offset", profile)}::text`, archivedAt: contractTransactionFieldExpression(alias, "archived", "effective_at", profile), active: `${alias}."archived_at_ix" is null`,
130
211
  };
131
- const requested = selected.map((field) => [field, expressions[field]]);
132
- if (requested.some(([, expression]) => expression === undefined))
133
- throw new Error("Nested contract.select references an unknown field");
134
- return requested;
212
+ return fields.map((field) => [field, expressions[field]]);
213
+ }
214
+ function compileCanonicalContractPredicate(predicate, add, profile, alias) {
215
+ return compileCanonicalPredicate("__contracts", predicate, alias, profile, add, true);
216
+ }
217
+ export function compileCanonicalPhysicalPredicate(relation, predicate, alias, profile, add) {
218
+ return compileCanonicalPredicate(relation, predicate, alias, profile, add, relation === "__contracts");
219
+ }
220
+ function compileCanonicalPredicate(relation, predicate, alias, profile, add, logicalContracts) {
221
+ if (predicate.kind === "and" || predicate.kind === "or")
222
+ return predicate.children.length === 0 ? predicate.kind === "and" ? "true" : "false" : `(${predicate.children.map((child) => compileCanonicalPredicate(relation, child, alias, profile, add, logicalContracts)).join(` ${predicate.kind} `)})`;
223
+ if (predicate.kind === "not")
224
+ return `not (${compileCanonicalPredicate(relation, predicate.child, alias, profile, add, logicalContracts)})`;
225
+ if (predicate.kind === "relation") {
226
+ const edge = pqsRelationEdges[relation]?.[predicate.edge];
227
+ if (edge === undefined)
228
+ throw new Error(`Invalid canonical edge ${predicate.edge}`);
229
+ const relatedAlias = `"${predicate.edge}"`;
230
+ const join = compileCanonicalPhysicalEdgeJoin(relation, predicate.edge, alias, relatedAlias, profile);
231
+ const condition = compileCanonicalPredicate(edge.target, predicate.predicate, relatedAlias, profile, add, edge.target === "__contracts");
232
+ const base = `select 1 from ${profile.relation(edge.target)} ${relatedAlias} where ${join} and (${condition})`;
233
+ return predicate.quantifier === "one" || predicate.quantifier === "some" ? `exists (${base})` : predicate.quantifier === "none" ? `not exists (${base})` : `not exists (select 1 from ${profile.relation(edge.target)} ${relatedAlias} where ${join} and not (${condition}))`;
234
+ }
235
+ if (predicate.kind !== "scalar")
236
+ throw new Error("Unknown canonical predicate");
237
+ const [field, ...path] = predicate.path;
238
+ if (logicalContracts && field === "active")
239
+ return `${logicalQualified(alias, "archived_at_ix")} is ${predicate.value === true ? "null" : "not null"}`;
240
+ if (logicalContracts && field === "witnesses")
241
+ return `${add(predicate.value)} = any(${logicalQualified(alias, "witnesses")})`;
242
+ const column = logicalContracts ? undefined : pqsRelationMetadata[relation].fields[field];
243
+ if (!logicalContracts && column === undefined)
244
+ throw new Error(`${field} is not a field of ${relation}`);
245
+ const canonicalExpression = logicalContracts
246
+ ? contractPredicateExpression(alias, field, path, profile, add)
247
+ : path.length > 0
248
+ ? `${compileCanonicalPhysicalField(relation, field, alias, profile)} #>> ${add(path)}::text[]`
249
+ : compileCanonicalPhysicalField(relation, field, alias, profile);
250
+ const inverseEventType = relation === "__events" && field === "type" && (predicate.operator === "equals" || predicate.operator === "in");
251
+ const expression = inverseEventType ? qualified(alias, column) : canonicalExpression;
252
+ const value = inverseEventType ? physicalEventType(predicate.value) : predicate.value;
253
+ const sql = { equals: "=", lt: "<", lte: "<=", gt: ">", gte: ">=", like: "like", ilike: "ilike" }[predicate.operator];
254
+ if (predicate.operator === "is")
255
+ return `${expression} is null`;
256
+ if (predicate.operator === "isNot")
257
+ return `${expression} is not null`;
258
+ if (predicate.operator === "in")
259
+ return value.length === 0 ? "false" : `${expression} = any(${add(value)})`;
260
+ if (predicate.operator === "has")
261
+ return `${add(value)} = any(${qualified(alias, column)})`;
262
+ if (sql === undefined)
263
+ throw new Error(`Unsupported canonical operator ${predicate.operator}`);
264
+ return `${expression} ${sql} ${add(value)}`;
265
+ }
266
+ function compileCanonicalPhysicalEdgeJoin(source, edgeName, parentAlias, targetAlias, profile) {
267
+ const edge = pqsRelationEdges[source]?.[edgeName];
268
+ if (edge === undefined)
269
+ throw new Error(`Invalid canonical edge ${edgeName}`);
270
+ if (source === "__contract_tpe" && edgeName === "contracts") {
271
+ return `${contractTypePublicKeyExpression(profile, qualified(targetAlias, "tpe_pk"))} = ${compileCanonicalPhysicalField(source, "pk", parentAlias, profile)}`;
272
+ }
273
+ if (source === "__contract_tpe" && edgeName === "exercises") {
274
+ return `${compileCanonicalPhysicalField("__exercises", "contractTpePk", targetAlias, profile)} = ${compileCanonicalPhysicalField(source, "pk", parentAlias, profile)}`;
275
+ }
276
+ if (source === "__exercise_tpe" && edgeName === "exercises") {
277
+ return `${compileCanonicalPhysicalField("__exercises", "tpePk", targetAlias, profile)} = ${compileCanonicalPhysicalField(source, "pk", parentAlias, profile)}`;
278
+ }
279
+ const parent = parentAlias.length === 0
280
+ ? `${profile.relation(source)}."${edge.sourceColumn}"`
281
+ : qualified(parentAlias, edge.sourceColumn);
282
+ return `${targetAlias}."${edge.targetColumn}" = ${parent}`;
283
+ }
284
+ function contractPredicateExpression(alias, field, path, profile, add) {
285
+ if (field === "payload")
286
+ return `${logicalQualified(alias, "payload")} #>> ${add(path)}::text[]`;
287
+ if (field === "contractId")
288
+ return logicalQualified(alias, "contract_id");
289
+ if (field === "packageId")
290
+ return contractPackageIdExpression(alias, profile);
291
+ if (field === "createdEventOffset")
292
+ return contractTransactionFieldExpression(alias, "created", "offset", profile);
293
+ if (field === "createdAt")
294
+ return contractTransactionFieldExpression(alias, "created", "effective_at", profile);
295
+ if (field === "archivedEventOffset")
296
+ return contractTransactionFieldExpression(alias, "archived", "offset", profile);
297
+ if (field === "archivedAt")
298
+ return contractTransactionFieldExpression(alias, "archived", "effective_at", profile);
299
+ if (field === "templateId" && path[0] === "packageId")
300
+ return contractPackageIdExpression(alias, profile);
301
+ if (field === "templateId" && path[0] === "moduleName")
302
+ return contractTypeFieldExpression(alias, "module_name", profile);
303
+ if (field === "templateId" && path[0] === "entityName")
304
+ return contractTypeFieldExpression(alias, "entity_name", profile);
305
+ throw new Error(`${field} is not a field of contracts`);
306
+ }
307
+ function physicalEventType(value) {
308
+ if (Array.isArray(value))
309
+ return value.map(physicalEventType);
310
+ if (value === "created")
311
+ return "create";
312
+ if (value === "exercised")
313
+ return "exercise";
314
+ return value;
315
+ }
316
+ function qualified(alias, column) {
317
+ return alias.length === 0 ? `"${column}"` : `${alias}."${column}"`;
318
+ }
319
+ function logicalQualified(alias, column) {
320
+ return alias === "contract_row" ? `${alias}.${column}` : qualified(alias, column);
135
321
  }
136
- function compilePhysicalOrderBy(relation, orderBy, alias) {
137
- if (orderBy === undefined)
138
- return "";
139
- if (!Array.isArray(orderBy) || orderBy.length === 0)
140
- throw new Error("Nested orderBy must be a non-empty list");
141
- const metadata = pqsRelationMetadata[relation];
142
- const entries = orderBy.flatMap((entry) => entry !== null && typeof entry === "object" ? Object.entries(entry) : []);
143
- if (entries.length !== orderBy.length || entries.some(([, direction]) => direction !== "asc" && direction !== "desc"))
144
- throw new Error("Nested orderBy entries must have one valid direction");
145
- return ` order by ${entries.map(([field, direction]) => {
146
- const column = metadata.fields[field];
147
- if (column === undefined)
148
- throw new Error(`${field} is not a field of ${relation}`);
149
- return `${alias}."${column}" ${direction}`;
150
- }).join(", ")}`;
322
+ function contractPackageIdExpression(alias, profile) {
323
+ const packageId = alias === "contract_row"
324
+ ? "contract_package.id"
325
+ : `(select contract_package."id" from ${profile.relation("__packages")} contract_package where contract_package."pk" = ${qualified(alias, "package_pk")})`;
326
+ return `coalesce(${logicalQualified(alias, "creation_package_id")}, ${packageId})`;
327
+ }
328
+ function contractTransactionFieldExpression(alias, lifecycle, field, profile) {
329
+ if (alias === "contract_row")
330
+ return `${lifecycle}_tx.${field}`;
331
+ const transaction = `${lifecycle}_transaction`;
332
+ return `(select ${transaction}."${field}" from ${profile.relation("__transactions")} ${transaction} where ${transaction}."ix" = ${qualified(alias, `${lifecycle}_at_ix`)})`;
333
+ }
334
+ function contractTypeFieldExpression(alias, field, profile) {
335
+ if (alias === "contract_row")
336
+ return `contract_tpe_row.${field}`;
337
+ return `(select contract_type."${field}" from ${profile.relation("__contract_tpe")} contract_type where contract_type."pk" = ${qualified(alias, "tpe_pk")})`;
338
+ }
339
+ function compileCanonicalContractOrderBy(orderBy) {
340
+ return compileCanonicalOrderBy(orderBy, { contractId: "contract_row.contract_id", createdEventOffset: "created_tx.offset", createdAt: "created_tx.effective_at", archivedEventOffset: "archived_tx.offset", archivedAt: "archived_tx.effective_at" });
341
+ }
342
+ function compileCanonicalPhysicalOrderBy(relation, orderBy, alias, profile) {
343
+ if (relation === "__contracts") {
344
+ return compileCanonicalOrderBy(orderBy, {
345
+ contractId: qualified(alias, "contract_id"),
346
+ createdEventOffset: contractTransactionFieldExpression(alias, "created", "offset", profile),
347
+ createdAt: contractTransactionFieldExpression(alias, "created", "effective_at", profile),
348
+ archivedEventOffset: contractTransactionFieldExpression(alias, "archived", "offset", profile),
349
+ archivedAt: contractTransactionFieldExpression(alias, "archived", "effective_at", profile),
350
+ });
351
+ }
352
+ return compileCanonicalOrderBy(orderBy, Object.fromEntries(Object.keys(pqsRelationMetadata[relation].fields).map((field) => [field, compileCanonicalPhysicalField(relation, field, alias, profile)])));
151
353
  }
152
- export function compileContractGroupBy(args, profile) {
153
- if (args.by.length === 0 || (!args.aggregate.count && !args.aggregate.min && !args.aggregate.max && !args.aggregate.sum))
354
+ function compileCanonicalOrderBy(orderBy, fields) {
355
+ return orderBy.length === 0 ? "" : ` order by ${orderBy.map((order) => `${fields[order.path[0]]} ${order.direction}`).join(", ")}`;
356
+ }
357
+ function pqsRelation(relation) {
358
+ return { contracts: "__contracts", contractTypes: "__contract_tpe", events: "__events", exercises: "__exercises", exerciseTypes: "__exercise_tpe", packages: "__packages", transactions: "__transactions", watermark: "__watermark" }[relation];
359
+ }
360
+ export function compileContractGroupBy(query, profile) {
361
+ if (query.relation !== "contracts")
362
+ throw new Error("compileContractGroupBy requires a contracts query");
363
+ if (query.by.length === 0 || (!query.aggregates.count && query.aggregates.min.length === 0 && query.aggregates.max.length === 0 && query.aggregates.sum.length === 0))
154
364
  throw new Error("groupBy requires keys and an aggregate");
155
365
  const values = [];
156
366
  const add = (value) => { values.push(value); return `$${values.length}`; };
157
- const where = args.where === undefined ? undefined : compileWhere(args.where, add, profile);
367
+ const where = query.predicate === undefined ? undefined : compileCanonicalContractPredicate(query.predicate, add, profile, "contract_row");
158
368
  const fields = {
159
369
  contractId: "contract_row.contract_id",
160
- createdEventOffset: "contract_row.created_at_ix",
161
- archivedEventOffset: "contract_row.archived_at_ix",
370
+ createdEventOffset: "created_tx.offset",
371
+ createdAt: "created_tx.effective_at",
372
+ archivedEventOffset: "archived_tx.offset",
373
+ archivedAt: "archived_tx.effective_at",
162
374
  };
163
375
  const expressions = [];
164
376
  const select = [];
165
377
  let unnestWitnesses = false;
166
- for (const key of args.by) {
167
- if (key === "witnesses") {
378
+ for (const key of query.by) {
379
+ if (key.kind === "field" && key.path[0] === "witnesses") {
168
380
  unnestWitnesses = true;
169
381
  expressions.push("witness.value");
170
382
  select.push("witness.value as \"witnesses\"");
171
383
  }
172
- else if (typeof key === "string") {
173
- const expression = fields[key];
384
+ else if (key.kind === "field") {
385
+ const expression = fields[key.path[0]];
174
386
  if (expression === undefined)
175
- throw new Error(`${key} is not a contract group key`);
387
+ throw new Error(`${key.path[0]} is not a contract group key`);
176
388
  expressions.push(expression);
177
- select.push(`${expression} as "${key}"`);
389
+ select.push(`${expression} as "${key.path[0]}"`);
178
390
  }
179
391
  else {
180
- const payload = key.payload;
181
- if (payload.path.length === 0 || payload.path.some((segment) => segment.length === 0))
182
- throw new Error("payload group path must be non-empty");
183
- const base = `contract_row.payload #>> ${add(payload.path)}::text[]`;
184
- const cast = payload.as === "text" ? base : `(${base})::${payload.as === "numeric" ? "numeric" : payload.as === "boolean" ? "boolean" : "timestamptz"}`;
392
+ if (key.kind !== "json" || key.field !== "payload")
393
+ throw new Error("invalid contract group key");
394
+ const base = `contract_row.payload #>> ${add(key.path)}::text[]`;
395
+ const cast = key.as === "text" ? base : `(${base})::${key.as === "numeric" ? "numeric" : key.as === "boolean" ? "boolean" : "timestamptz"}`;
185
396
  expressions.push(cast);
186
- select.push(`${cast} as "${payload.name}"`);
397
+ select.push(`${cast} as ${quotePqsIdentifier(key.name)}`);
187
398
  }
188
399
  }
189
- if (args.aggregate.count)
400
+ if (query.aggregates.count)
190
401
  select.push("count(*)::text as count");
191
- for (const [operation, requested] of [["min", args.aggregate.min], ["max", args.aggregate.max], ["sum", args.aggregate.sum]]) {
192
- for (const name of requested ?? []) {
402
+ for (const [operation, requested] of [["min", query.aggregates.min], ["max", query.aggregates.max], ["sum", query.aggregates.sum]]) {
403
+ for (const name of requested) {
193
404
  const expression = fields[name];
194
405
  select.push(`${operation}(${expression})::text as "${operation}_${name}"`);
195
406
  }
196
407
  }
197
408
  return {
198
- text: `select ${select.join(", ")} from ${profile.relation("__contracts")} contract_row${unnestWitnesses ? " cross join lateral unnest(contract_row.witnesses) as witness(value)" : ""}${where === undefined ? "" : ` where ${where}`} group by ${expressions.join(", ")}`,
409
+ text: `select ${select.join(", ")} ${contractFrom(profile)}${unnestWitnesses ? " cross join lateral unnest(contract_row.witnesses) as witness(value)" : ""}${where === undefined ? "" : ` where ${where}`} group by ${expressions.join(", ")}`,
199
410
  values,
200
411
  };
201
412
  }
202
- function compileWhere(where, addValue, profile) {
203
- const columns = { contractId: "contract_row.contract_id", packageId: "contract_row.creation_package_id", createdEventOffset: "contract_row.created_at_ix", createdAt: "created_tx.effective_at", archivedEventOffset: "contract_row.archived_at_ix", archivedAt: "archived_tx.effective_at" };
204
- const parts = [];
205
- for (const [key, value] of Object.entries(where)) {
206
- if (key === "and" || key === "or") {
207
- if (!Array.isArray(value))
208
- throw new Error(`${key} must be an array`);
209
- parts.push(value.length === 0 ? key === "and" ? "true" : "false" : `(${value.map((child) => compileWhere(child, addValue, profile)).join(` ${key} `)})`);
210
- continue;
211
- }
212
- if (key === "not") {
213
- parts.push(`not (${compileWhere(value, addValue, profile)})`);
214
- continue;
215
- }
216
- if (key === "active") {
217
- const active = typeof value === "boolean" ? value : value.equals;
218
- if (typeof active !== "boolean")
219
- throw new Error("active supports only equals");
220
- parts.push(active ? "contract_row.archived_at_ix is null" : "contract_row.archived_at_ix is not null");
221
- continue;
222
- }
223
- if (key === "witnesses") {
224
- const has = value.has;
225
- if (has === undefined)
226
- throw new Error("witnesses supports only has");
227
- parts.push(`${addValue(has)} = any(contract_row.witnesses)`);
228
- continue;
229
- }
230
- if (key === "payload") {
231
- const payload = value;
232
- const compilePayload = (path, filter) => { const ops = ["equals", "lt", "lte", "gt", "gte", "like", "ilike"].filter((op) => filter[op] !== undefined); if (ops.length === 1) {
233
- const op = { equals: "=", lt: "<", lte: "<=", gt: ">", gte: ">=", like: "like", ilike: "ilike" }[ops[0]];
234
- parts.push(`contract_row.payload #>> ${addValue(path)}::text[] ${op} ${addValue(filter[ops[0]])}`);
235
- return;
236
- } for (const [name, child] of Object.entries(filter))
237
- compilePayload([...path, name], child); };
238
- if (payload.match === undefined)
239
- throw new Error("payload requires match");
240
- compilePayload([], payload.match);
241
- continue;
242
- }
243
- if (key === "templateId") {
244
- const fields = { packageId: "contract_row.creation_package_id", moduleName: "contract_tpe_row.module_name", entityName: "contract_tpe_row.entity_name" };
245
- for (const [name, filter] of Object.entries(value))
246
- for (const [op, operand] of Object.entries(filter)) {
247
- const sql = { equals: "=", lt: "<", lte: "<=", gt: ">", gte: ">=", like: "like", ilike: "ilike" }[op];
248
- if (fields[name] === undefined || sql === undefined)
249
- throw new Error("invalid templateId filter");
250
- parts.push(`${fields[name]} ${sql} ${addValue(operand)}`);
251
- }
252
- continue;
253
- }
254
- const edge = pqsRelationEdges.__contracts?.[key];
255
- if (edge !== undefined) {
256
- parts.push(compilePhysicalRelationPredicate(edge.target, key, edge.targetColumn, edge.sourceColumn, edge.cardinality, value, "contract_row", profile, addValue));
257
- continue;
258
- }
259
- const column = columns[key];
260
- if (column === undefined || value === null || typeof value !== "object")
261
- throw new Error(`${key} is not a supported contract filter`);
262
- for (const [op, operand] of Object.entries(value)) {
263
- const sql = { equals: "=", lt: "<", lte: "<=", gt: ">", gte: ">=", like: "like", ilike: "ilike" }[op];
264
- if (sql !== undefined)
265
- parts.push(`${column} ${sql} ${addValue(operand)}`);
266
- else if (op === "is" && operand === null)
267
- parts.push(`${column} is null`);
268
- else if (op === "isNot" && operand === null)
269
- parts.push(`${column} is not null`);
270
- else if (op === "in" && Array.isArray(operand))
271
- parts.push(operand.length ? `${column} = any(${addValue(operand)})` : "false");
272
- else
273
- throw new Error(`${op} is not supported for ${key}`);
274
- }
275
- }
276
- return parts.length === 0 ? "true" : parts.join(" and ");
277
- }
278
- function compilePhysicalRelationPredicate(target, edgeName, targetColumn, sourceColumn, cardinality, value, parentAlias, profile, addValue) {
279
- if (value === null || typeof value !== "object" || Array.isArray(value))
280
- throw new Error(`${edgeName} must be a relation filter`);
281
- const relation = value;
282
- const alias = `"${edgeName}"`;
283
- const join = `${alias}."${targetColumn}" = ${parentAlias}."${sourceColumn}"`;
284
- if (cardinality === "one") {
285
- const predicate = compilePhysicalWhere(target, relation, alias, profile, addValue);
286
- return `exists (select 1 from ${profile.relation(target)} ${alias} where ${join} and (${predicate}))`;
287
- }
288
- const operators = ["some", "none", "every"].filter((name) => relation[name] !== undefined);
289
- if (operators.length !== 1)
290
- throw new Error(`${edgeName} requires exactly one of some, none, or every`);
291
- const predicate = compilePhysicalWhere(target, relation[operators[0]], alias, profile, addValue);
292
- const base = `select 1 from ${profile.relation(target)} ${alias} where ${join} and (${predicate})`;
293
- return operators[0] === "some" ? `exists (${base})` : operators[0] === "none" ? `not exists (${base})` : `not exists (select 1 from ${profile.relation(target)} ${alias} where ${join} and not (${predicate}))`;
294
- }
295
- function compilePhysicalWhere(relation, expression, alias, profile, addValue) {
296
- if (expression === null || typeof expression !== "object" || Array.isArray(expression))
297
- throw new Error(`${relation} relation predicate must be an expression`);
298
- const metadata = pqsRelationMetadata[relation];
299
- const conditions = [];
300
- for (const [field, value] of Object.entries(expression)) {
301
- if (field === "and" || field === "or") {
302
- if (!Array.isArray(value))
303
- throw new Error(`${field} must be an array`);
304
- conditions.push(value.length === 0 ? field === "and" ? "true" : "false" : `(${value.map((child) => compilePhysicalWhere(relation, child, alias, profile, addValue)).join(` ${field} `)})`);
305
- continue;
306
- }
307
- if (field === "not") {
308
- conditions.push(`not (${compilePhysicalWhere(relation, value, alias, profile, addValue)})`);
309
- continue;
310
- }
311
- const edge = pqsRelationEdges[relation]?.[field];
312
- if (edge !== undefined) {
313
- conditions.push(compilePhysicalRelationPredicate(edge.target, field, edge.targetColumn, edge.sourceColumn, edge.cardinality, value, alias, profile, addValue));
314
- continue;
315
- }
316
- const column = metadata.fields[field];
317
- if (column === undefined || value === null || typeof value !== "object" || Array.isArray(value))
318
- throw new Error(`${field} is not a field of ${relation}`);
319
- const filter = value;
320
- const text = PqsSchemaProfileV1.jsonField(relation, field) && Array.isArray(filter.path) ? `${alias}."${column}" #>> ${addValue(filter.path)}::text[]` : `${alias}."${column}"`;
321
- if (Array.isArray(filter.path) && (filter.path.length === 0 || filter.path.some((segment) => typeof segment !== "string" || segment.length === 0)))
322
- throw new Error(`${field}.path must be a non-empty JSON path`);
323
- if (filter.is === null)
324
- conditions.push(`${text} is null`);
325
- if (filter.isNot === null)
326
- conditions.push(`${text} is not null`);
327
- if (filter.equals !== undefined)
328
- conditions.push(`${text} = ${addValue(filter.equals)}`);
329
- if (filter.in !== undefined) {
330
- if (!Array.isArray(filter.in))
331
- throw new Error(`${field}.in must be an array`);
332
- conditions.push(filter.in.length === 0 ? "false" : `${text} = any(${addValue(filter.in)})`);
333
- }
334
- if (filter.has !== undefined) {
335
- if (!metadata.arrayFields.includes(field))
336
- throw new Error(`${field} is not an array field of ${relation}`);
337
- conditions.push(`${addValue(filter.has)} = any(${alias}."${column}")`);
338
- }
339
- for (const [operator, operand] of Object.entries(filter)) {
340
- const sql = { lt: "<", lte: "<=", gt: ">", gte: ">=", like: "like", ilike: "ilike" }[operator];
341
- if (sql !== undefined)
342
- conditions.push(`${text} ${sql} ${addValue(operand)}`);
343
- }
344
- }
345
- return conditions.length === 0 ? "true" : conditions.join(" and ");
346
- }
347
- function compileOrderBy(orderBy) {
348
- if (orderBy === undefined) {
349
- return "order by contract_row.contract_id asc";
350
- }
351
- const fields = {
352
- contractId: "contract_row.contract_id",
353
- createdEventOffset: "contract_row.created_at_ix",
354
- createdAt: "created_tx.effective_at",
355
- archivedEventOffset: "contract_row.archived_at_ix",
356
- archivedAt: "archived_tx.effective_at",
357
- };
358
- const entries = orderBy.flatMap((entry) => Object.entries(entry));
359
- if (entries.some(([field, direction]) => fields[field] === undefined || (direction !== "asc" && direction !== "desc"))) {
360
- throw new Error("orderBy must contain valid one-field entries");
361
- }
362
- return `order by ${entries.map(([field, direction]) => `${fields[field]} ${direction}`).join(", ")}`;
363
- }