@rebasepro/server-postgres 0.25.0 → 0.26.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 (144) hide show
  1. package/dist/{BranchService-iXXZeVYD.js → BranchService-CWBnDhcv.js} +88 -12
  2. package/dist/BranchService-CWBnDhcv.js.map +1 -0
  3. package/dist/PostgresBackendDriver.d.ts +13 -0
  4. package/dist/PostgresBootstrapper.d.ts +7 -0
  5. package/dist/auth/services.d.ts +8 -1
  6. package/dist/auth/verify-auth-tables.d.ts +21 -0
  7. package/dist/backup/backup-cli.d.ts +40 -3
  8. package/dist/backup/backup-service.d.ts +11 -0
  9. package/dist/{backup-service-Bb39plam.js → backup-cli-PoRG-zEq.js} +777 -10
  10. package/dist/backup-cli-PoRG-zEq.js.map +1 -0
  11. package/dist/{boot-ddl-s_3Hkz94.js → boot-ddl-EsgqDkx0.js} +3 -3
  12. package/dist/{boot-ddl-s_3Hkz94.js.map → boot-ddl-EsgqDkx0.js.map} +1 -1
  13. package/dist/branch-argv.d.ts +15 -0
  14. package/dist/branch-diff-CxE4VlxT.js +1396 -0
  15. package/dist/branch-diff-CxE4VlxT.js.map +1 -0
  16. package/dist/branch-diff-report-DpvVf7hR.js +48 -0
  17. package/dist/branch-diff-report-DpvVf7hR.js.map +1 -0
  18. package/dist/branching/branch-diff-report.d.ts +7 -0
  19. package/dist/branching/branch-diff.d.ts +115 -0
  20. package/dist/{cli-console-DQ_K-sdM.js → cli-console-f0k6_TsJ.js} +41 -9
  21. package/dist/cli-console-f0k6_TsJ.js.map +1 -0
  22. package/dist/cli-console.d.ts +17 -1
  23. package/dist/{cli-errors-DwQnNATs.js → cli-errors-DsYyNYzd.js} +90 -11
  24. package/dist/cli-errors-DsYyNYzd.js.map +1 -0
  25. package/dist/cli-errors.d.ts +14 -0
  26. package/dist/cli-flags.d.ts +51 -0
  27. package/dist/{cli-prompt-egtNKkZl.js → cli-prompt-DXG0yk-h.js} +39 -4
  28. package/dist/{cli-prompt-egtNKkZl.js.map → cli-prompt-DXG0yk-h.js.map} +1 -1
  29. package/dist/cli.js +121 -271
  30. package/dist/cli.js.map +1 -1
  31. package/dist/{collections-schema-version-T6cGvrDi.js → collections-schema-version-D7bDI0PJ.js} +13 -9
  32. package/dist/collections-schema-version-D7bDI0PJ.js.map +1 -0
  33. package/dist/{column-plan-helpers-D_V4oaZh.js → column-plan-helpers-IG6uFb1s.js} +2 -2
  34. package/dist/{column-plan-helpers-D_V4oaZh.js.map → column-plan-helpers-IG6uFb1s.js.map} +1 -1
  35. package/dist/{connection-C938nR3E.js → connection-czvI7TZL.js} +57 -10
  36. package/dist/connection-czvI7TZL.js.map +1 -0
  37. package/dist/{databasePoolManager-Yqdw14wY.js → databasePoolManager-3stMNFL-.js} +4 -6
  38. package/dist/databasePoolManager-3stMNFL-.js.map +1 -0
  39. package/dist/db-command-line.d.ts +14 -0
  40. package/dist/db-subcommands.d.ts +11 -0
  41. package/dist/{deferred-index-build-Cis_HoGn.js → deferred-index-build-CqaOv1Xx.js} +8 -8
  42. package/dist/{deferred-index-build-Cis_HoGn.js.map → deferred-index-build-CqaOv1Xx.js.map} +1 -1
  43. package/dist/{doctor-DvJH-IQG.js → doctor-DW_mlTzz.js} +186 -42
  44. package/dist/doctor-DW_mlTzz.js.map +1 -0
  45. package/dist/{ensure-collection-policies-CCqayoUz.js → ensure-collection-policies-DLWgabQ2.js} +13 -10
  46. package/dist/ensure-collection-policies-DLWgabQ2.js.map +1 -0
  47. package/dist/{ensure-collection-tables-DvvImXDe.js → ensure-collection-tables-CFIRq8ni.js} +9 -9
  48. package/dist/{ensure-collection-tables-DvvImXDe.js.map → ensure-collection-tables-CFIRq8ni.js.map} +1 -1
  49. package/dist/{ensure-tables-Dpgwx5sy.js → ensure-tables-4nA-tfwi.js} +5 -5
  50. package/dist/{ensure-tables-Dpgwx5sy.js.map → ensure-tables-4nA-tfwi.js.map} +1 -1
  51. package/dist/{enum-labels-kept-CUlUlRkO.js → enum-labels-kept-PcPW65xy.js} +3 -3
  52. package/dist/{enum-labels-kept-CUlUlRkO.js.map → enum-labels-kept-PcPW65xy.js.map} +1 -1
  53. package/dist/{generate-drizzle-schema-C3EAP9HE.js → generate-drizzle-schema-DC0BOUpn.js} +3 -3
  54. package/dist/{generate-drizzle-schema-C3EAP9HE.js.map → generate-drizzle-schema-DC0BOUpn.js.map} +1 -1
  55. package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js → generate-drizzle-schema-logic-CPzCRD-8.js} +4 -4
  56. package/dist/{generate-drizzle-schema-logic-DA3zM8zc.js.map → generate-drizzle-schema-logic-CPzCRD-8.js.map} +1 -1
  57. package/dist/{generate-files-ClmOUHXj.js → generate-files-BP-4W_gm.js} +6 -6
  58. package/dist/{generate-files-ClmOUHXj.js.map → generate-files-BP-4W_gm.js.map} +1 -1
  59. package/dist/{generate-next-step-DCeS4LrJ.js → generate-next-step-DgK5BlBn.js} +2 -2
  60. package/dist/{generate-next-step-DCeS4LrJ.js.map → generate-next-step-DgK5BlBn.js.map} +1 -1
  61. package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js → generate-postgres-ddl-logic-DPl1oH_z.js} +4 -4
  62. package/dist/{generate-postgres-ddl-logic-4eUP4t8w.js.map → generate-postgres-ddl-logic-DPl1oH_z.js.map} +1 -1
  63. package/dist/generated-schema-diff-eZCklRZQ.js +1025 -0
  64. package/dist/generated-schema-diff-eZCklRZQ.js.map +1 -0
  65. package/dist/{generated-schema-staleness-FlxM2i4i.js → generated-schema-staleness-BvNF3i-s.js} +2 -2
  66. package/dist/{generated-schema-staleness-FlxM2i4i.js.map → generated-schema-staleness-BvNF3i-s.js.map} +1 -1
  67. package/dist/history/HistoryService.d.ts +7 -0
  68. package/dist/index.d.ts +21 -0
  69. package/dist/index.es.js +288 -11
  70. package/dist/index.es.js.map +1 -1
  71. package/dist/{introspect-db-logic-CI5CU_n0.js → introspect-db-logic-DgQVzccW.js} +2 -2
  72. package/dist/{introspect-db-logic-CI5CU_n0.js.map → introspect-db-logic-DgQVzccW.js.map} +1 -1
  73. package/dist/{rls-bootstrap-sql-Bd-hJk08.js → migrate-baseline-B-2Ef4Mv.js} +718 -41
  74. package/dist/migrate-baseline-B-2Ef4Mv.js.map +1 -0
  75. package/dist/migrate-policy-stamps-BQtFHApx.js +89 -0
  76. package/dist/migrate-policy-stamps-BQtFHApx.js.map +1 -0
  77. package/dist/{open-rebase-BKA7Nyoe.js → open-rebase-C8jQ6s5a.js} +324 -1047
  78. package/dist/open-rebase-C8jQ6s5a.js.map +1 -0
  79. package/dist/open-rebase.d.ts +9 -1
  80. package/dist/{pg-error-utils-Bv5n2PLn.js → pg-error-utils-DhiUTlVx.js} +2 -2
  81. package/dist/{pg-error-utils-Bv5n2PLn.js.map → pg-error-utils-DhiUTlVx.js.map} +1 -1
  82. package/dist/{plan-schema-CXOucCw-.js → plan-schema-Czf3q4-Q.js} +4 -4
  83. package/dist/{plan-schema-CXOucCw-.js.map → plan-schema-Czf3q4-Q.js.map} +1 -1
  84. package/dist/{policy-drift-vm0BcvcE.js → policy-drift-X4A-xAzv.js} +3 -3
  85. package/dist/{policy-drift-vm0BcvcE.js.map → policy-drift-X4A-xAzv.js.map} +1 -1
  86. package/dist/pool-disconnects.d.ts +28 -0
  87. package/dist/{push-index-prebuild-DIequ4bV.js → push-index-prebuild-UTvB5pqH.js} +4 -4
  88. package/dist/{push-index-prebuild-DIequ4bV.js.map → push-index-prebuild-UTvB5pqH.js.map} +1 -1
  89. package/dist/push-kept-defaults-BGOlOqRQ.js +137 -0
  90. package/dist/push-kept-defaults-BGOlOqRQ.js.map +1 -0
  91. package/dist/{push-legacy-renames-BqJut4F3.js → push-legacy-renames-BwaKi8xF.js} +4 -4
  92. package/dist/{push-legacy-renames-BqJut4F3.js.map → push-legacy-renames-BwaKi8xF.js.map} +1 -1
  93. package/dist/{push-policy-plan-CvX8hiPb.js → push-policy-plan-DQ2tWceD.js} +5 -5
  94. package/dist/{push-policy-plan-CvX8hiPb.js.map → push-policy-plan-DQ2tWceD.js.map} +1 -1
  95. package/dist/{render-ddl-CQNwZdse.js → render-ddl-4v88y-Gv.js} +4 -4
  96. package/dist/{render-ddl-CQNwZdse.js.map → render-ddl-4v88y-Gv.js.map} +1 -1
  97. package/dist/{rls-enforcement-D7P4-ZYX.js → rls-enforcement-BOHuHVlc.js} +43 -4
  98. package/dist/rls-enforcement-BOHuHVlc.js.map +1 -0
  99. package/dist/schema/destructive-sql.d.ts +16 -0
  100. package/dist/schema/doctor-cli.js +5 -5
  101. package/dist/schema/doctor.d.ts +20 -2
  102. package/dist/schema/ensure-collection-policies.d.ts +4 -1
  103. package/dist/schema/generate-drizzle-schema.js +1 -1
  104. package/dist/schema/generate-postgres-ddl.js +2 -2
  105. package/dist/schema/generated-schema-diff.d.ts +21 -0
  106. package/dist/schema/introspect-db.js +5 -6
  107. package/dist/schema/introspect-db.js.map +1 -1
  108. package/dist/schema/migrate-policy-stamps.d.ts +23 -0
  109. package/dist/schema/migration-freshness.d.ts +69 -0
  110. package/dist/schema/push-kept-defaults.d.ts +57 -0
  111. package/dist/schema/push-policy-plan.d.ts +5 -0
  112. package/dist/schema/rls-bootstrap-sql.d.ts +47 -10
  113. package/dist/security/rls-enforcement.d.ts +18 -0
  114. package/dist/services/BranchService.d.ts +20 -2
  115. package/dist/services/FetchService.d.ts +12 -0
  116. package/dist/services/channel-presence.d.ts +3 -1
  117. package/dist/services/nested-path.d.ts +16 -0
  118. package/dist/services/pg-notify-listener.d.ts +2 -0
  119. package/dist/services/realtimeService.d.ts +18 -1
  120. package/dist/{source-Bv5KPeIx.js → source-fZb92pN8.js} +2 -2
  121. package/dist/{source-Bv5KPeIx.js.map → source-fZb92pN8.js.map} +1 -1
  122. package/dist/{vector-index-BFhyEaiz.js → vector-index-DsQ3V2d2.js} +2 -2
  123. package/dist/{vector-index-BFhyEaiz.js.map → vector-index-DsQ3V2d2.js.map} +1 -1
  124. package/dist/{websocket-MC0zTBar.js → websocket-CKYc3V4N.js} +56 -14
  125. package/dist/websocket-CKYc3V4N.js.map +1 -0
  126. package/package.json +7 -7
  127. package/dist/BranchService-iXXZeVYD.js.map +0 -1
  128. package/dist/backup-cli-B2CAn9fR.js +0 -647
  129. package/dist/backup-cli-B2CAn9fR.js.map +0 -1
  130. package/dist/backup-service-Bb39plam.js.map +0 -1
  131. package/dist/cli-console-DQ_K-sdM.js.map +0 -1
  132. package/dist/cli-errors-DwQnNATs.js.map +0 -1
  133. package/dist/cli-flags-D-b0qL9T.js +0 -206
  134. package/dist/cli-flags-D-b0qL9T.js.map +0 -1
  135. package/dist/collections-schema-version-T6cGvrDi.js.map +0 -1
  136. package/dist/connection-C938nR3E.js.map +0 -1
  137. package/dist/databasePoolManager-Yqdw14wY.js.map +0 -1
  138. package/dist/doctor-DvJH-IQG.js.map +0 -1
  139. package/dist/ensure-collection-policies-CCqayoUz.js.map +0 -1
  140. package/dist/open-rebase-BKA7Nyoe.js.map +0 -1
  141. package/dist/rls-bootstrap-sql-Bd-hJk08.js.map +0 -1
  142. package/dist/rls-enforcement-D7P4-ZYX.js.map +0 -1
  143. package/dist/rolldown-runtime-V7rFbMRe.js +0 -39
  144. package/dist/websocket-MC0zTBar.js.map +0 -1
