@adcp/sdk 15.4.0 → 15.5.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 (115) hide show
  1. package/dist/lib/advanced.d.mts +2 -2
  2. package/dist/lib/advanced.d.ts +2 -2
  3. package/dist/lib/advanced.js +8 -2
  4. package/dist/lib/advanced.mjs +8 -2
  5. package/dist/lib/core/AgentClient.d.mts +4 -0
  6. package/dist/lib/core/AgentClient.d.ts +4 -0
  7. package/dist/lib/core/AgentClient.js +8 -0
  8. package/dist/lib/core/AgentClient.mjs +8 -0
  9. package/dist/lib/core/SingleAgentClient.d.mts +154 -0
  10. package/dist/lib/core/SingleAgentClient.d.ts +154 -0
  11. package/dist/lib/core/SingleAgentClient.js +661 -23
  12. package/dist/lib/core/SingleAgentClient.mjs +666 -23
  13. package/dist/lib/core/agent-discovery-cache.d.mts +103 -0
  14. package/dist/lib/core/agent-discovery-cache.d.ts +103 -0
  15. package/dist/lib/core/agent-discovery-cache.js +121 -0
  16. package/dist/lib/core/agent-discovery-cache.mjs +92 -0
  17. package/dist/lib/index.d.mts +5 -3
  18. package/dist/lib/index.d.ts +5 -3
  19. package/dist/lib/index.js +7 -0
  20. package/dist/lib/index.mjs +6 -0
  21. package/dist/lib/protocols/index.d.mts +2 -2
  22. package/dist/lib/protocols/index.d.ts +2 -2
  23. package/dist/lib/protocols/index.js +1 -1
  24. package/dist/lib/protocols/index.mjs +1 -1
  25. package/dist/lib/protocols/mcp-call-context.d.mts +109 -0
  26. package/dist/lib/protocols/mcp-call-context.d.ts +109 -0
  27. package/dist/lib/protocols/mcp-call-context.js +221 -0
  28. package/dist/lib/protocols/mcp-call-context.mjs +186 -0
  29. package/dist/lib/protocols/mcp-modern.d.mts +9 -1
  30. package/dist/lib/protocols/mcp-modern.d.ts +9 -1
  31. package/dist/lib/protocols/mcp-modern.js +180 -104
  32. package/dist/lib/protocols/mcp-modern.mjs +193 -104
  33. package/dist/lib/protocols/mcp-scope.d.mts +15 -0
  34. package/dist/lib/protocols/mcp-scope.d.ts +15 -0
  35. package/dist/lib/protocols/mcp.d.mts +4 -2
  36. package/dist/lib/protocols/mcp.d.ts +4 -2
  37. package/dist/lib/protocols/mcp.js +178 -154
  38. package/dist/lib/protocols/mcp.mjs +189 -153
  39. package/dist/lib/protocols/transportDiagnostics.d.mts +33 -1
  40. package/dist/lib/protocols/transportDiagnostics.d.ts +33 -1
  41. package/dist/lib/protocols/transportDiagnostics.js +128 -12
  42. package/dist/lib/protocols/transportDiagnostics.mjs +128 -12
  43. package/dist/lib/reporting/gcs/index.d.mts +2 -0
  44. package/dist/lib/reporting/gcs/index.d.ts +2 -0
  45. package/dist/lib/reporting/gcs/index.js +3 -0
  46. package/dist/lib/reporting/gcs/index.mjs +2 -0
  47. package/dist/lib/reporting/gcs/row-provider.d.mts +32 -0
  48. package/dist/lib/reporting/gcs/row-provider.d.ts +32 -0
  49. package/dist/lib/reporting/gcs/row-provider.js +205 -0
  50. package/dist/lib/reporting/gcs/row-provider.mjs +188 -0
  51. package/dist/lib/reporting/ledger/change-feed.d.mts +41 -0
  52. package/dist/lib/reporting/ledger/change-feed.d.ts +41 -0
  53. package/dist/lib/reporting/ledger/change-feed.js +101 -0
  54. package/dist/lib/reporting/ledger/change-feed.mjs +74 -0
  55. package/dist/lib/reporting/ledger/handler.js +22 -8
  56. package/dist/lib/reporting/ledger/handler.mjs +22 -8
  57. package/dist/lib/reporting/ledger/index.d.mts +4 -0
  58. package/dist/lib/reporting/ledger/index.d.ts +4 -0
  59. package/dist/lib/reporting/ledger/index.js +9 -1
  60. package/dist/lib/reporting/ledger/index.mjs +4 -0
  61. package/dist/lib/reporting/ledger/managed-postgres.d.mts +2 -0
  62. package/dist/lib/reporting/ledger/managed-postgres.d.ts +2 -0
  63. package/dist/lib/reporting/ledger/managed-postgres.js +21 -1
  64. package/dist/lib/reporting/ledger/managed-postgres.mjs +21 -1
  65. package/dist/lib/reporting/ledger/managed.d.mts +9 -0
  66. package/dist/lib/reporting/ledger/managed.d.ts +9 -0
  67. package/dist/lib/reporting/ledger/managed.js +18 -0
  68. package/dist/lib/reporting/ledger/managed.mjs +18 -0
  69. package/dist/lib/reporting/ledger/postgres.d.mts +175 -0
  70. package/dist/lib/reporting/ledger/postgres.d.ts +175 -0
  71. package/dist/lib/reporting/ledger/postgres.js +851 -26
  72. package/dist/lib/reporting/ledger/postgres.mjs +859 -26
  73. package/dist/lib/reporting/ledger/producer.js +10 -1
  74. package/dist/lib/reporting/ledger/producer.mjs +10 -1
  75. package/dist/lib/reporting/ledger/row-storage-errors.d.mts +7 -0
  76. package/dist/lib/reporting/ledger/row-storage-errors.d.ts +7 -0
  77. package/dist/lib/reporting/ledger/row-storage-errors.js +41 -0
  78. package/dist/lib/reporting/ledger/row-storage-errors.mjs +16 -0
  79. package/dist/lib/reporting/ledger/row-storage-migration.d.mts +41 -0
  80. package/dist/lib/reporting/ledger/row-storage-migration.d.ts +41 -0
  81. package/dist/lib/reporting/ledger/row-storage-migration.js +234 -0
  82. package/dist/lib/reporting/ledger/row-storage-migration.mjs +208 -0
  83. package/dist/lib/reporting/ledger/row-storage-object.d.mts +112 -0
  84. package/dist/lib/reporting/ledger/row-storage-object.d.ts +112 -0
  85. package/dist/lib/reporting/ledger/row-storage-object.js +281 -0
  86. package/dist/lib/reporting/ledger/row-storage-object.mjs +239 -0
  87. package/dist/lib/reporting/ledger/row-storage-provider-io.d.mts +51 -0
  88. package/dist/lib/reporting/ledger/row-storage-provider-io.d.ts +51 -0
  89. package/dist/lib/reporting/ledger/row-storage-provider-io.js +152 -0
  90. package/dist/lib/reporting/ledger/row-storage-provider-io.mjs +119 -0
  91. package/dist/lib/reporting/ledger/row-storage.d.mts +224 -0
  92. package/dist/lib/reporting/ledger/row-storage.d.ts +224 -0
  93. package/dist/lib/reporting/ledger/row-storage.js +692 -0
  94. package/dist/lib/reporting/ledger/row-storage.mjs +680 -0
  95. package/dist/lib/reporting/ledger/types.d.mts +8 -0
  96. package/dist/lib/reporting/ledger/types.d.ts +8 -0
  97. package/dist/lib/reporting/service/index.d.mts +41 -1
  98. package/dist/lib/reporting/service/index.d.ts +41 -1
  99. package/dist/lib/reporting/service/index.js +68 -1
  100. package/dist/lib/reporting/service/index.mjs +70 -1
  101. package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
  102. package/dist/lib/testing/storyboard/validations.d.mts +1 -1
  103. package/dist/lib/testing/storyboard/validations.d.ts +1 -1
  104. package/dist/lib/version.d.mts +3 -3
  105. package/dist/lib/version.d.ts +3 -3
  106. package/dist/lib/version.js +3 -3
  107. package/dist/lib/version.mjs +3 -3
  108. package/docs/README.md +1 -1
  109. package/docs/TYPE-SUMMARY.md +35 -2
  110. package/docs/guides/DISCOVERY-CACHE-AND-SESSION-REUSE.md +142 -0
  111. package/docs/guides/EXISTING-PLATFORM.md +43 -12
  112. package/docs/guides/REPORTING-LEDGER.md +209 -0
  113. package/docs/llms.txt +4 -2
  114. package/docs/migration-14.x-rc-worksheet.md +4 -4
  115. package/package.json +1 -1