@@ -0,0 +1,1396 @@
1
+ import { createRequire as __createRequire } from "module";
2
+ __createRequire(import.meta.url);
3
+ import { i as __exportAll } from "./cli-prompt-DXG0yk-h.js";
4
+ import { ApiError } from "@rebasepro/server";
5
+ import { isBranchAdmin, isSQLAdmin } from "@rebasepro/types";
6
+ import { isRebaseInternalTable } from "@rebasepro/common";
7
+ import { createHash } from "node:crypto";
8
+ //#region src/branching/branch-diff.ts
9
+ /**
10
+ * What a database branch changed, and applying it to the database it was made
11
+ * from.
12
+ *
13
+ * A branch is a `CREATE DATABASE … TEMPLATE` copy (see `BranchService`): a
14
+ * database of its own, on the same server. Two databases cannot be joined in
15
+ * one query, so each side is read on its own through `SQLAdmin.executeSql`
16
+ * with `database`, and compared here.
17
+ *
18
+ * ## Which side changed a row
19
+ *
20
+ * Comparing the two databases says which rows differ, not who changed them: a
21
+ * row the target inserted after the branch was made looks exactly like a row
22
+ * the branch deleted. A merge that took every difference as the branch's would
23
+ * delete everything the target wrote meanwhile.
24
+ *
25
+ * Transaction ids are the server's, not a database's, and the template copy
26
+ * keeps every row's `xmin`. So a row written after the branch was made — on
27
+ * either side — has an `xmin` younger than the transaction that created the
28
+ * branch database. That is the fork point: the `xmin` of the new database's
29
+ * `pg_database` row, read when the branch is made and kept in its record
30
+ * (`BranchInfo.forkXid`), because a `GRANT`, an `ALTER DATABASE` or a forced
31
+ * copy from the branch rewrites that row later. With it each difference has an
32
+ * author:
33
+ *
34
+ * | branch row | target row | the change |
35
+ * |---------------------|---------------------|--------------------------------|
36
+ * | younger | absent | the branch inserted it |
37
+ * | absent | older | the branch deleted it |
38
+ * | younger | older | the branch updated it |
39
+ * | older or absent | younger or absent | the target changed it: kept |
40
+ * | younger | younger | both changed it: a conflict |
41
+ *
42
+ * Two cases cannot be told apart and are read the way that loses nothing: a
43
+ * row the branch deleted and the target then changed is the target's change,
44
+ * and kept; a row the target deleted and the branch then changed is the
45
+ * branch's insert. An update that wrote the same values still counts as a
46
+ * write, so it can make a conflict of a row only one side meant to change.
47
+ *
48
+ * A delete reaches further than its row. A row the branch deleted can be
49
+ * referenced, through a foreign key, by a row the target wrote since — a post
50
+ * by an author the branch removed. Deleting it on the target would cascade
51
+ * into that post, null its column, or fail outright, so it is a conflict, not
52
+ * a delete: `onConflict: "target"` keeps the row, `"branch"` deletes it and
53
+ * says what the foreign key reached (`referencedBy` on the diff).
54
+ *
55
+ * A branch recorded before the fork point was kept falls back to the
56
+ * `pg_database` row as it is now. When the fork point cannot be read — that
57
+ * row was frozen by a vacuum — every difference is counted as the branch's and
58
+ * the diff says so (`forkPointKnown: false`).
59
+ *
60
+ * ## What a merge does
61
+ *
62
+ * See `BranchMergeOptions` in `@rebasepro/types`. In short: the branch's
63
+ * inserts, updates and deletes, in one statement on the target, all or none;
64
+ * the target's own changes kept; nothing at all when either side moved since
65
+ * the reviewed diff. Schema and policies are never merged.
66
+ */
67
+ var branch_diff_exports = /* @__PURE__ */ __exportAll({
68
+ DEFAULT_BRANCH_DIFF_MAX_ROWS: () => DEFAULT_BRANCH_DIFF_MAX_ROWS,
69
+ DEFAULT_BRANCH_DIFF_SAMPLE_LIMIT: () => 100,
70
+ MAX_BRANCH_DIFF_SAMPLE_LIMIT: () => MAX_BRANCH_DIFF_SAMPLE_LIMIT,
71
+ REDACTED: () => REDACTED,
72
+ classifyRows: () => classifyRows,
73
+ columnDefinitionSql: () => columnDefinitionSql,
74
+ diffCatalogs: () => diffCatalogs,
75
+ diffDatabaseBranch: () => diffDatabaseBranch,
76
+ diffSchemaObjects: () => diffSchemaObjects,
77
+ mergeDatabaseBranch: () => mergeDatabaseBranch,
78
+ pickSamples: () => pickSamples,
79
+ policySql: () => policySql,
80
+ rewrittenSides: () => rewrittenSides,
81
+ tableSql: () => tableSql
82
+ });
83
+ /** Rows shown per table when the caller does not say. */
84
+ var DEFAULT_BRANCH_DIFF_SAMPLE_LIMIT = 100;
85
+ /** The most rows a diff samples per table, whatever the caller asks for. */
86
+ var MAX_BRANCH_DIFF_SAMPLE_LIMIT = 1e3;
87
+ /** What a sample shows in place of a secret's value. */
88
+ var REDACTED = "[redacted]";
89
+ /**
90
+ * Columns whose values a sample never shows: the auth users table's password
91
+ * hash and mailed tokens, and anything named like a secret, a token or a hash
92
+ * in any table — a diff is read by whoever reviews a branch, and these are
93
+ * credentials, not data to review. Matched by name, so a users table in
94
+ * another schema, or an app's own `api_secret`, is covered too.
95
+ */
96
+ var SECRET_COLUMN = /password|secret|(^|_)token$|_token_hash$|(^|_)hash$/i;
97
+ /** `row` with every secret column's value replaced by {@link REDACTED}. */
98
+ function redacted(row) {
99
+ if (!row) return row;
100
+ const shown = {};
101
+ for (const [column, value] of Object.entries(row)) shown[column] = value !== null && SECRET_COLUMN.test(column) ? REDACTED : value;
102
+ return shown;
103
+ }
104
+ /** Rows a table may hold, per side, to be matched row by row, when the caller does not say. */
105
+ var DEFAULT_BRANCH_DIFF_MAX_ROWS = 1e5;
106
+ /** The age Postgres gives a frozen or permanent transaction id. */
107
+ var FROZEN_AGE = 2147483647;
108
+ /**
109
+ * Every refusal here is an `ApiError` the caller can act on — the status a
110
+ * route answers it with, and the tables it is about in `details.tables` — so
111
+ * none logs as a warning. Thrown with its code and status written out at the
112
+ * call, where the error-code reference's scan reads them.
113
+ */
114
+ var EXPECTED = true;
115
+ function text(row, key) {
116
+ const value = row[key];
117
+ if (typeof value === "string") return value;
118
+ return value === null || value === void 0 ? "" : String(value);
119
+ }
120
+ function nullableText(row, key) {
121
+ const value = row[key];
122
+ if (value === null || value === void 0) return null;
123
+ return typeof value === "string" ? value : String(value);
124
+ }
125
+ function flag(row, key) {
126
+ const value = row[key];
127
+ return value === true || value === "t" || value === "true";
128
+ }
129
+ function stringList(json) {
130
+ const parsed = JSON.parse(json || "[]");
131
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
132
+ }
133
+ /** A row as `json_object_agg` over `json_each_text` writes it: every value text or null. */
134
+ function textRecord(json) {
135
+ const parsed = JSON.parse(json);
136
+ const record = {};
137
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return record;
138
+ for (const [key, value] of Object.entries(parsed)) record[key] = typeof value === "string" ? value : null;
139
+ return record;
140
+ }
141
+ var md5 = (value) => createHash("md5").update(value, "utf8").digest("hex");
142
+ function ident(name) {
143
+ return `"${name.replace(/"/g, "\"\"")}"`;
144
+ }
145
+ function qualified(schema, table) {
146
+ return `${ident(schema)}.${ident(table)}`;
147
+ }
148
+ /**
149
+ * The schemas a query reads, given `$1`: the ones asked for, or every schema
150
+ * but the system's.
151
+ */
152
+ function schemaFilter(column) {
153
+ return `(CASE WHEN $1::text[] IS NULL
154
+ THEN ${column} NOT LIKE 'pg\\_%' AND ${column} <> 'information_schema'
155
+ ELSE ${column} = ANY($1::text[]) END)`;
156
+ }
157
+ /** The primary key of `t` as a JSON array: unique, and lossless for any type. */
158
+ function keyExpression(primaryKey) {
159
+ return `json_build_array(${primaryKey.map((column) => `t.${ident(column)}`).join(", ")})::text`;
160
+ }
161
+ /**
162
+ * `t.<column>` as text that reads the same in any session.
163
+ *
164
+ * A row's text output follows the session's settings, and a database-level
165
+ * `ALTER DATABASE … SET timezone` (or `datestyle`) is not copied to a branch
166
+ * made from it — so the same `timestamptz` wrote different text on the two
167
+ * sides, and every untouched row read as changed. Times, timestamps and dates
168
+ * are written out with `to_char` in UTC instead; infinities keep their own
169
+ * words, which no setting changes.
170
+ */
171
+ function settledValue(column) {
172
+ const value = `t.${ident(column.name)}`;
173
+ const finite = (expression, format) => `CASE WHEN isfinite(${value}) THEN to_char(${expression}, '${format}') ELSE ${value}::text END`;
174
+ if (column.type === "timestamp with time zone" || column.type.startsWith("timestamp(") && column.type.endsWith("with time zone")) return finite(`${value} AT TIME ZONE 'UTC'`, "YYYY-MM-DD\"T\"HH24:MI:SS.US");
175
+ if (column.type === "timestamp without time zone" || column.type.startsWith("timestamp(")) return finite(value, "YYYY-MM-DD\"T\"HH24:MI:SS.US");
176
+ if (column.type === "date") return finite(value, "YYYY-MM-DD");
177
+ return value;
178
+ }
179
+ /**
180
+ * A digest of `t`'s values in `columns`. A row constructor rather than a
181
+ * function call, which would stop at 100 columns. The same columns, of the
182
+ * same types, write the same text on both sides, whatever either session's
183
+ * timezone or date style.
184
+ */
185
+ function hashExpression(columns) {
186
+ return `md5(ROW(${columns.map(settledValue).join(", ")})::text)`;
187
+ }
188
+ var tableKey = (schema, table) => JSON.stringify([schema, table]);
189
+ var TABLE_FILTER = `c.relkind IN ('r', 'p') AND NOT c.relispartition
190
+ AND NOT EXISTS (SELECT 1 FROM pg_depend dep
191
+ WHERE dep.classid = 'pg_class'::regclass AND dep.objid = c.oid AND dep.deptype = 'e')`;
192
+ var TABLES_SQL = `
193
+ SELECT n.nspname AS schema_name, c.relname AS table_name,
194
+ c.relrowsecurity AS rls, c.relforcerowsecurity AS force_rls
195
+ FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
196
+ WHERE ${TABLE_FILTER} AND ${schemaFilter("n.nspname")}`;
197
+ var COLUMNS_SQL = `
198
+ SELECT n.nspname AS schema_name, c.relname AS table_name, a.attname AS column_name,
199
+ format_type(a.atttypid, a.atttypmod) AS data_type, a.attnotnull AS not_null,
200
+ pg_get_expr(ad.adbin, ad.adrelid) AS default_expr,
201
+ a.attgenerated::text AS generated, a.attidentity::text AS identity,
202
+ pg_get_serial_sequence(format('%I.%I', n.nspname, c.relname), a.attname) AS sequence_name
203
+ FROM pg_attribute a
204
+ JOIN pg_class c ON c.oid = a.attrelid
205
+ JOIN pg_namespace n ON n.oid = c.relnamespace
206
+ LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
207
+ WHERE a.attnum > 0 AND NOT a.attisdropped AND ${TABLE_FILTER} AND ${schemaFilter("n.nspname")}
208
+ ORDER BY n.nspname, c.relname, a.attnum`;
209
+ var PRIMARY_KEYS_SQL = `
210
+ SELECT n.nspname AS schema_name, c.relname AS table_name,
211
+ json_agg(a.attname::text ORDER BY k.ord)::text AS columns
212
+ FROM pg_index i
213
+ JOIN pg_class c ON c.oid = i.indrelid
214
+ JOIN pg_namespace n ON n.oid = c.relnamespace
215
+ CROSS JOIN LATERAL unnest(i.indkey::int2[]) WITH ORDINALITY AS k(attnum, ord)
216
+ JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum = k.attnum
217
+ WHERE i.indisprimary AND ${schemaFilter("n.nspname")}
218
+ GROUP BY n.nspname, c.relname`;
219
+ var POLICIES_SQL = `
220
+ SELECT schemaname::text AS schema_name, tablename::text AS table_name, policyname::text AS policy_name,
221
+ permissive, array_to_json(roles)::text AS roles, cmd, qual, with_check
222
+ FROM pg_policies
223
+ WHERE ${schemaFilter("schemaname::text")}
224
+ ORDER BY schemaname, tablename, policyname`;
225
+ var CONSTRAINTS_SQL = `
226
+ SELECT n.nspname AS schema_name, c.relname AS table_name, con.conname AS object_name,
227
+ pg_get_constraintdef(con.oid) AS definition
228
+ FROM pg_constraint con
229
+ JOIN pg_class c ON c.oid = con.conrelid
230
+ JOIN pg_namespace n ON n.oid = c.relnamespace
231
+ WHERE con.contype IN ('u', 'f', 'c', 'x') AND ${TABLE_FILTER} AND ${schemaFilter("n.nspname")}
232
+ ORDER BY n.nspname, c.relname, con.conname`;
233
+ var INDEXES_SQL = `
234
+ SELECT n.nspname AS schema_name, c.relname AS table_name, i.relname AS object_name,
235
+ pg_get_indexdef(i.oid) AS definition
236
+ FROM pg_index x
237
+ JOIN pg_class i ON i.oid = x.indexrelid
238
+ JOIN pg_class c ON c.oid = x.indrelid
239
+ JOIN pg_namespace n ON n.oid = c.relnamespace
240
+ WHERE NOT EXISTS (SELECT 1 FROM pg_constraint con WHERE con.conindid = x.indexrelid AND con.contype IN ('p', 'u', 'x'))
241
+ AND ${TABLE_FILTER} AND ${schemaFilter("n.nspname")}
242
+ ORDER BY n.nspname, c.relname, i.relname`;
243
+ var TRIGGERS_SQL = `
244
+ SELECT n.nspname AS schema_name, c.relname AS table_name, t.tgname AS object_name,
245
+ pg_get_triggerdef(t.oid) AS definition
246
+ FROM pg_trigger t
247
+ JOIN pg_class c ON c.oid = t.tgrelid
248
+ JOIN pg_namespace n ON n.oid = c.relnamespace
249
+ WHERE NOT t.tgisinternal AND ${TABLE_FILTER} AND ${schemaFilter("n.nspname")}
250
+ ORDER BY n.nspname, c.relname, t.tgname`;
251
+ /** Not an extension's: `CREATE EXTENSION` owns those, not a migration. */
252
+ var notFromExtension = (catalog, oid) => `NOT EXISTS (SELECT 1 FROM pg_depend dep
253
+ WHERE dep.classid = '${catalog}'::regclass AND dep.objid = ${oid} AND dep.deptype = 'e')`;
254
+ var ENUMS_SQL = `
255
+ SELECT n.nspname AS schema_name, t.typname AS object_name,
256
+ json_agg(e.enumlabel::text ORDER BY e.enumsortorder)::text AS labels
257
+ FROM pg_type t
258
+ JOIN pg_namespace n ON n.oid = t.typnamespace
259
+ JOIN pg_enum e ON e.enumtypid = t.oid
260
+ WHERE ${schemaFilter("n.nspname")} AND ${notFromExtension("pg_type", "t.oid")}
261
+ GROUP BY n.nspname, t.typname`;
262
+ var VIEWS_SQL = `
263
+ SELECT n.nspname AS schema_name, c.relname AS object_name, c.relkind::text AS kind,
264
+ pg_get_viewdef(c.oid) AS definition
265
+ FROM pg_class c
266
+ JOIN pg_namespace n ON n.oid = c.relnamespace
267
+ WHERE c.relkind IN ('v', 'm') AND ${schemaFilter("n.nspname")} AND ${notFromExtension("pg_class", "c.oid")}`;
268
+ var FUNCTIONS_SQL = `
269
+ SELECT n.nspname AS schema_name,
270
+ p.proname || '(' || pg_get_function_identity_arguments(p.oid) || ')' AS object_name,
271
+ pg_get_functiondef(p.oid) AS definition
272
+ FROM pg_proc p
273
+ JOIN pg_namespace n ON n.oid = p.pronamespace
274
+ WHERE p.prokind IN ('f', 'p') AND ${schemaFilter("n.nspname")} AND ${notFromExtension("pg_proc", "p.oid")}`;
275
+ var literal = (value) => `'${value.replace(/'/g, "''")}'`;
276
+ /** The enum types, views and functions of the schemas a diff reads. */
277
+ async function readSchemaObjects(sql, database, schemas) {
278
+ const params = [schemas];
279
+ const [enums, views, functions] = await Promise.all([
280
+ sql.executeSql(ENUMS_SQL, {
281
+ database,
282
+ params
283
+ }),
284
+ sql.executeSql(VIEWS_SQL, {
285
+ database,
286
+ params
287
+ }),
288
+ sql.executeSql(FUNCTIONS_SQL, {
289
+ database,
290
+ params
291
+ })
292
+ ]);
293
+ const objects = /* @__PURE__ */ new Map();
294
+ const add = (facts) => objects.set(JSON.stringify([
295
+ facts.object,
296
+ facts.schema,
297
+ facts.name
298
+ ]), facts);
299
+ for (const row of enums) {
300
+ const schema = text(row, "schema_name");
301
+ const name = text(row, "object_name");
302
+ add({
303
+ object: "enum",
304
+ schema,
305
+ name,
306
+ definition: `CREATE TYPE ${qualified(schema, name)} AS ENUM (${stringList(text(row, "labels")).map(literal).join(", ")});`
307
+ });
308
+ }
309
+ for (const row of views) {
310
+ const schema = text(row, "schema_name");
311
+ const name = text(row, "object_name");
312
+ add({
313
+ object: "view",
314
+ schema,
315
+ name,
316
+ definition: `CREATE ${text(row, "kind") === "m" ? "MATERIALIZED VIEW" : "VIEW"} ${qualified(schema, name)} AS\n${text(row, "definition").trim()}`
317
+ });
318
+ }
319
+ for (const row of functions) add({
320
+ object: "function",
321
+ schema: text(row, "schema_name"),
322
+ name: text(row, "object_name"),
323
+ definition: text(row, "definition").trim()
324
+ });
325
+ return objects;
326
+ }
327
+ async function readCatalog(sql, database, schemas) {
328
+ const params = [schemas];
329
+ const [tables, columns, primaryKeys, policies, constraints, indexes, triggers] = await Promise.all([
330
+ sql.executeSql(TABLES_SQL, {
331
+ database,
332
+ params
333
+ }),
334
+ sql.executeSql(COLUMNS_SQL, {
335
+ database,
336
+ params
337
+ }),
338
+ sql.executeSql(PRIMARY_KEYS_SQL, {
339
+ database,
340
+ params
341
+ }),
342
+ sql.executeSql(POLICIES_SQL, {
343
+ database,
344
+ params
345
+ }),
346
+ sql.executeSql(CONSTRAINTS_SQL, {
347
+ database,
348
+ params
349
+ }),
350
+ sql.executeSql(INDEXES_SQL, {
351
+ database,
352
+ params
353
+ }),
354
+ sql.executeSql(TRIGGERS_SQL, {
355
+ database,
356
+ params
357
+ })
358
+ ]);
359
+ const catalog = /* @__PURE__ */ new Map();
360
+ for (const row of tables) {
361
+ const schema = text(row, "schema_name");
362
+ const table = text(row, "table_name");
363
+ if (isRebaseInternalTable(table, schema)) continue;
364
+ catalog.set(tableKey(schema, table), {
365
+ schema,
366
+ table,
367
+ rls: flag(row, "rls"),
368
+ forceRls: flag(row, "force_rls"),
369
+ columns: [],
370
+ primaryKey: [],
371
+ policies: [],
372
+ constraints: [],
373
+ indexes: [],
374
+ triggers: []
375
+ });
376
+ }
377
+ for (const row of columns) catalog.get(tableKey(text(row, "schema_name"), text(row, "table_name")))?.columns.push({
378
+ name: text(row, "column_name"),
379
+ type: text(row, "data_type"),
380
+ notNull: flag(row, "not_null"),
381
+ defaultExpression: nullableText(row, "default_expr"),
382
+ generated: text(row, "generated"),
383
+ identity: text(row, "identity"),
384
+ sequence: nullableText(row, "sequence_name")
385
+ });
386
+ for (const row of primaryKeys) {
387
+ const facts = catalog.get(tableKey(text(row, "schema_name"), text(row, "table_name")));
388
+ if (facts) facts.primaryKey = stringList(text(row, "columns"));
389
+ }
390
+ for (const row of policies) catalog.get(tableKey(text(row, "schema_name"), text(row, "table_name")))?.policies.push({
391
+ name: text(row, "policy_name"),
392
+ permissive: text(row, "permissive"),
393
+ roles: stringList(text(row, "roles")),
394
+ command: text(row, "cmd"),
395
+ using: nullableText(row, "qual"),
396
+ withCheck: nullableText(row, "with_check")
397
+ });
398
+ const named = (rows, list) => {
399
+ for (const row of rows) {
400
+ const facts = catalog.get(tableKey(text(row, "schema_name"), text(row, "table_name")));
401
+ if (facts) list(facts)?.push({
402
+ name: text(row, "object_name"),
403
+ definition: text(row, "definition")
404
+ });
405
+ }
406
+ };
407
+ named(constraints, (facts) => facts.constraints);
408
+ named(indexes, (facts) => facts.indexes);
409
+ named(triggers, (facts) => facts.triggers);
410
+ return catalog;
411
+ }
412
+ /** A column as it would be declared in `CREATE TABLE`. */
413
+ function columnDefinitionSql(column) {
414
+ const parts = [ident(column.name), column.type];
415
+ if (column.generated === "s") parts.push(`GENERATED ALWAYS AS (${column.defaultExpression ?? ""}) STORED`);
416
+ else if (column.identity === "a" || column.identity === "d") parts.push(`GENERATED ${column.identity === "a" ? "ALWAYS" : "BY DEFAULT"} AS IDENTITY`);
417
+ if (column.notNull) parts.push("NOT NULL");
418
+ if (column.generated !== "s" && column.defaultExpression !== null) parts.push(`DEFAULT ${column.defaultExpression}`);
419
+ return parts.join(" ");
420
+ }
421
+ function rlsSql(facts) {
422
+ const table = qualified(facts.schema, facts.table);
423
+ return [`ALTER TABLE ${table} ${facts.rls ? "ENABLE" : "DISABLE"} ROW LEVEL SECURITY;`, `ALTER TABLE ${table} ${facts.forceRls ? "FORCE" : "NO FORCE"} ROW LEVEL SECURITY;`].join("\n");
424
+ }
425
+ function tableSql(facts) {
426
+ const lines = facts.columns.map((column) => ` ${columnDefinitionSql(column)}`);
427
+ if (facts.primaryKey.length > 0) lines.push(` PRIMARY KEY (${facts.primaryKey.map(ident).join(", ")})`);
428
+ const create = `CREATE TABLE ${qualified(facts.schema, facts.table)} (\n${lines.join(",\n")}\n);`;
429
+ return facts.rls || facts.forceRls ? `${create}\n${rlsSql(facts)}` : create;
430
+ }
431
+ function policySql(schema, table, policy) {
432
+ const roles = policy.roles.map((role) => role === "public" ? "public" : ident(role)).join(", ");
433
+ const lines = [
434
+ `CREATE POLICY ${ident(policy.name)} ON ${qualified(schema, table)}`,
435
+ ` AS ${policy.permissive}`,
436
+ ` FOR ${policy.command}`,
437
+ ` TO ${roles || "public"}`
438
+ ];
439
+ if (policy.using !== null) lines.push(` USING (${policy.using})`);
440
+ if (policy.withCheck !== null) lines.push(` WITH CHECK (${policy.withCheck})`);
441
+ return `${lines.join("\n")};`;
442
+ }
443
+ function primaryKeySql(facts) {
444
+ return facts.primaryKey.length === 0 ? `-- ${qualified(facts.schema, facts.table)} has no primary key` : `ALTER TABLE ${qualified(facts.schema, facts.table)} ADD PRIMARY KEY (${facts.primaryKey.map(ident).join(", ")});`;
445
+ }
446
+ /** A table's constraint, index or trigger, as the SQL that would create it. */
447
+ function tableObjectSql(object, facts, item) {
448
+ return object === "constraint" ? `ALTER TABLE ${qualified(facts.schema, facts.table)} ADD CONSTRAINT ${ident(item.name)} ${item.definition};` : `${item.definition};`;
449
+ }
450
+ var TABLE_OBJECTS = [
451
+ ["constraint", (facts) => facts.constraints ?? []],
452
+ ["index", (facts) => facts.indexes ?? []],
453
+ ["trigger", (facts) => facts.triggers ?? []]
454
+ ];
455
+ var sameList = (a, b) => a.length === b.length && a.every((item, index) => item === b[index]);
456
+ /**
457
+ * The schema changes from `target` to `branch`: tables, columns, primary keys,
458
+ * constraints, indexes, triggers, row-level security and policies, ordered by
459
+ * table.
460
+ */
461
+ function diffCatalogs(target, branch) {
462
+ const changes = [];
463
+ const keys = [.../* @__PURE__ */ new Set([...target.keys(), ...branch.keys()])].sort();
464
+ for (const key of keys) {
465
+ const before = target.get(key);
466
+ const after = branch.get(key);
467
+ const facts = after ?? before;
468
+ if (!facts) continue;
469
+ const { schema, table } = facts;
470
+ if (!before && after) changes.push({
471
+ object: "table",
472
+ change: "added",
473
+ schema,
474
+ table,
475
+ after: tableSql(after)
476
+ });
477
+ else if (before && !after) changes.push({
478
+ object: "table",
479
+ change: "dropped",
480
+ schema,
481
+ table,
482
+ before: tableSql(before)
483
+ });
484
+ else if (before && after) {
485
+ if (!sameList(before.primaryKey, after.primaryKey)) changes.push({
486
+ object: "table",
487
+ change: "altered",
488
+ schema,
489
+ table,
490
+ before: primaryKeySql(before),
491
+ after: primaryKeySql(after)
492
+ });
493
+ const columnsBefore = new Map(before.columns.map((column) => [column.name, column]));
494
+ const columnsAfter = new Map(after.columns.map((column) => [column.name, column]));
495
+ for (const column of after.columns) {
496
+ const old = columnsBefore.get(column.name);
497
+ const definition = columnDefinitionSql(column);
498
+ if (!old) changes.push({
499
+ object: "column",
500
+ change: "added",
501
+ schema,
502
+ table,
503
+ name: column.name,
504
+ after: definition
505
+ });
506
+ else if (columnDefinitionSql(old) !== definition) changes.push({
507
+ object: "column",
508
+ change: "altered",
509
+ schema,
510
+ table,
511
+ name: column.name,
512
+ before: columnDefinitionSql(old),
513
+ after: definition
514
+ });
515
+ }
516
+ for (const column of before.columns) if (!columnsAfter.has(column.name)) changes.push({
517
+ object: "column",
518
+ change: "dropped",
519
+ schema,
520
+ table,
521
+ name: column.name,
522
+ before: columnDefinitionSql(column)
523
+ });
524
+ if (before.rls !== after.rls || before.forceRls !== after.forceRls) changes.push({
525
+ object: "rls",
526
+ change: "altered",
527
+ schema,
528
+ table,
529
+ before: rlsSql(before),
530
+ after: rlsSql(after)
531
+ });
532
+ }
533
+ if (after) for (const [object, list] of TABLE_OBJECTS) {
534
+ const old = new Map((before ? list(before) : []).map((item) => [item.name, item]));
535
+ const now = new Map(list(after).map((item) => [item.name, item]));
536
+ for (const [name, item] of now) {
537
+ const was = old.get(name);
538
+ const definition = tableObjectSql(object, after, item);
539
+ if (!was) changes.push({
540
+ object,
541
+ change: "added",
542
+ schema,
543
+ table,
544
+ name,
545
+ after: definition
546
+ });
547
+ else if (was.definition !== item.definition) changes.push({
548
+ object,
549
+ change: "altered",
550
+ schema,
551
+ table,
552
+ name,
553
+ before: tableObjectSql(object, after, was),
554
+ after: definition
555
+ });
556
+ }
557
+ for (const [name, item] of old) if (!now.has(name)) changes.push({
558
+ object,
559
+ change: "dropped",
560
+ schema,
561
+ table,
562
+ name,
563
+ before: tableObjectSql(object, after, item)
564
+ });
565
+ }
566
+ const policiesBefore = new Map((before?.policies ?? []).map((policy) => [policy.name, policy]));
567
+ const policiesAfter = new Map((after?.policies ?? []).map((policy) => [policy.name, policy]));
568
+ for (const [name, policy] of policiesAfter) {
569
+ const old = policiesBefore.get(name);
570
+ const definition = policySql(schema, table, policy);
571
+ if (!old) changes.push({
572
+ object: "policy",
573
+ change: "added",
574
+ schema,
575
+ table,
576
+ name,
577
+ after: definition
578
+ });
579
+ else if (policySql(schema, table, old) !== definition) changes.push({
580
+ object: "policy",
581
+ change: "altered",
582
+ schema,
583
+ table,
584
+ name,
585
+ before: policySql(schema, table, old),
586
+ after: definition
587
+ });
588
+ }
589
+ for (const [name, policy] of policiesBefore) if (!policiesAfter.has(name)) changes.push({
590
+ object: "policy",
591
+ change: "dropped",
592
+ schema,
593
+ table,
594
+ name,
595
+ before: policySql(schema, table, policy)
596
+ });
597
+ }
598
+ return changes;
599
+ }
600
+ /** The enum types, views and functions added, dropped or altered from `target` to `branch`. */
601
+ function diffSchemaObjects(target, branch) {
602
+ const changes = [];
603
+ for (const key of [.../* @__PURE__ */ new Set([...target.keys(), ...branch.keys()])].sort()) {
604
+ const before = target.get(key);
605
+ const after = branch.get(key);
606
+ const facts = after ?? before;
607
+ if (!facts) continue;
608
+ const base = {
609
+ object: facts.object,
610
+ schema: facts.schema,
611
+ table: facts.name
612
+ };
613
+ if (!before && after) changes.push({
614
+ ...base,
615
+ change: "added",
616
+ after: after.definition
617
+ });
618
+ else if (before && !after) changes.push({
619
+ ...base,
620
+ change: "dropped",
621
+ before: before.definition
622
+ });
623
+ else if (before && after && before.definition !== after.definition) changes.push({
624
+ ...base,
625
+ change: "altered",
626
+ before: before.definition,
627
+ after: after.definition
628
+ });
629
+ }
630
+ return changes;
631
+ }
632
+ /**
633
+ * Who changed each row that differs — see the table in the module comment.
634
+ * Without a fork point every difference is the branch's.
635
+ */
636
+ function classifyRows(target, branch, forkPointKnown) {
637
+ const result = {
638
+ inserted: [],
639
+ updated: [],
640
+ deleted: [],
641
+ conflicts: [],
642
+ targetChanges: 0
643
+ };
644
+ const targetByKey = new Map(target.map((row) => [row.key, row]));
645
+ const branchKeys = /* @__PURE__ */ new Set();
646
+ for (const row of branch) {
647
+ branchKeys.add(row.key);
648
+ const old = targetByKey.get(row.key);
649
+ const branchWrote = !forkPointKnown || row.young;
650
+ if (!old) {
651
+ if (branchWrote) result.inserted.push(row.key);
652
+ else result.targetChanges += 1;
653
+ continue;
654
+ }
655
+ if (old.hash === row.hash) continue;
656
+ const targetWrote = forkPointKnown && old.young;
657
+ if (branchWrote && !targetWrote) result.updated.push(row.key);
658
+ else if (!branchWrote && targetWrote) result.targetChanges += 1;
659
+ else result.conflicts.push(row.key);
660
+ }
661
+ for (const row of target) {
662
+ if (branchKeys.has(row.key)) continue;
663
+ if (forkPointKnown && row.young) result.targetChanges += 1;
664
+ else result.deleted.push(row.key);
665
+ }
666
+ return result;
667
+ }
668
+ /**
669
+ * The sides that rewrote the table since the fork, as far as its rows tell.
670
+ *
671
+ * `ALTER COLUMN … TYPE`, an `ADD COLUMN` with a volatile default, `VACUUM
672
+ * FULL`, `CLUSTER`, `pg_repack`: each writes every row anew, so every row of
673
+ * that side reads as written after the fork, and the provenance the
674
+ * classification rests on is gone. Its mark is a side whose every row is
675
+ * young while some of them hold exactly what the other side holds — rows
676
+ * nobody changed, written again.
677
+ */
678
+ function rewrittenSides(target, branch) {
679
+ const branchHashes = new Map(branch.map((row) => [row.key, row.hash]));
680
+ if (!target.some((row) => branchHashes.get(row.key) === row.hash)) return [];
681
+ const sides = [];
682
+ if (target.length > 0 && target.every((row) => row.young)) sides.push("target");
683
+ if (branch.length > 0 && branch.every((row) => row.young)) sides.push("branch");
684
+ return sides;
685
+ }
686
+ /** Up to `limit` keys, taken in turn from each kind so that no kind crowds out the rest. */
687
+ function pickSamples(kinds, limit) {
688
+ const picked = kinds.map(() => []);
689
+ let remaining = limit;
690
+ for (let index = 0; remaining > 0; index += 1) {
691
+ let took = false;
692
+ for (let kind = 0; kind < kinds.length && remaining > 0; kind += 1) {
693
+ const key = kinds[kind][index];
694
+ if (key === void 0) continue;
695
+ picked[kind].push(key);
696
+ remaining -= 1;
697
+ took = true;
698
+ }
699
+ if (!took) break;
700
+ }
701
+ return picked;
702
+ }
703
+ var ON_DELETE = {
704
+ a: "no action",
705
+ r: "restrict",
706
+ c: "cascade",
707
+ n: "set null",
708
+ d: "set default"
709
+ };
710
+ /**
711
+ * Every foreign key that references `$1.$2`, on any table of any schema — a
712
+ * delete reaches a referencing row wherever it is. A partition's copy of a
713
+ * key (`conparentid`) is its parent's, and counted once.
714
+ */
715
+ var REFERENCING_KEYS_SQL = `
716
+ SELECT con.conname AS constraint_name, cn.nspname AS schema_name, cc.relname AS table_name,
717
+ con.confdeltype::text AS on_delete,
718
+ (SELECT json_agg(a.attname::text ORDER BY k.ord)
719
+ FROM unnest(con.conkey) WITH ORDINALITY AS k(attnum, ord)
720
+ JOIN pg_attribute a ON a.attrelid = con.conrelid AND a.attnum = k.attnum)::text AS columns,
721
+ (SELECT json_agg(a.attname::text ORDER BY k.ord)
722
+ FROM unnest(con.confkey) WITH ORDINALITY AS k(attnum, ord)
723
+ JOIN pg_attribute a ON a.attrelid = con.confrelid AND a.attnum = k.attnum)::text AS referenced
724
+ FROM pg_constraint con
725
+ JOIN pg_class cc ON cc.oid = con.conrelid
726
+ JOIN pg_namespace cn ON cn.oid = cc.relnamespace
727
+ JOIN pg_class pc ON pc.oid = con.confrelid
728
+ JOIN pg_namespace pn ON pn.oid = pc.relnamespace
729
+ WHERE con.contype = 'f' AND con.conparentid = 0 AND pn.nspname = $1 AND pc.relname = $2
730
+ ORDER BY cn.nspname, cc.relname, con.conname`;
731
+ async function referencingKeys(sql, database, facts) {
732
+ return (await sql.executeSql(REFERENCING_KEYS_SQL, {
733
+ database,
734
+ params: [facts.schema, facts.table]
735
+ })).map((row) => ({
736
+ constraint: text(row, "constraint_name"),
737
+ schema: text(row, "schema_name"),
738
+ table: text(row, "table_name"),
739
+ onDelete: ON_DELETE[text(row, "on_delete")] ?? "no action",
740
+ columns: stringList(text(row, "columns")),
741
+ referenced: stringList(text(row, "referenced"))
742
+ }));
743
+ }
744
+ /**
745
+ * The rows of `key`'s table, written after the fork, that reference a row of
746
+ * `facts` among the keys in `keysParam` — joined from the referenced table, so
747
+ * the result is per referenced key. `forkParam` is the fork point's
748
+ * transaction id.
749
+ */
750
+ function referencingRowsFrom(facts, keyExpr, key, keysParam, forkParam) {
751
+ const join = key.columns.map((column, index) => `c.${ident(column)} = t.${ident(key.referenced[index] ?? column)}`).join(" AND ");
752
+ return `FROM ${qualified(facts.schema, facts.table)} t
753
+ JOIN ${qualified(key.schema, key.table)} c ON ${join}
754
+ WHERE ${keyExpr} = ANY(${keysParam}::text[])
755
+ AND age(c.xmin) < age(${forkParam}::text::xid)`;
756
+ }
757
+ /**
758
+ * Of the keys the branch deleted from `facts`, the ones that rows the target
759
+ * wrote after the fork still reference. Read on the target only: on the
760
+ * branch, the delete already went through its foreign keys.
761
+ */
762
+ async function deleteConflicts(sql, targetDb, forkXid, facts, keyExpr, deleted) {
763
+ const found = {
764
+ keys: [],
765
+ referencedBy: []
766
+ };
767
+ if (deleted.length === 0) return found;
768
+ const keys = await referencingKeys(sql, targetDb, facts);
769
+ const conflicting = /* @__PURE__ */ new Set();
770
+ for (const key of keys) {
771
+ const rows = await sql.executeSql(`SELECT ${keyExpr} AS k, count(*)::text AS n ${referencingRowsFrom(facts, keyExpr, key, "$1", "$2")} GROUP BY 1`, {
772
+ database: targetDb,
773
+ params: [[...deleted], forkXid]
774
+ });
775
+ if (rows.length === 0) continue;
776
+ let count = 0;
777
+ for (const row of rows) {
778
+ conflicting.add(text(row, "k"));
779
+ count += Number(text(row, "n"));
780
+ }
781
+ found.referencedBy.push({
782
+ schema: key.schema,
783
+ table: key.table,
784
+ constraint: key.constraint,
785
+ onDelete: key.onDelete,
786
+ rows: count,
787
+ key
788
+ });
789
+ }
790
+ found.keys = deleted.filter((rowKey) => conflicting.has(rowKey));
791
+ return found;
792
+ }
793
+ async function rowStates(sql, database, facts, keyExpr, hashExpr, forkXid) {
794
+ const young = forkXid === null ? "false" : "(age(t.xmin) < age($1::text::xid))";
795
+ return (await sql.executeSql(`SELECT ${keyExpr} AS k, ${hashExpr} AS h, ${young} AS young
796
+ FROM ${qualified(facts.schema, facts.table)} t
797
+ ORDER BY (${keyExpr}) COLLATE "C"`, {
798
+ database,
799
+ params: forkXid === null ? void 0 : [forkXid]
800
+ })).map((row) => ({
801
+ key: text(row, "k"),
802
+ hash: text(row, "h"),
803
+ young: flag(row, "young")
804
+ }));
805
+ }
806
+ /** Each table's row count and an order-free checksum of its rows, on one side, in one query. */
807
+ async function checksums(sql, database, tables) {
808
+ if (tables.length === 0) return [];
809
+ const parts = tables.map(({ facts, keyExpr, hashExpr }, index) => `SELECT ${index} AS idx, count(*)::text AS n,
810
+ coalesce(sum(('x' || left(md5(${keyExpr} || ':' || ${hashExpr}), 15))::bit(60)::bigint), 0)::text AS s
811
+ FROM ${qualified(facts.schema, facts.table)} t`);
812
+ const rows = await sql.executeSql(parts.join("\nUNION ALL\n"), { database });
813
+ const result = tables.map(() => ({
814
+ count: 0,
815
+ sum: "0"
816
+ }));
817
+ for (const row of rows) {
818
+ const index = Number(text(row, "idx"));
819
+ result[index] = {
820
+ count: Number(text(row, "n")),
821
+ sum: text(row, "s")
822
+ };
823
+ }
824
+ return result;
825
+ }
826
+ /**
827
+ * Of one side's rows, how many were written after the fork, and the checksum
828
+ * of the ones written before it.
829
+ */
830
+ async function forkSplit(sql, database, facts, keyExpr, hashExpr, forkXid) {
831
+ const young = "(age(t.xmin) < age($1::text::xid))";
832
+ const [row] = await sql.executeSql(`SELECT count(*) FILTER (WHERE ${young})::text AS young,
833
+ coalesce(sum(('x' || left(md5(${keyExpr} || ':' || ${hashExpr}), 15))::bit(60)::bigint) FILTER (WHERE NOT ${young}), 0)::text AS s
834
+ FROM ${qualified(facts.schema, facts.table)} t`, {
835
+ database,
836
+ params: [forkXid]
837
+ });
838
+ return row ? {
839
+ young: Number(text(row, "young")),
840
+ oldSum: text(row, "s")
841
+ } : {
842
+ young: 0,
843
+ oldSum: "0"
844
+ };
845
+ }
846
+ async function sampleRows(sql, database, facts, keyExpr, keys) {
847
+ const found = /* @__PURE__ */ new Map();
848
+ if (keys.length === 0) return found;
849
+ const rows = await sql.executeSql(`SELECT ${keyExpr} AS k,
850
+ (SELECT json_object_agg(e.key, e.value) FROM json_each_text(row_to_json(t)) e)::text AS r
851
+ FROM ${qualified(facts.schema, facts.table)} t
852
+ WHERE ${keyExpr} = ANY($1::text[])`, {
853
+ database,
854
+ params: [[...keys]]
855
+ });
856
+ for (const row of rows) found.set(text(row, "k"), textRecord(text(row, "r")));
857
+ return found;
858
+ }
859
+ async function firstRows(sql, database, facts, limit) {
860
+ if (limit <= 0) return [];
861
+ const keyExpr = facts.primaryKey.length > 0 ? keyExpression(facts.primaryKey) : "'[]'";
862
+ return (await sql.executeSql(`SELECT ${keyExpr} AS k,
863
+ (SELECT json_object_agg(e.key, e.value) FROM json_each_text(row_to_json(t)) e)::text AS r
864
+ FROM ${qualified(facts.schema, facts.table)} t
865
+ LIMIT ${Math.floor(limit)}`, { database })).map((row) => ({
866
+ key: text(row, "k"),
867
+ row: textRecord(text(row, "r"))
868
+ }));
869
+ }
870
+ async function countRows(sql, database, facts) {
871
+ const [row] = await sql.executeSql(`SELECT count(*)::text AS n FROM ${qualified(facts.schema, facts.table)}`, { database });
872
+ return row ? Number(text(row, "n")) : 0;
873
+ }
874
+ function emptyTableDiff(facts) {
875
+ return {
876
+ schema: facts.schema,
877
+ table: facts.table,
878
+ primaryKey: facts.primaryKey,
879
+ inserted: 0,
880
+ updated: 0,
881
+ deleted: 0,
882
+ conflicts: 0,
883
+ targetChanges: 0,
884
+ samples: []
885
+ };
886
+ }
887
+ function positiveInteger(value, fallback, name, allowZero) {
888
+ if (value === void 0) return fallback;
889
+ if (!Number.isInteger(value) || value < (allowZero ? 0 : 1)) throw new ApiError(400, "INVALID_INPUT", `\`${name}\` must be a ${allowZero ? "non-negative" : "positive"} integer.`, void 0, EXPECTED);
890
+ return value;
891
+ }
892
+ function requireAdmin(admin) {
893
+ if (!isSQLAdmin(admin) || !isBranchAdmin(admin)) throw new ApiError(501, "BRANCH_DIFF_UNSUPPORTED", "This data source cannot compare branches: it needs SQL and database branching (a Postgres backend with an admin connection).", void 0, EXPECTED);
894
+ return admin;
895
+ }
896
+ async function computeDiff(admin, branchName, options = {}) {
897
+ const sql = requireAdmin(admin);
898
+ const sampleLimit = Math.min(positiveInteger(options.sampleLimit, 100, "sampleLimit", true), MAX_BRANCH_DIFF_SAMPLE_LIMIT);
899
+ const maxRows = positiveInteger(options.maxRowsPerTable, DEFAULT_BRANCH_DIFF_MAX_ROWS, "maxRowsPerTable", false);
900
+ const schemas = options.schemas && options.schemas.length > 0 ? [...new Set(options.schemas)].sort() : null;
901
+ const branch = await sql.getBranchInfo(branchName);
902
+ if (!branch) throw new ApiError(404, "BRANCH_NOT_FOUND", `Branch "${branchName}" not found.`, void 0, EXPECTED);
903
+ const branchDb = branch.database;
904
+ const targetDb = branch.parentDatabase;
905
+ const [targetCatalog, branchCatalog, targetObjects, branchObjects, fork] = await Promise.all([
906
+ readCatalog(sql, targetDb, schemas),
907
+ readCatalog(sql, branchDb, schemas),
908
+ readSchemaObjects(sql, targetDb, schemas),
909
+ readSchemaObjects(sql, branchDb, schemas),
910
+ branch.forkXid !== void 0 ? sql.executeSql("SELECT $1::text AS xid, age($1::text::xid)::text AS age", {
911
+ database: targetDb,
912
+ params: [branch.forkXid]
913
+ }) : sql.executeSql("SELECT xmin::text AS xid, age(xmin)::text AS age FROM pg_database WHERE datname = $1", {
914
+ database: targetDb,
915
+ params: [branchDb]
916
+ })
917
+ ]);
918
+ const forkAge = fork[0] ? Number(text(fork[0], "age")) : NaN;
919
+ const forkPointKnown = Number.isFinite(forkAge) && forkAge >= 0 && forkAge < FROZEN_AGE;
920
+ const forkXid = forkPointKnown && fork[0] ? text(fork[0], "xid") : null;
921
+ const schemaChanges = [...diffCatalogs(targetCatalog, branchCatalog), ...diffSchemaObjects(targetObjects, branchObjects)];
922
+ const data = [];
923
+ const matched = /* @__PURE__ */ new Map();
924
+ /** What the fingerprint covers, per table, beyond the schema. */
925
+ const seen = [];
926
+ const shared = [];
927
+ for (const key of [.../* @__PURE__ */ new Set([...targetCatalog.keys(), ...branchCatalog.keys()])].sort()) {
928
+ const before = targetCatalog.get(key);
929
+ const after = branchCatalog.get(key);
930
+ if (before && after) {
931
+ const types = new Map(before.columns.map((column) => [column.name, column.type]));
932
+ const compared = after.columns.filter((column) => types.get(column.name) === column.type).map((column) => column.name).sort();
933
+ shared.push({
934
+ key,
935
+ target: before,
936
+ branch: after,
937
+ compared
938
+ });
939
+ continue;
940
+ }
941
+ const facts = after ?? before;
942
+ if (!facts) continue;
943
+ const database = after ? branchDb : targetDb;
944
+ const count = await countRows(sql, database, facts);
945
+ seen.push({
946
+ key,
947
+ side: after ? "branch" : "target",
948
+ count
949
+ });
950
+ if (count === 0) continue;
951
+ const entry = emptyTableDiff(facts);
952
+ const change = after ? "inserted" : "deleted";
953
+ if (after) entry.inserted = count;
954
+ else entry.deleted = count;
955
+ entry.samples = (await firstRows(sql, database, facts, sampleLimit)).map(({ key: rowKey, row }) => after ? {
956
+ change,
957
+ key: rowKey,
958
+ after: redacted(row)
959
+ } : {
960
+ change,
961
+ key: rowKey,
962
+ before: redacted(row)
963
+ });
964
+ entry.blocked = after ? {
965
+ code: "TABLE_ADDED",
966
+ message: "The table is new on the branch. Create it on the target with your migrations (rebase db push), then merge its rows."
967
+ } : {
968
+ code: "TABLE_DROPPED",
969
+ message: "The branch dropped the table. A merge never drops one; drop it with your migrations."
970
+ };
971
+ data.push(entry);
972
+ }
973
+ const probes = shared.map(({ target, compared }) => ({
974
+ facts: target,
975
+ keyExpr: target.primaryKey.length > 0 ? keyExpression(target.primaryKey) : "''",
976
+ hashExpr: hashExpression(compared.length > 0 ? target.columns.filter((column) => compared.includes(column.name)).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0) : target.columns)
977
+ }));
978
+ const [targetSums, branchSums] = await Promise.all([checksums(sql, targetDb, probes), checksums(sql, branchDb, probes)]);
979
+ for (let index = 0; index < shared.length; index += 1) {
980
+ const { key, target, branch: branchFacts, compared } = shared[index];
981
+ const { keyExpr, hashExpr } = probes[index];
982
+ const targetSum = targetSums[index];
983
+ const branchSum = branchSums[index];
984
+ if (targetSum.count === branchSum.count && targetSum.sum === branchSum.sum) continue;
985
+ const entry = emptyTableDiff(branchFacts);
986
+ const rowCounts = {
987
+ target: targetSum.count,
988
+ branch: branchSum.count
989
+ };
990
+ const typesOf = (facts) => facts.primaryKey.map((column) => facts.columns.find((c) => c.name === column)?.type ?? "");
991
+ let block;
992
+ if (!sameList(target.primaryKey, branchFacts.primaryKey) || !sameList(typesOf(target), typesOf(branchFacts))) block = {
993
+ code: "PRIMARY_KEY_DIFFERS",
994
+ message: "The primary key differs between the branch and the target, so rows cannot be matched."
995
+ };
996
+ else if (target.primaryKey.length === 0) block = {
997
+ code: "NO_PRIMARY_KEY",
998
+ message: "The table has no primary key, so its rows cannot be matched one by one."
999
+ };
1000
+ else if (targetSum.count > maxRows || branchSum.count > maxRows) block = {
1001
+ code: "TOO_LARGE",
1002
+ message: `The table holds more than ${maxRows} rows on one side, more than a diff matches row by row.`
1003
+ };
1004
+ if (block) {
1005
+ if (forkXid !== null) {
1006
+ const [targetSplit, branchSplit] = await Promise.all([forkSplit(sql, targetDb, target, keyExpr, hashExpr, forkXid), forkSplit(sql, branchDb, branchFacts, keyExpr, hashExpr, forkXid)]);
1007
+ if (branchSplit.young === 0 && targetSplit.oldSum === branchSplit.oldSum) {
1008
+ entry.targetChanges = targetSplit.young;
1009
+ seen.push({
1010
+ key,
1011
+ untouched: true,
1012
+ targetSum,
1013
+ branchSum
1014
+ });
1015
+ data.push(entry);
1016
+ continue;
1017
+ }
1018
+ }
1019
+ entry.blocked = block;
1020
+ entry.rowCounts = rowCounts;
1021
+ seen.push({
1022
+ key,
1023
+ block: block.code,
1024
+ targetSum,
1025
+ branchSum
1026
+ });
1027
+ data.push(entry);
1028
+ continue;
1029
+ }
1030
+ const [targetRows, branchRows] = await Promise.all([rowStates(sql, targetDb, target, keyExpr, hashExpr, forkXid), rowStates(sql, branchDb, branchFacts, keyExpr, hashExpr, forkXid)]);
1031
+ const rows = classifyRows(targetRows, branchRows, forkPointKnown);
1032
+ const rewritten = forkPointKnown ? rewrittenSides(targetRows, branchRows) : [];
1033
+ if (rewritten.includes("target") || rewritten.includes("branch") && rows.conflicts.length > 0) {
1034
+ entry.blocked = {
1035
+ code: "TABLE_REWRITTEN",
1036
+ message: `${rewritten.includes("target") ? targetDb : "The branch"} rewrote the table after the branch was made — a column type change, a volatile DEFAULT, VACUUM FULL or CLUSTER writes every row anew — so every row reads as written since, and which side changed which row can no longer be told. Merge the branch before such a migration, or make the branch again from ${targetDb}.`
1037
+ };
1038
+ entry.rowCounts = rowCounts;
1039
+ seen.push({
1040
+ key,
1041
+ block: "TABLE_REWRITTEN",
1042
+ targetSum,
1043
+ branchSum
1044
+ });
1045
+ data.push(entry);
1046
+ continue;
1047
+ }
1048
+ const referenced = forkXid !== null ? await deleteConflicts(sql, targetDb, forkXid, target, keyExpr, rows.deleted) : {
1049
+ keys: [],
1050
+ referencedBy: []
1051
+ };
1052
+ if (referenced.keys.length > 0) {
1053
+ const inConflict = new Set(referenced.keys);
1054
+ rows.deleted = rows.deleted.filter((rowKey) => !inConflict.has(rowKey));
1055
+ entry.referencedBy = referenced.referencedBy.map(({ key: _key, ...rest }) => rest);
1056
+ }
1057
+ entry.inserted = rows.inserted.length;
1058
+ entry.updated = rows.updated.length;
1059
+ entry.deleted = rows.deleted.length;
1060
+ entry.conflicts = rows.conflicts.length + referenced.keys.length;
1061
+ entry.targetChanges = rows.targetChanges;
1062
+ const branchChanged = entry.inserted + entry.updated + entry.deleted + entry.conflicts > 0;
1063
+ if (!branchChanged && entry.targetChanges === 0) continue;
1064
+ const columnsDiffer = target.columns.length !== compared.length || branchFacts.columns.length !== compared.length;
1065
+ if (branchChanged && columnsDiffer) entry.blocked = {
1066
+ code: "COLUMNS_DIFFER",
1067
+ message: "The table's columns differ between the branch and the target, so its rows are compared on the columns they share and cannot be merged. Add the missing columns to the target with your migrations (rebase db push) and merge again. A migration that rewrites the table — a column type change does — makes every row read as written since and blocks the table for good: merge before such a change, or make the branch again after it."
1068
+ };
1069
+ const [conflictKeys, insertedKeys, updatedKeys, deletedKeys] = pickSamples([
1070
+ [...rows.conflicts, ...referenced.keys],
1071
+ rows.inserted,
1072
+ rows.updated,
1073
+ rows.deleted
1074
+ ], sampleLimit);
1075
+ const [before, after] = await Promise.all([sampleRows(sql, targetDb, target, keyExpr, [
1076
+ ...conflictKeys,
1077
+ ...updatedKeys,
1078
+ ...deletedKeys
1079
+ ]), sampleRows(sql, branchDb, branchFacts, keyExpr, [
1080
+ ...conflictKeys,
1081
+ ...insertedKeys,
1082
+ ...updatedKeys
1083
+ ])]);
1084
+ const changed = (rowKey) => {
1085
+ const old = before.get(rowKey) ?? {};
1086
+ const now = after.get(rowKey) ?? {};
1087
+ return compared.filter((column) => old[column] !== now[column]);
1088
+ };
1089
+ const shownBefore = (rowKey) => redacted(before.get(rowKey));
1090
+ const shownAfter = (rowKey) => redacted(after.get(rowKey));
1091
+ entry.samples = [
1092
+ ...conflictKeys.map((rowKey) => after.has(rowKey) ? {
1093
+ change: "conflict",
1094
+ key: rowKey,
1095
+ before: shownBefore(rowKey),
1096
+ after: shownAfter(rowKey),
1097
+ changedColumns: changed(rowKey)
1098
+ } : {
1099
+ change: "conflict",
1100
+ key: rowKey,
1101
+ before: shownBefore(rowKey)
1102
+ }),
1103
+ ...insertedKeys.map((rowKey) => ({
1104
+ change: "inserted",
1105
+ key: rowKey,
1106
+ after: shownAfter(rowKey)
1107
+ })),
1108
+ ...updatedKeys.map((rowKey) => ({
1109
+ change: "updated",
1110
+ key: rowKey,
1111
+ before: shownBefore(rowKey),
1112
+ after: shownAfter(rowKey),
1113
+ changedColumns: changed(rowKey)
1114
+ })),
1115
+ ...deletedKeys.map((rowKey) => ({
1116
+ change: "deleted",
1117
+ key: rowKey,
1118
+ before: shownBefore(rowKey)
1119
+ }))
1120
+ ];
1121
+ data.push(entry);
1122
+ const targetDigest = md5(targetRows.map((row) => `${row.key}:${row.hash}`).join(","));
1123
+ const stateDigest = (states) => md5(states.map((row) => `${row.key}:${row.hash}:${row.young ? 1 : 0}`).join(","));
1124
+ seen.push({
1125
+ key,
1126
+ target: stateDigest(targetRows),
1127
+ branch: stateDigest(branchRows),
1128
+ referencedBy: entry.referencedBy ?? []
1129
+ });
1130
+ matched.set(key, {
1131
+ facts: target,
1132
+ keyExpression: keyExpr,
1133
+ hashExpression: hashExpr,
1134
+ targetDigest,
1135
+ branchHashes: new Map(branchRows.map((row) => [row.key, row.hash])),
1136
+ rows,
1137
+ deleteConflicts: referenced
1138
+ });
1139
+ }
1140
+ const fingerprint = createHash("sha256").update(JSON.stringify({
1141
+ branchDb,
1142
+ targetDb,
1143
+ schemas,
1144
+ maxRows,
1145
+ forkPointKnown,
1146
+ schema: schemaChanges,
1147
+ tables: seen
1148
+ }), "utf8").digest("hex");
1149
+ return {
1150
+ diff: {
1151
+ branch: branch.name,
1152
+ branchDatabase: branchDb,
1153
+ targetDatabase: targetDb,
1154
+ schemas: schemas ?? [...new Set([...targetCatalog.values(), ...branchCatalog.values()].map((facts) => facts.schema))].sort(),
1155
+ forkPointKnown,
1156
+ fingerprint,
1157
+ schema: schemaChanges,
1158
+ data,
1159
+ sampleLimit,
1160
+ computedAt: (/* @__PURE__ */ new Date()).toISOString()
1161
+ },
1162
+ matched,
1163
+ forkXid
1164
+ };
1165
+ }
1166
+ /**
1167
+ * What the branch `branchName` changed against the database it was made from:
1168
+ * its schema and policies, and per table the rows it inserted, updated and
1169
+ * deleted, with up to `sampleLimit` of them. Reads both databases and writes
1170
+ * neither.
1171
+ */
1172
+ async function diffDatabaseBranch(admin, branchName, options) {
1173
+ return (await computeDiff(admin, branchName, options)).diff;
1174
+ }
1175
+ /** Up to ten keys, for a message. */
1176
+ function keyList(keys) {
1177
+ return keys.length > 10 ? `${keys.slice(0, 10).join(", ")} and ${keys.length - 10} more` : keys.join(", ");
1178
+ }
1179
+ /** `public.posts (1 row, ON DELETE CASCADE)`, for each foreign key. */
1180
+ function referencingList(found) {
1181
+ return found.map((entry) => `${entry.schema}.${entry.table} (${entry.rows} row(s), ON DELETE ${entry.onDelete.toUpperCase()})`).join(", ");
1182
+ }
1183
+ function databaseFailure(error) {
1184
+ const field = (value, key) => {
1185
+ const found = Reflect.get(value, key);
1186
+ return typeof found === "string" && found !== "" ? found : void 0;
1187
+ };
1188
+ let current = error;
1189
+ for (let depth = 0; depth < 5 && typeof current === "object" && current !== null; depth += 1) {
1190
+ const sqlState = field(current, "code");
1191
+ if (sqlState && /^[0-9A-Z]{5}$/.test(sqlState)) return {
1192
+ sqlState,
1193
+ message: field(current, "message") ?? sqlState,
1194
+ table: field(current, "table"),
1195
+ constraint: field(current, "constraint"),
1196
+ detail: field(current, "detail")
1197
+ };
1198
+ current = Reflect.get(current, "cause");
1199
+ }
1200
+ }
1201
+ /** The marker a merge statement's own refusal carries, to be told from any other failure. */
1202
+ var MERGE_REFUSAL = "rebase branch merge refused";
1203
+ /**
1204
+ * Apply the rows the branch `branchName` changed to the database it was made
1205
+ * from. See `BranchMergeOptions` for what wins and when nothing is written.
1206
+ */
1207
+ async function mergeDatabaseBranch(admin, branchName, options) {
1208
+ const sql = requireAdmin(admin);
1209
+ if (typeof options?.expectedFingerprint !== "string" || options.expectedFingerprint === "") throw new ApiError(400, "INVALID_INPUT", "Send `expectedFingerprint`: the fingerprint of the diff that was reviewed.", void 0, EXPECTED);
1210
+ const onConflict = options.onConflict ?? "abort";
1211
+ if (onConflict !== "abort" && onConflict !== "branch" && onConflict !== "target") throw new ApiError(400, "INVALID_INPUT", "`onConflict` must be \"abort\", \"branch\" or \"target\".", void 0, EXPECTED);
1212
+ const { diff, matched, forkXid } = await computeDiff(admin, branchName, {
1213
+ sampleLimit: 0,
1214
+ schemas: options.schemas,
1215
+ maxRowsPerTable: options.maxRowsPerTable
1216
+ });
1217
+ if (diff.fingerprint !== options.expectedFingerprint) throw new ApiError(409, "BRANCH_DIFF_STALE", `The branch or ${diff.targetDatabase} changed since the diff was read. Nothing was merged; review the diff again.`, void 0, EXPECTED);
1218
+ const blocked = diff.data.filter((entry) => entry.blocked);
1219
+ if (blocked.length > 0 && options.skipBlocked !== true) throw new ApiError(409, "BRANCH_MERGE_BLOCKED", `${blocked.length} table(s) with changed rows cannot be merged (${blocked.map((entry) => `${entry.schema}.${entry.table}: ${entry.blocked?.code}`).join(", ")}). Nothing was merged. Merge with skipBlocked to leave them as they are.`, { tables: blocked.map((entry) => ({
1220
+ schema: entry.schema,
1221
+ table: entry.table,
1222
+ code: entry.blocked?.code,
1223
+ reason: entry.blocked?.message ?? ""
1224
+ })) }, EXPECTED);
1225
+ const conflicted = diff.data.filter((entry) => !entry.blocked && entry.conflicts > 0);
1226
+ if (conflicted.length > 0 && onConflict === "abort") {
1227
+ const referenced = conflicted.flatMap((entry) => {
1228
+ const table = matched.get(tableKey(entry.schema, entry.table));
1229
+ return table && table.deleteConflicts.keys.length > 0 ? [{
1230
+ entry,
1231
+ conflicts: table.deleteConflicts
1232
+ }] : [];
1233
+ });
1234
+ const both = conflicted.reduce((sum, entry) => sum + entry.conflicts, 0) - referenced.reduce((sum, { conflicts }) => sum + conflicts.keys.length, 0);
1235
+ const sentences = [];
1236
+ if (both > 0) sentences.push(`${both} row(s) were changed on both the branch and ${diff.targetDatabase}.`);
1237
+ for (const { entry, conflicts } of referenced) sentences.push(`The branch deleted ${conflicts.keys.length} row(s) of ${entry.schema}.${entry.table} (${keyList(conflicts.keys)}) that rows ${diff.targetDatabase} wrote since still reference: ${referencingList(conflicts.referencedBy)}.`);
1238
+ const remedy = referenced.length > 0 ? "Merge again with onConflict \"target\" to keep those rows, or \"branch\" to delete them and let the foreign keys reach the rows that reference them." : "Merge again choosing which side wins.";
1239
+ throw new ApiError(409, "BRANCH_MERGE_CONFLICT", `${sentences.join(" ")} Nothing was merged. ${remedy}`, { tables: conflicted.map((entry) => {
1240
+ const keys = matched.get(tableKey(entry.schema, entry.table))?.deleteConflicts;
1241
+ const reason = keys && keys.keys.length > 0 ? `${entry.conflicts} conflicting row(s); deleted on the branch and referenced by rows ${diff.targetDatabase} wrote since: ${keyList(keys.keys)} (${referencingList(keys.referencedBy)})` : `${entry.conflicts} conflicting row(s)`;
1242
+ return {
1243
+ schema: entry.schema,
1244
+ table: entry.table,
1245
+ reason
1246
+ };
1247
+ }) }, EXPECTED);
1248
+ }
1249
+ const result = {
1250
+ branch: diff.branch,
1251
+ targetDatabase: diff.targetDatabase,
1252
+ tables: [],
1253
+ skipped: blocked.flatMap((entry) => entry.blocked ? [{
1254
+ schema: entry.schema,
1255
+ table: entry.table,
1256
+ reason: entry.blocked
1257
+ }] : []),
1258
+ conflictsKept: onConflict === "target" ? conflicted.reduce((sum, entry) => sum + entry.conflicts, 0) : 0,
1259
+ schemaChangesNotMerged: diff.schema.length,
1260
+ warnings: []
1261
+ };
1262
+ const params = [];
1263
+ const param = (value) => {
1264
+ params.push(value);
1265
+ return `$${params.length}`;
1266
+ };
1267
+ const guards = [];
1268
+ const writes = [];
1269
+ const written = [];
1270
+ for (const entry of diff.data) {
1271
+ if (entry.blocked) continue;
1272
+ const table = matched.get(tableKey(entry.schema, entry.table));
1273
+ if (!table) continue;
1274
+ const updates = onConflict === "branch" ? [...table.rows.updated, ...table.rows.conflicts] : table.rows.updated;
1275
+ const inserts = table.rows.inserted;
1276
+ const deletes = onConflict === "branch" ? [...table.rows.deleted, ...table.deleteConflicts.keys] : table.rows.deleted;
1277
+ if (updates.length + inserts.length + deletes.length === 0) continue;
1278
+ const branchRows = await sql.executeSql(`SELECT ${table.keyExpression} AS k, ${table.hashExpression} AS h, row_to_json(t)::text AS r
1279
+ FROM ${qualified(table.facts.schema, table.facts.table)} t
1280
+ WHERE ${table.keyExpression} = ANY($1::text[])`, {
1281
+ database: diff.branchDatabase,
1282
+ params: [[...updates, ...inserts]]
1283
+ });
1284
+ const rowText = /* @__PURE__ */ new Map();
1285
+ for (const row of branchRows) {
1286
+ const rowKey = text(row, "k");
1287
+ if (table.branchHashes.get(rowKey) !== text(row, "h")) break;
1288
+ rowText.set(rowKey, text(row, "r"));
1289
+ }
1290
+ if (rowText.size !== updates.length + inserts.length) throw new ApiError(409, "BRANCH_DIFF_STALE", "The branch changed while it was being merged. Nothing was merged; review the diff again.", void 0, EXPECTED);
1291
+ const rowsJson = (keys) => `[${keys.map((rowKey) => rowText.get(rowKey)).join(",")}]`;
1292
+ const target = qualified(table.facts.schema, table.facts.table);
1293
+ const index = written.length;
1294
+ const writesBefore = writes.length;
1295
+ const writable = table.facts.columns.filter((column) => column.generated !== "s");
1296
+ const primaryKey = new Set(table.facts.primaryKey);
1297
+ const keyMatch = table.facts.primaryKey.map((column) => `t.${ident(column)} = r.${ident(column)}`).join(" AND ");
1298
+ if (deletes.length > 0) writes.push({
1299
+ name: `d${index}`,
1300
+ sql: `DELETE FROM ${target} AS t WHERE (SELECT ok FROM guard) AND ${table.keyExpression} = ANY(${param([...deletes])}::text[]) RETURNING 1`,
1301
+ expected: deletes.length
1302
+ });
1303
+ const settable = writable.filter((column) => !primaryKey.has(column.name) && column.identity !== "a");
1304
+ if (updates.length > 0 && settable.length > 0) writes.push({
1305
+ name: `u${index}`,
1306
+ sql: `UPDATE ${target} AS t SET (${settable.map((column) => ident(column.name)).join(", ")}) = ROW(${settable.map((column) => `r.${ident(column.name)}`).join(", ")}) FROM json_populate_recordset(NULL::${target}, ${param(rowsJson(updates))}::json) AS r WHERE (SELECT ok FROM guard) AND ${keyMatch} RETURNING 1`,
1307
+ expected: updates.length
1308
+ });
1309
+ if (inserts.length > 0) writes.push({
1310
+ name: `i${index}`,
1311
+ sql: `INSERT INTO ${target} (${writable.map((column) => ident(column.name)).join(", ")}) OVERRIDING SYSTEM VALUE SELECT ${writable.map((column) => `r.${ident(column.name)}`).join(", ")} FROM json_populate_recordset(NULL::${target}, ${param(rowsJson(inserts))}::json) AS r WHERE (SELECT ok FROM guard) RETURNING 1`,
1312
+ expected: inserts.length
1313
+ });
1314
+ if (writes.length === writesBefore) continue;
1315
+ guards.push(`(SELECT md5(coalesce(string_agg(${table.keyExpression} || ':' || ${table.hashExpression}, ',' ORDER BY (${table.keyExpression}) COLLATE "C"), '')) FROM ${target} t) = ${param(table.targetDigest)}`);
1316
+ if (deletes.length > 0 && forkXid !== null) {
1317
+ const counted = new Map(table.deleteConflicts.referencedBy.map((found) => [found.constraint + "\0" + found.schema + "." + found.table, found.rows]));
1318
+ const keys = await referencingKeys(sql, diff.targetDatabase, table.facts);
1319
+ const keysParam = keys.length > 0 ? param([...deletes]) : "";
1320
+ const forkParam = keys.length > 0 ? param(forkXid) : "";
1321
+ for (const key of keys) {
1322
+ const expected = onConflict === "branch" ? counted.get(key.constraint + "\0" + key.schema + "." + key.table) ?? 0 : 0;
1323
+ guards.push(`(SELECT count(*) ${referencingRowsFrom(table.facts, table.keyExpression, key, keysParam, forkParam)}) = ${expected}`);
1324
+ }
1325
+ }
1326
+ written.push({
1327
+ entry,
1328
+ table,
1329
+ inserted: inserts.length,
1330
+ updated: updates.length,
1331
+ deleted: deletes.length
1332
+ });
1333
+ }
1334
+ if (written.length === 0) return result;
1335
+ if (onConflict === "branch") for (const { entry, table } of written) {
1336
+ const { keys, referencedBy } = table.deleteConflicts;
1337
+ if (keys.length === 0) continue;
1338
+ result.warnings.push(`Deleting ${keys.length} row(s) of ${entry.schema}.${entry.table} the branch deleted (${keyList(keys)}) reached rows ${diff.targetDatabase} wrote since the branch was made: ${referencingList(referencedBy)}.`);
1339
+ }
1340
+ const counted = writes.map((write) => `(SELECT count(*) FROM ${write.name}) = ${write.expected}`).join(" AND ");
1341
+ const statement = `WITH guard AS (SELECT (${guards.join(" AND ")}) AS ok),\n` + writes.map((write) => `${write.name} AS (${write.sql})`).join(",\n") + `\nSELECT CASE WHEN NOT (SELECT ok FROM guard) THEN 'conflict'
1342
+ WHEN ${counted || "true"} THEN 'merged'
1343
+ ELSE CAST('${MERGE_REFUSAL}: a write did not reach the rows it read (' ||
1344
+ ${writes.map((write) => `(SELECT count(*) FROM ${write.name})::text`).join(" || ',' || ") || "''"} || ')' AS integer)::text
1345
+ END AS outcome`;
1346
+ let outcome;
1347
+ try {
1348
+ const [row] = await sql.executeSql(statement, {
1349
+ database: diff.targetDatabase,
1350
+ params
1351
+ });
1352
+ outcome = row ? text(row, "outcome") : "";
1353
+ } catch (error) {
1354
+ if ((error instanceof Error ? error.message : String(error)).includes(MERGE_REFUSAL)) throw new ApiError(409, "BRANCH_MERGE_CONFLICT", `${diff.targetDatabase} refused part of the merge — a row changed meanwhile, a trigger skipped it, or a policy hid it. Nothing was merged.`, void 0, EXPECTED);
1355
+ const failure = databaseFailure(error);
1356
+ if (failure && /^(22|23|P0|42501)/.test(failure.sqlState)) {
1357
+ const where = [failure.table ? `table ${failure.table}` : "", failure.constraint ? `constraint ${failure.constraint}` : ""].filter(Boolean).join(", ");
1358
+ throw new ApiError(409, "BRANCH_MERGE_FAILED", `${diff.targetDatabase} refused the branch's rows${where ? ` (${where})` : ""}: ${failure.message}. Nothing was merged. The branch's rows break a rule the target holds — its schema or constraints differ from the branch's; see the diff's schema changes.`, {
1359
+ sqlState: failure.sqlState,
1360
+ table: failure.table,
1361
+ constraint: failure.constraint,
1362
+ detail: failure.detail
1363
+ }, EXPECTED);
1364
+ }
1365
+ throw error;
1366
+ }
1367
+ if (outcome !== "merged") throw new ApiError(409, "BRANCH_MERGE_CONFLICT", `${diff.targetDatabase} changed while the merge was being prepared. Nothing was merged; review the diff again.`, void 0, EXPECTED);
1368
+ for (const { entry, table, inserted, updated, deleted } of written) {
1369
+ result.tables.push({
1370
+ schema: entry.schema,
1371
+ table: entry.table,
1372
+ inserted,
1373
+ updated,
1374
+ deleted
1375
+ });
1376
+ if (inserted === 0) continue;
1377
+ for (const column of table.facts.columns) {
1378
+ if (!column.sequence) continue;
1379
+ try {
1380
+ await sql.executeSql(`SELECT setval($1::regclass, m.mx)
1381
+ FROM (SELECT max(${ident(column.name)})::bigint AS mx FROM ${qualified(entry.schema, entry.table)}) m
1382
+ WHERE m.mx IS NOT NULL AND m.mx > coalesce(pg_sequence_last_value($1::regclass), 0)`, {
1383
+ database: diff.targetDatabase,
1384
+ params: [column.sequence]
1385
+ });
1386
+ } catch (error) {
1387
+ result.warnings.push(`The rows were merged, but the sequence ${column.sequence} could not be advanced past them: ${error instanceof Error ? error.message : String(error)}. Run SELECT setval('${column.sequence}', (SELECT max(${ident(column.name)}) FROM ${qualified(entry.schema, entry.table)})); on ${diff.targetDatabase}.`);
1388
+ }
1389
+ }
1390
+ }
1391
+ return result;
1392
+ }
1393
+ //#endregion
1394
+ export { mergeDatabaseBranch as a, diffDatabaseBranch as i, DEFAULT_BRANCH_DIFF_SAMPLE_LIMIT as n, branch_diff_exports as r, DEFAULT_BRANCH_DIFF_MAX_ROWS as t };
1395
+
1396
+ //# sourceMappingURL=branch-diff-CxE4VlxT.js.map