@@ -124,6 +124,7 @@ function createReportingProducer(options) {
124
124
  (left, right) => instant(left.installedAt, "installedAt") - instant(right.installedAt, "installedAt") || left.delivery_config_version - right.delivery_config_version || left.configurationId.localeCompare(right.configurationId)
125
125
  );
126
126
  const obligationsByAccount = /* @__PURE__ */ new Map();
127
+ const retiredByAccount = /* @__PURE__ */ new Map();
127
128
  for (const configuration of configurations) {
128
129
  const successor = (0, import_coverage.reportingLedgerSuccessor)(configuration, configurations);
129
130
  const anchor = instant(configuration.schedule.anchor, "schedule.anchor");
@@ -161,7 +162,15 @@ function createReportingProducer(options) {
161
162
  accountObligations = await options.store.listObligations(configuration.account.account_id);
162
163
  obligationsByAccount.set(configuration.account.account_id, accountObligations);
163
164
  }
164
- const existingOrdinals = accountObligations.filter((value) => value.configurationId === configuration.configurationId).map((value) => value.periodOrdinal);
165
+ let accountRetired = retiredByAccount.get(configuration.account.account_id);
166
+ if (!accountRetired) {
167
+ accountRetired = await options.store.listRetiredObligationOrdinals?.(configuration.account.account_id) ?? [];
168
+ retiredByAccount.set(configuration.account.account_id, accountRetired);
169
+ }
170
+ const existingOrdinals = [
171
+ ...accountObligations.filter((value) => value.configurationId === configuration.configurationId).map((value) => value.periodOrdinal),
172
+ ...accountRetired.filter((value) => value.configurationId === configuration.configurationId).map((value) => value.periodOrdinal)
173
+ ];
165
174
  const candidates = missingOrdinals(first, last, existingOrdinals, maxObligations - attempted);
166
175
  for (const ordinal of candidates) {
167
176
  attempted += 1;
@@ -120,6 +120,7 @@ function createReportingProducer(options) {
120
120
  (left, right) => instant(left.installedAt, "installedAt") - instant(right.installedAt, "installedAt") || left.delivery_config_version - right.delivery_config_version || left.configurationId.localeCompare(right.configurationId)
121
121
  );
122
122
  const obligationsByAccount = /* @__PURE__ */ new Map();
123
+ const retiredByAccount = /* @__PURE__ */ new Map();
123
124
  for (const configuration of configurations) {
124
125
  const successor = reportingLedgerSuccessor(configuration, configurations);
125
126
  const anchor = instant(configuration.schedule.anchor, "schedule.anchor");
@@ -157,7 +158,15 @@ function createReportingProducer(options) {
157
158
  accountObligations = await options.store.listObligations(configuration.account.account_id);
158
159
  obligationsByAccount.set(configuration.account.account_id, accountObligations);
159
160
  }
160
- const existingOrdinals = accountObligations.filter((value) => value.configurationId === configuration.configurationId).map((value) => value.periodOrdinal);
161
+ let accountRetired = retiredByAccount.get(configuration.account.account_id);
162
+ if (!accountRetired) {
163
+ accountRetired = await options.store.listRetiredObligationOrdinals?.(configuration.account.account_id) ?? [];
164
+ retiredByAccount.set(configuration.account.account_id, accountRetired);
165
+ }
166
+ const existingOrdinals = [
167
+ ...accountObligations.filter((value) => value.configurationId === configuration.configurationId).map((value) => value.periodOrdinal),
168
+ ...accountRetired.filter((value) => value.configurationId === configuration.configurationId).map((value) => value.periodOrdinal)
169
+ ];
161
170
  const candidates = missingOrdinals(first, last, existingOrdinals, maxObligations - attempted);
162
171
  for (const ordinal of candidates) {
163
172
  attempted += 1;
@@ -0,0 +1,7 @@
1
+ export type ReportingRowStoreErrorCode = 'INVALID_INPUT' | 'CONTENT_CONFLICT' | 'ROWS_INTEGRITY_FAILED' | 'ROWS_UNAVAILABLE' | 'ROWS_EXPIRED' | 'PROVIDER_UNAVAILABLE' | 'DEADLINE_EXCEEDED' | 'ABORTED' | 'UNSAFE_BINDING' | 'STATE_UNAVAILABLE';
2
+ /** Stable, secret-free row-storage failure. Switch on `code`. */
3
+ export declare class ReportingRowStoreError extends Error {
4
+ readonly code: ReportingRowStoreErrorCode;
5
+ constructor(code: ReportingRowStoreErrorCode, detail?: string);
6
+ }
7
+ export declare function isReportingRowStoreError(error: unknown): error is ReportingRowStoreError;
@@ -0,0 +1,7 @@
1
+ export type ReportingRowStoreErrorCode = 'INVALID_INPUT' | 'CONTENT_CONFLICT' | 'ROWS_INTEGRITY_FAILED' | 'ROWS_UNAVAILABLE' | 'ROWS_EXPIRED' | 'PROVIDER_UNAVAILABLE' | 'DEADLINE_EXCEEDED' | 'ABORTED' | 'UNSAFE_BINDING' | 'STATE_UNAVAILABLE';
2
+ /** Stable, secret-free row-storage failure. Switch on `code`. */
3
+ export declare class ReportingRowStoreError extends Error {
4
+ readonly code: ReportingRowStoreErrorCode;
5
+ constructor(code: ReportingRowStoreErrorCode, detail?: string);
6
+ }
7
+ export declare function isReportingRowStoreError(error: unknown): error is ReportingRowStoreError;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var row_storage_errors_exports = {};
20
+ __export(row_storage_errors_exports, {
21
+ ReportingRowStoreError: () => ReportingRowStoreError,
22
+ isReportingRowStoreError: () => isReportingRowStoreError
23
+ });
24
+ module.exports = __toCommonJS(row_storage_errors_exports);
25
+ class ReportingRowStoreError extends Error {
26
+ constructor(code, detail) {
27
+ super(detail ? `Reporting row store: ${code}: ${detail}` : `Reporting row store: ${code}`);
28
+ this.code = code;
29
+ this.name = "ReportingRowStoreError";
30
+ Object.defineProperty(this, /* @__PURE__ */ Symbol.for("adcp.reportingRowStoreError"), { value: true });
31
+ }
32
+ code;
33
+ }
34
+ function isReportingRowStoreError(error) {
35
+ return !!error && typeof error === "object" && error[/* @__PURE__ */ Symbol.for("adcp.reportingRowStoreError")] === true;
36
+ }
37
+ // Annotate the CommonJS export names for ESM import in node:
38
+ 0 && (module.exports = {
39
+ ReportingRowStoreError,
40
+ isReportingRowStoreError
41
+ });
@@ -0,0 +1,16 @@
1
+ class ReportingRowStoreError extends Error {
2
+ constructor(code, detail) {
3
+ super(detail ? `Reporting row store: ${code}: ${detail}` : `Reporting row store: ${code}`);
4
+ this.code = code;
5
+ this.name = "ReportingRowStoreError";
6
+ Object.defineProperty(this, /* @__PURE__ */ Symbol.for("adcp.reportingRowStoreError"), { value: true });
7
+ }
8
+ code;
9
+ }
10
+ function isReportingRowStoreError(error) {
11
+ return !!error && typeof error === "object" && error[/* @__PURE__ */ Symbol.for("adcp.reportingRowStoreError")] === true;
12
+ }
13
+ export {
14
+ ReportingRowStoreError,
15
+ isReportingRowStoreError
16
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Row-storage tables for reporting revisions (shared SDK persistence spec,
3
+ * `reporting-row-storage` v1, adcontextprotocol/adcp#7996 §3.2).
4
+ *
5
+ * Apply after {@link REPORTING_LEDGER_MIGRATION}. PostgreSQL 13 or later
6
+ * (`gen_random_uuid()`, `sha256()`). Idempotent; reapplying preserves the
7
+ * installation identity and every recorded location.
8
+ *
9
+ * - `adcp_persistence_installation` holds one installation UUID per isolated
10
+ * schema. A restore of the same authority keeps it; an independently
11
+ * writable clone must mint a new one before it writes or sweeps.
12
+ * - `adcp_reporting_row_bindings` holds immutable, non-secret descriptions of
13
+ * where row bytes live. Only `state`, `operational_config` and `retired_at`
14
+ * may change.
15
+ * - `adcp_reporting_row_sets` records where a revision's (or adjustment's)
16
+ * rows live and binds the chunk manifest digest. A revision without a row
17
+ * set still carries its rows inline in its ledger document.
18
+ * - `adcp_reporting_row_chunks` is the per-chunk manifest; only location
19
+ * columns may change, through a guarded move.
20
+ * - `adcp_reporting_chunk_bodies` holds `postgres`-kind chunk bytes,
21
+ * content-addressed within one obligation so revisions that repeat their
22
+ * predecessor's rows share bodies.
23
+ * - `adcp_reporting_row_write_intents` fences external uploads so a sweep
24
+ * never deletes bytes a committing writer still needs.
25
+ */
26
+ export declare const REPORTING_ROW_STORAGE_MIGRATION: string;
27
+ /** Tables {@link REPORTING_ROW_STORAGE_MIGRATION} creates, in dependency order. */
28
+ export declare const REPORTING_ROW_STORAGE_TABLES: readonly ["adcp_persistence_installation", "adcp_reporting_row_bindings", "adcp_reporting_row_sets", "adcp_reporting_row_chunks", "adcp_reporting_chunk_bodies", "adcp_reporting_row_write_intents"];
29
+ interface QueryablePoolV1 {
30
+ query<Row extends Record<string, unknown> = Record<string, unknown>>(text: string, values?: unknown[]): Promise<{
31
+ rows: Row[];
32
+ }>;
33
+ }
34
+ /**
35
+ * Fail closed unless every row-storage table exists in the current schema.
36
+ * Returns the installation identity.
37
+ */
38
+ export declare function probeReportingRowStorageSchemaV1(pool: QueryablePoolV1): Promise<{
39
+ installationId: string;
40
+ }>;
41
+ export {};
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Row-storage tables for reporting revisions (shared SDK persistence spec,
3
+ * `reporting-row-storage` v1, adcontextprotocol/adcp#7996 §3.2).
4
+ *
5
+ * Apply after {@link REPORTING_LEDGER_MIGRATION}. PostgreSQL 13 or later
6
+ * (`gen_random_uuid()`, `sha256()`). Idempotent; reapplying preserves the
7
+ * installation identity and every recorded location.
8
+ *
9
+ * - `adcp_persistence_installation` holds one installation UUID per isolated
10
+ * schema. A restore of the same authority keeps it; an independently
11
+ * writable clone must mint a new one before it writes or sweeps.
12
+ * - `adcp_reporting_row_bindings` holds immutable, non-secret descriptions of
13
+ * where row bytes live. Only `state`, `operational_config` and `retired_at`
14
+ * may change.
15
+ * - `adcp_reporting_row_sets` records where a revision's (or adjustment's)
16
+ * rows live and binds the chunk manifest digest. A revision without a row
17
+ * set still carries its rows inline in its ledger document.
18
+ * - `adcp_reporting_row_chunks` is the per-chunk manifest; only location
19
+ * columns may change, through a guarded move.
20
+ * - `adcp_reporting_chunk_bodies` holds `postgres`-kind chunk bytes,
21
+ * content-addressed within one obligation so revisions that repeat their
22
+ * predecessor's rows share bodies.
23
+ * - `adcp_reporting_row_write_intents` fences external uploads so a sweep
24
+ * never deletes bytes a committing writer still needs.
25
+ */
26
+ export declare const REPORTING_ROW_STORAGE_MIGRATION: string;
27
+ /** Tables {@link REPORTING_ROW_STORAGE_MIGRATION} creates, in dependency order. */
28
+ export declare const REPORTING_ROW_STORAGE_TABLES: readonly ["adcp_persistence_installation", "adcp_reporting_row_bindings", "adcp_reporting_row_sets", "adcp_reporting_row_chunks", "adcp_reporting_chunk_bodies", "adcp_reporting_row_write_intents"];
29
+ interface QueryablePoolV1 {
30
+ query<Row extends Record<string, unknown> = Record<string, unknown>>(text: string, values?: unknown[]): Promise<{
31
+ rows: Row[];
32
+ }>;
33
+ }
34
+ /**
35
+ * Fail closed unless every row-storage table exists in the current schema.
36
+ * Returns the installation identity.
37
+ */
38
+ export declare function probeReportingRowStorageSchemaV1(pool: QueryablePoolV1): Promise<{
39
+ installationId: string;
40
+ }>;
41
+ export {};
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var row_storage_migration_exports = {};
20
+ __export(row_storage_migration_exports, {
21
+ REPORTING_ROW_STORAGE_MIGRATION: () => REPORTING_ROW_STORAGE_MIGRATION,
22
+ REPORTING_ROW_STORAGE_TABLES: () => REPORTING_ROW_STORAGE_TABLES,
23
+ probeReportingRowStorageSchemaV1: () => probeReportingRowStorageSchemaV1
24
+ });
25
+ module.exports = __toCommonJS(row_storage_migration_exports);
26
+ const REPORTING_ROW_STORAGE_MIGRATION = `
27
+ CREATE TABLE IF NOT EXISTS adcp_persistence_installation (
28
+ singleton BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (singleton),
29
+ installation_id UUID NOT NULL DEFAULT gen_random_uuid(),
30
+ created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
31
+ );
32
+ INSERT INTO adcp_persistence_installation (singleton) VALUES (TRUE) ON CONFLICT DO NOTHING;
33
+
34
+ CREATE TABLE IF NOT EXISTS adcp_reporting_row_bindings (
35
+ row_binding_id TEXT COLLATE "C" PRIMARY KEY CHECK (row_binding_id ~ '^[A-Za-z0-9_.:-]{1,128}$'),
36
+ kind TEXT NOT NULL CHECK (kind IN ('postgres', 'object')),
37
+ provider TEXT NOT NULL CHECK (provider ~ '^[a-z][a-z0-9_]{0,63}$'),
38
+ identity_config JSONB NOT NULL CHECK (jsonb_typeof(identity_config) = 'object'),
39
+ identity_sha256 TEXT COLLATE "C" NOT NULL CHECK (identity_sha256 ~ '^[0-9a-f]{64}$'),
40
+ operational_config JSONB NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(operational_config) = 'object'),
41
+ namespace_key TEXT COLLATE "C" NOT NULL CHECK (namespace_key ~ '^[0-9a-f]{64}$'),
42
+ state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active', 'read_only', 'retired')),
43
+ created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
44
+ retired_at TIMESTAMPTZ,
45
+ CHECK ((state = 'retired') = (retired_at IS NOT NULL))
46
+ );
47
+
48
+ CREATE TABLE IF NOT EXISTS adcp_reporting_row_sets (
49
+ row_set_id TEXT COLLATE "C" PRIMARY KEY,
50
+ row_set_kind TEXT NOT NULL CHECK (row_set_kind IN ('revision', 'adjustment')),
51
+ account_id TEXT COLLATE "C" NOT NULL,
52
+ obligation_id TEXT COLLATE "C" NOT NULL REFERENCES adcp_reporting_obligations(obligation_id),
53
+ encoding TEXT NOT NULL CHECK (encoding = 'adcp_canonical_jsonl_v1'),
54
+ digest_profile TEXT NOT NULL CHECK (digest_profile IN ('revision_envelope_v1', 'rows_v1')),
55
+ content_sha256 TEXT COLLATE "C" NOT NULL CHECK (content_sha256 ~ '^[0-9a-f]{64}$'),
56
+ canonical_byte_count BIGINT NOT NULL CHECK (canonical_byte_count > 0),
57
+ row_count BIGINT NOT NULL CHECK (row_count >= 0),
58
+ row_manifest_sha256 TEXT COLLATE "C" NOT NULL CHECK (row_manifest_sha256 ~ '^[0-9a-f]{64}$'),
59
+ chunk_count INTEGER NOT NULL CHECK (chunk_count >= 0),
60
+ rows_shared_from_row_set_id TEXT COLLATE "C",
61
+ row_binding_id TEXT COLLATE "C" NOT NULL REFERENCES adcp_reporting_row_bindings(row_binding_id),
62
+ row_location_version INTEGER NOT NULL DEFAULT 1 CHECK (row_location_version > 0),
63
+ rows_state TEXT NOT NULL DEFAULT 'live' CHECK (rows_state IN ('live', 'pruning', 'pruned', 'unavailable')),
64
+ rows_state_changed_at TIMESTAMPTZ,
65
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
66
+ CHECK ((row_count = 0) = (chunk_count = 0)),
67
+ CHECK (rows_shared_from_row_set_id IS NULL OR rows_shared_from_row_set_id <> row_set_id),
68
+ -- Tenant columns are the composite key children reference, so a chunk or a
69
+ -- shared row set can never name another account's or obligation's rows.
70
+ UNIQUE (row_set_id, account_id, obligation_id),
71
+ FOREIGN KEY (rows_shared_from_row_set_id, account_id, obligation_id)
72
+ REFERENCES adcp_reporting_row_sets (row_set_id, account_id, obligation_id)
73
+ );
74
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_sets_obligation
75
+ ON adcp_reporting_row_sets (obligation_id, recorded_at, row_set_id);
76
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_sets_state
77
+ ON adcp_reporting_row_sets (account_id, rows_state, recorded_at)
78
+ WHERE rows_state <> 'pruned';
79
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_sets_shared
80
+ ON adcp_reporting_row_sets (rows_shared_from_row_set_id)
81
+ WHERE rows_shared_from_row_set_id IS NOT NULL;
82
+
83
+ CREATE TABLE IF NOT EXISTS adcp_reporting_row_chunks (
84
+ row_set_id TEXT COLLATE "C" NOT NULL,
85
+ chunk_index INTEGER NOT NULL CHECK (chunk_index >= 0),
86
+ account_id TEXT COLLATE "C" NOT NULL,
87
+ obligation_id TEXT COLLATE "C" NOT NULL,
88
+ first_ordinal BIGINT NOT NULL CHECK (first_ordinal >= 0),
89
+ row_count INTEGER NOT NULL CHECK (row_count > 0),
90
+ byte_count BIGINT NOT NULL CHECK (byte_count > 0),
91
+ sha256 TEXT COLLATE "C" NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),
92
+ segments JSONB NOT NULL CHECK (jsonb_typeof(segments) = 'array'),
93
+ object_key TEXT COLLATE "C",
94
+ native_version TEXT,
95
+ compression TEXT NOT NULL DEFAULT 'none' CHECK (compression IN ('none', 'gzip')),
96
+ physical_sha256 TEXT COLLATE "C" CHECK (physical_sha256 IS NULL OR physical_sha256 ~ '^[0-9a-f]{64}$'),
97
+ physical_byte_count BIGINT CHECK (physical_byte_count IS NULL OR physical_byte_count > 0),
98
+ PRIMARY KEY (row_set_id, chunk_index),
99
+ FOREIGN KEY (row_set_id, account_id, obligation_id)
100
+ REFERENCES adcp_reporting_row_sets (row_set_id, account_id, obligation_id),
101
+ CHECK ((object_key IS NULL) = (native_version IS NULL)),
102
+ CHECK ((object_key IS NULL) = (physical_sha256 IS NULL)),
103
+ CHECK ((object_key IS NULL) = (physical_byte_count IS NULL))
104
+ );
105
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_chunks_object
106
+ ON adcp_reporting_row_chunks (object_key, native_version)
107
+ WHERE object_key IS NOT NULL;
108
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_chunks_body
109
+ ON adcp_reporting_row_chunks (account_id, obligation_id, sha256)
110
+ WHERE object_key IS NULL;
111
+
112
+ CREATE TABLE IF NOT EXISTS adcp_reporting_chunk_bodies (
113
+ account_id TEXT COLLATE "C" NOT NULL,
114
+ obligation_id TEXT COLLATE "C" NOT NULL,
115
+ sha256 TEXT COLLATE "C" NOT NULL,
116
+ body BYTEA NOT NULL,
117
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
118
+ PRIMARY KEY (account_id, obligation_id, sha256),
119
+ CHECK (sha256 = encode(sha256(body), 'hex'))
120
+ );
121
+
122
+ CREATE TABLE IF NOT EXISTS adcp_reporting_row_write_intents (
123
+ row_binding_id TEXT COLLATE "C" NOT NULL REFERENCES adcp_reporting_row_bindings(row_binding_id),
124
+ account_id TEXT COLLATE "C" NOT NULL,
125
+ row_set_id TEXT COLLATE "C" NOT NULL,
126
+ content_sha256 TEXT COLLATE "C" NOT NULL CHECK (content_sha256 ~ '^[0-9a-f]{64}$'),
127
+ state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'sweeping')),
128
+ owner_token TEXT COLLATE "C" NOT NULL,
129
+ created_objects JSONB NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(created_objects) = 'array'),
130
+ created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
131
+ expires_at TIMESTAMPTZ NOT NULL,
132
+ PRIMARY KEY (row_binding_id, row_set_id, content_sha256)
133
+ );
134
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_write_intents_expiry
135
+ ON adcp_reporting_row_write_intents (expires_at, account_id)
136
+ WHERE state = 'open';
137
+
138
+ CREATE OR REPLACE FUNCTION adcp_reporting_row_storage_immutable() RETURNS trigger
139
+ LANGUAGE plpgsql AS $immutable$
140
+ BEGIN
141
+ IF TG_TABLE_NAME = 'adcp_reporting_row_bindings' THEN
142
+ IF (NEW.row_binding_id, NEW.kind, NEW.provider, NEW.identity_config, NEW.identity_sha256, NEW.namespace_key)
143
+ IS DISTINCT FROM
144
+ (OLD.row_binding_id, OLD.kind, OLD.provider, OLD.identity_config, OLD.identity_sha256, OLD.namespace_key) THEN
145
+ RAISE EXCEPTION 'reporting row binding identity is immutable' USING ERRCODE = '23514';
146
+ END IF;
147
+ ELSIF TG_TABLE_NAME = 'adcp_reporting_row_sets' THEN
148
+ IF (NEW.row_set_id, NEW.row_set_kind, NEW.account_id, NEW.obligation_id, NEW.encoding, NEW.digest_profile,
149
+ NEW.content_sha256, NEW.canonical_byte_count, NEW.row_count, NEW.row_manifest_sha256, NEW.chunk_count,
150
+ NEW.rows_shared_from_row_set_id, NEW.recorded_at)
151
+ IS DISTINCT FROM
152
+ (OLD.row_set_id, OLD.row_set_kind, OLD.account_id, OLD.obligation_id, OLD.encoding, OLD.digest_profile,
153
+ OLD.content_sha256, OLD.canonical_byte_count, OLD.row_count, OLD.row_manifest_sha256, OLD.chunk_count,
154
+ OLD.rows_shared_from_row_set_id, OLD.recorded_at) THEN
155
+ RAISE EXCEPTION 'reporting row set content is immutable' USING ERRCODE = '23514';
156
+ END IF;
157
+ IF NEW.row_binding_id IS DISTINCT FROM OLD.row_binding_id
158
+ AND NEW.row_location_version <> OLD.row_location_version + 1 THEN
159
+ RAISE EXCEPTION 'reporting row set moves must advance row_location_version' USING ERRCODE = '23514';
160
+ END IF;
161
+ ELSIF TG_TABLE_NAME = 'adcp_reporting_row_chunks' THEN
162
+ IF (NEW.row_set_id, NEW.chunk_index, NEW.account_id, NEW.obligation_id, NEW.first_ordinal, NEW.row_count,
163
+ NEW.byte_count, NEW.sha256, NEW.segments)
164
+ IS DISTINCT FROM
165
+ (OLD.row_set_id, OLD.chunk_index, OLD.account_id, OLD.obligation_id, OLD.first_ordinal, OLD.row_count,
166
+ OLD.byte_count, OLD.sha256, OLD.segments) THEN
167
+ RAISE EXCEPTION 'reporting row chunk content is immutable' USING ERRCODE = '23514';
168
+ END IF;
169
+ ELSIF TG_TABLE_NAME = 'adcp_reporting_chunk_bodies' THEN
170
+ RAISE EXCEPTION 'reporting chunk bodies are immutable' USING ERRCODE = '23514';
171
+ END IF;
172
+ RETURN NEW;
173
+ END
174
+ $immutable$;
175
+
176
+ DO $triggers$
177
+ DECLARE
178
+ target TEXT;
179
+ BEGIN
180
+ FOREACH target IN ARRAY ARRAY[
181
+ 'adcp_reporting_row_bindings',
182
+ 'adcp_reporting_row_sets',
183
+ 'adcp_reporting_row_chunks',
184
+ 'adcp_reporting_chunk_bodies'
185
+ ] LOOP
186
+ IF NOT EXISTS (
187
+ SELECT 1 FROM pg_trigger
188
+ WHERE tgname = target || '_immutable'
189
+ AND tgrelid = to_regclass(quote_ident(current_schema()) || '.' || quote_ident(target))
190
+ ) THEN
191
+ EXECUTE format(
192
+ 'CREATE TRIGGER %I BEFORE UPDATE ON %I FOR EACH ROW EXECUTE FUNCTION adcp_reporting_row_storage_immutable()',
193
+ target || '_immutable',
194
+ target
195
+ );
196
+ END IF;
197
+ END LOOP;
198
+ END
199
+ $triggers$;
200
+ `.trim();
201
+ const REPORTING_ROW_STORAGE_TABLES = Object.freeze([
202
+ "adcp_persistence_installation",
203
+ "adcp_reporting_row_bindings",
204
+ "adcp_reporting_row_sets",
205
+ "adcp_reporting_row_chunks",
206
+ "adcp_reporting_chunk_bodies",
207
+ "adcp_reporting_row_write_intents"
208
+ ]);
209
+ async function probeReportingRowStorageSchemaV1(pool) {
210
+ const present = await pool.query(
211
+ `SELECT table_name AS name FROM information_schema.tables
212
+ WHERE table_schema = current_schema() AND table_name = ANY($1::text[])`,
213
+ [[...REPORTING_ROW_STORAGE_TABLES]]
214
+ );
215
+ const names = new Set(present.rows.map((row) => row.name));
216
+ const missing = REPORTING_ROW_STORAGE_TABLES.filter((name) => !names.has(name));
217
+ if (missing.length > 0) {
218
+ throw new Error(
219
+ `Reporting row storage requires REPORTING_ROW_STORAGE_MIGRATION; missing tables: ${missing.join(", ")}`
220
+ );
221
+ }
222
+ const installation = await pool.query(
223
+ "SELECT installation_id::text AS installation_id FROM adcp_persistence_installation WHERE singleton"
224
+ );
225
+ const installationId = installation.rows[0]?.installation_id;
226
+ if (!installationId) throw new Error("Reporting row storage installation identity is missing");
227
+ return { installationId };
228
+ }
229
+ // Annotate the CommonJS export names for ESM import in node:
230
+ 0 && (module.exports = {
231
+ REPORTING_ROW_STORAGE_MIGRATION,
232
+ REPORTING_ROW_STORAGE_TABLES,
233
+ probeReportingRowStorageSchemaV1
234
+ });
@@ -0,0 +1,208 @@
1
+ const REPORTING_ROW_STORAGE_MIGRATION = `
2
+ CREATE TABLE IF NOT EXISTS adcp_persistence_installation (
3
+ singleton BOOLEAN PRIMARY KEY DEFAULT TRUE CHECK (singleton),
4
+ installation_id UUID NOT NULL DEFAULT gen_random_uuid(),
5
+ created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
6
+ );
7
+ INSERT INTO adcp_persistence_installation (singleton) VALUES (TRUE) ON CONFLICT DO NOTHING;
8
+
9
+ CREATE TABLE IF NOT EXISTS adcp_reporting_row_bindings (
10
+ row_binding_id TEXT COLLATE "C" PRIMARY KEY CHECK (row_binding_id ~ '^[A-Za-z0-9_.:-]{1,128}$'),
11
+ kind TEXT NOT NULL CHECK (kind IN ('postgres', 'object')),
12
+ provider TEXT NOT NULL CHECK (provider ~ '^[a-z][a-z0-9_]{0,63}$'),
13
+ identity_config JSONB NOT NULL CHECK (jsonb_typeof(identity_config) = 'object'),
14
+ identity_sha256 TEXT COLLATE "C" NOT NULL CHECK (identity_sha256 ~ '^[0-9a-f]{64}$'),
15
+ operational_config JSONB NOT NULL DEFAULT '{}'::jsonb CHECK (jsonb_typeof(operational_config) = 'object'),
16
+ namespace_key TEXT COLLATE "C" NOT NULL CHECK (namespace_key ~ '^[0-9a-f]{64}$'),
17
+ state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active', 'read_only', 'retired')),
18
+ created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
19
+ retired_at TIMESTAMPTZ,
20
+ CHECK ((state = 'retired') = (retired_at IS NOT NULL))
21
+ );
22
+
23
+ CREATE TABLE IF NOT EXISTS adcp_reporting_row_sets (
24
+ row_set_id TEXT COLLATE "C" PRIMARY KEY,
25
+ row_set_kind TEXT NOT NULL CHECK (row_set_kind IN ('revision', 'adjustment')),
26
+ account_id TEXT COLLATE "C" NOT NULL,
27
+ obligation_id TEXT COLLATE "C" NOT NULL REFERENCES adcp_reporting_obligations(obligation_id),
28
+ encoding TEXT NOT NULL CHECK (encoding = 'adcp_canonical_jsonl_v1'),
29
+ digest_profile TEXT NOT NULL CHECK (digest_profile IN ('revision_envelope_v1', 'rows_v1')),
30
+ content_sha256 TEXT COLLATE "C" NOT NULL CHECK (content_sha256 ~ '^[0-9a-f]{64}$'),
31
+ canonical_byte_count BIGINT NOT NULL CHECK (canonical_byte_count > 0),
32
+ row_count BIGINT NOT NULL CHECK (row_count >= 0),
33
+ row_manifest_sha256 TEXT COLLATE "C" NOT NULL CHECK (row_manifest_sha256 ~ '^[0-9a-f]{64}$'),
34
+ chunk_count INTEGER NOT NULL CHECK (chunk_count >= 0),
35
+ rows_shared_from_row_set_id TEXT COLLATE "C",
36
+ row_binding_id TEXT COLLATE "C" NOT NULL REFERENCES adcp_reporting_row_bindings(row_binding_id),
37
+ row_location_version INTEGER NOT NULL DEFAULT 1 CHECK (row_location_version > 0),
38
+ rows_state TEXT NOT NULL DEFAULT 'live' CHECK (rows_state IN ('live', 'pruning', 'pruned', 'unavailable')),
39
+ rows_state_changed_at TIMESTAMPTZ,
40
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
41
+ CHECK ((row_count = 0) = (chunk_count = 0)),
42
+ CHECK (rows_shared_from_row_set_id IS NULL OR rows_shared_from_row_set_id <> row_set_id),
43
+ -- Tenant columns are the composite key children reference, so a chunk or a
44
+ -- shared row set can never name another account's or obligation's rows.
45
+ UNIQUE (row_set_id, account_id, obligation_id),
46
+ FOREIGN KEY (rows_shared_from_row_set_id, account_id, obligation_id)
47
+ REFERENCES adcp_reporting_row_sets (row_set_id, account_id, obligation_id)
48
+ );
49
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_sets_obligation
50
+ ON adcp_reporting_row_sets (obligation_id, recorded_at, row_set_id);
51
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_sets_state
52
+ ON adcp_reporting_row_sets (account_id, rows_state, recorded_at)
53
+ WHERE rows_state <> 'pruned';
54
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_sets_shared
55
+ ON adcp_reporting_row_sets (rows_shared_from_row_set_id)
56
+ WHERE rows_shared_from_row_set_id IS NOT NULL;
57
+
58
+ CREATE TABLE IF NOT EXISTS adcp_reporting_row_chunks (
59
+ row_set_id TEXT COLLATE "C" NOT NULL,
60
+ chunk_index INTEGER NOT NULL CHECK (chunk_index >= 0),
61
+ account_id TEXT COLLATE "C" NOT NULL,
62
+ obligation_id TEXT COLLATE "C" NOT NULL,
63
+ first_ordinal BIGINT NOT NULL CHECK (first_ordinal >= 0),
64
+ row_count INTEGER NOT NULL CHECK (row_count > 0),
65
+ byte_count BIGINT NOT NULL CHECK (byte_count > 0),
66
+ sha256 TEXT COLLATE "C" NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$'),
67
+ segments JSONB NOT NULL CHECK (jsonb_typeof(segments) = 'array'),
68
+ object_key TEXT COLLATE "C",
69
+ native_version TEXT,
70
+ compression TEXT NOT NULL DEFAULT 'none' CHECK (compression IN ('none', 'gzip')),
71
+ physical_sha256 TEXT COLLATE "C" CHECK (physical_sha256 IS NULL OR physical_sha256 ~ '^[0-9a-f]{64}$'),
72
+ physical_byte_count BIGINT CHECK (physical_byte_count IS NULL OR physical_byte_count > 0),
73
+ PRIMARY KEY (row_set_id, chunk_index),
74
+ FOREIGN KEY (row_set_id, account_id, obligation_id)
75
+ REFERENCES adcp_reporting_row_sets (row_set_id, account_id, obligation_id),
76
+ CHECK ((object_key IS NULL) = (native_version IS NULL)),
77
+ CHECK ((object_key IS NULL) = (physical_sha256 IS NULL)),
78
+ CHECK ((object_key IS NULL) = (physical_byte_count IS NULL))
79
+ );
80
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_chunks_object
81
+ ON adcp_reporting_row_chunks (object_key, native_version)
82
+ WHERE object_key IS NOT NULL;
83
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_chunks_body
84
+ ON adcp_reporting_row_chunks (account_id, obligation_id, sha256)
85
+ WHERE object_key IS NULL;
86
+
87
+ CREATE TABLE IF NOT EXISTS adcp_reporting_chunk_bodies (
88
+ account_id TEXT COLLATE "C" NOT NULL,
89
+ obligation_id TEXT COLLATE "C" NOT NULL,
90
+ sha256 TEXT COLLATE "C" NOT NULL,
91
+ body BYTEA NOT NULL,
92
+ recorded_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
93
+ PRIMARY KEY (account_id, obligation_id, sha256),
94
+ CHECK (sha256 = encode(sha256(body), 'hex'))
95
+ );
96
+
97
+ CREATE TABLE IF NOT EXISTS adcp_reporting_row_write_intents (
98
+ row_binding_id TEXT COLLATE "C" NOT NULL REFERENCES adcp_reporting_row_bindings(row_binding_id),
99
+ account_id TEXT COLLATE "C" NOT NULL,
100
+ row_set_id TEXT COLLATE "C" NOT NULL,
101
+ content_sha256 TEXT COLLATE "C" NOT NULL CHECK (content_sha256 ~ '^[0-9a-f]{64}$'),
102
+ state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'sweeping')),
103
+ owner_token TEXT COLLATE "C" NOT NULL,
104
+ created_objects JSONB NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(created_objects) = 'array'),
105
+ created_at TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp(),
106
+ expires_at TIMESTAMPTZ NOT NULL,
107
+ PRIMARY KEY (row_binding_id, row_set_id, content_sha256)
108
+ );
109
+ CREATE INDEX IF NOT EXISTS adcp_reporting_row_write_intents_expiry
110
+ ON adcp_reporting_row_write_intents (expires_at, account_id)
111
+ WHERE state = 'open';
112
+
113
+ CREATE OR REPLACE FUNCTION adcp_reporting_row_storage_immutable() RETURNS trigger
114
+ LANGUAGE plpgsql AS $immutable$
115
+ BEGIN
116
+ IF TG_TABLE_NAME = 'adcp_reporting_row_bindings' THEN
117
+ IF (NEW.row_binding_id, NEW.kind, NEW.provider, NEW.identity_config, NEW.identity_sha256, NEW.namespace_key)
118
+ IS DISTINCT FROM
119
+ (OLD.row_binding_id, OLD.kind, OLD.provider, OLD.identity_config, OLD.identity_sha256, OLD.namespace_key) THEN
120
+ RAISE EXCEPTION 'reporting row binding identity is immutable' USING ERRCODE = '23514';
121
+ END IF;
122
+ ELSIF TG_TABLE_NAME = 'adcp_reporting_row_sets' THEN
123
+ IF (NEW.row_set_id, NEW.row_set_kind, NEW.account_id, NEW.obligation_id, NEW.encoding, NEW.digest_profile,
124
+ NEW.content_sha256, NEW.canonical_byte_count, NEW.row_count, NEW.row_manifest_sha256, NEW.chunk_count,
125
+ NEW.rows_shared_from_row_set_id, NEW.recorded_at)
126
+ IS DISTINCT FROM
127
+ (OLD.row_set_id, OLD.row_set_kind, OLD.account_id, OLD.obligation_id, OLD.encoding, OLD.digest_profile,
128
+ OLD.content_sha256, OLD.canonical_byte_count, OLD.row_count, OLD.row_manifest_sha256, OLD.chunk_count,
129
+ OLD.rows_shared_from_row_set_id, OLD.recorded_at) THEN
130
+ RAISE EXCEPTION 'reporting row set content is immutable' USING ERRCODE = '23514';
131
+ END IF;
132
+ IF NEW.row_binding_id IS DISTINCT FROM OLD.row_binding_id
133
+ AND NEW.row_location_version <> OLD.row_location_version + 1 THEN
134
+ RAISE EXCEPTION 'reporting row set moves must advance row_location_version' USING ERRCODE = '23514';
135
+ END IF;
136
+ ELSIF TG_TABLE_NAME = 'adcp_reporting_row_chunks' THEN
137
+ IF (NEW.row_set_id, NEW.chunk_index, NEW.account_id, NEW.obligation_id, NEW.first_ordinal, NEW.row_count,
138
+ NEW.byte_count, NEW.sha256, NEW.segments)
139
+ IS DISTINCT FROM
140
+ (OLD.row_set_id, OLD.chunk_index, OLD.account_id, OLD.obligation_id, OLD.first_ordinal, OLD.row_count,
141
+ OLD.byte_count, OLD.sha256, OLD.segments) THEN
142
+ RAISE EXCEPTION 'reporting row chunk content is immutable' USING ERRCODE = '23514';
143
+ END IF;
144
+ ELSIF TG_TABLE_NAME = 'adcp_reporting_chunk_bodies' THEN
145
+ RAISE EXCEPTION 'reporting chunk bodies are immutable' USING ERRCODE = '23514';
146
+ END IF;
147
+ RETURN NEW;
148
+ END
149
+ $immutable$;
150
+
151
+ DO $triggers$
152
+ DECLARE
153
+ target TEXT;
154
+ BEGIN
155
+ FOREACH target IN ARRAY ARRAY[
156
+ 'adcp_reporting_row_bindings',
157
+ 'adcp_reporting_row_sets',
158
+ 'adcp_reporting_row_chunks',
159
+ 'adcp_reporting_chunk_bodies'
160
+ ] LOOP
161
+ IF NOT EXISTS (
162
+ SELECT 1 FROM pg_trigger
163
+ WHERE tgname = target || '_immutable'
164
+ AND tgrelid = to_regclass(quote_ident(current_schema()) || '.' || quote_ident(target))
165
+ ) THEN
166
+ EXECUTE format(
167
+ 'CREATE TRIGGER %I BEFORE UPDATE ON %I FOR EACH ROW EXECUTE FUNCTION adcp_reporting_row_storage_immutable()',
168
+ target || '_immutable',
169
+ target
170
+ );
171
+ END IF;
172
+ END LOOP;
173
+ END
174
+ $triggers$;
175
+ `.trim();
176
+ const REPORTING_ROW_STORAGE_TABLES = Object.freeze([
177
+ "adcp_persistence_installation",
178
+ "adcp_reporting_row_bindings",
179
+ "adcp_reporting_row_sets",
180
+ "adcp_reporting_row_chunks",
181
+ "adcp_reporting_chunk_bodies",
182
+ "adcp_reporting_row_write_intents"
183
+ ]);
184
+ async function probeReportingRowStorageSchemaV1(pool) {
185
+ const present = await pool.query(
186
+ `SELECT table_name AS name FROM information_schema.tables
187
+ WHERE table_schema = current_schema() AND table_name = ANY($1::text[])`,
188
+ [[...REPORTING_ROW_STORAGE_TABLES]]
189
+ );
190
+ const names = new Set(present.rows.map((row) => row.name));
191
+ const missing = REPORTING_ROW_STORAGE_TABLES.filter((name) => !names.has(name));
192
+ if (missing.length > 0) {
193
+ throw new Error(
194
+ `Reporting row storage requires REPORTING_ROW_STORAGE_MIGRATION; missing tables: ${missing.join(", ")}`
195
+ );
196
+ }
197
+ const installation = await pool.query(
198
+ "SELECT installation_id::text AS installation_id FROM adcp_persistence_installation WHERE singleton"
199
+ );
200
+ const installationId = installation.rows[0]?.installation_id;
201
+ if (!installationId) throw new Error("Reporting row storage installation identity is missing");
202
+ return { installationId };
203
+ }
204
+ export {
205
+ REPORTING_ROW_STORAGE_MIGRATION,
206
+ REPORTING_ROW_STORAGE_TABLES,
207
+ probeReportingRowStorageSchemaV1
208
+ };