@rocicorp/zero 1.4.0 → 1.5.0-canary.1

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 (189) hide show
  1. package/out/analyze-query/src/analyze-cli.js +2 -2
  2. package/out/analyze-query/src/analyze-cli.js.map +1 -1
  3. package/out/zero/package.js +1 -1
  4. package/out/zero/package.js.map +1 -1
  5. package/out/zero-cache/src/auth/auth.d.ts +1 -1
  6. package/out/zero-cache/src/auth/auth.d.ts.map +1 -1
  7. package/out/zero-cache/src/auth/auth.js +1 -1
  8. package/out/zero-cache/src/auth/auth.js.map +1 -1
  9. package/out/zero-cache/src/auth/write-authorizer.d.ts +1 -1
  10. package/out/zero-cache/src/auth/write-authorizer.d.ts.map +1 -1
  11. package/out/zero-cache/src/auth/write-authorizer.js.map +1 -1
  12. package/out/zero-cache/src/config/normalize.d.ts.map +1 -1
  13. package/out/zero-cache/src/config/normalize.js +8 -0
  14. package/out/zero-cache/src/config/normalize.js.map +1 -1
  15. package/out/zero-cache/src/config/zero-config.d.ts +8 -4
  16. package/out/zero-cache/src/config/zero-config.d.ts.map +1 -1
  17. package/out/zero-cache/src/config/zero-config.js +28 -6
  18. package/out/zero-cache/src/config/zero-config.js.map +1 -1
  19. package/out/zero-cache/src/custom/fetch.d.ts +1 -1
  20. package/out/zero-cache/src/custom/fetch.d.ts.map +1 -1
  21. package/out/zero-cache/src/custom/fetch.js +2 -2
  22. package/out/zero-cache/src/custom/fetch.js.map +1 -1
  23. package/out/zero-cache/src/custom-queries/transform-query.d.ts +21 -7
  24. package/out/zero-cache/src/custom-queries/transform-query.d.ts.map +1 -1
  25. package/out/zero-cache/src/custom-queries/transform-query.js +26 -9
  26. package/out/zero-cache/src/custom-queries/transform-query.js.map +1 -1
  27. package/out/zero-cache/src/server/change-streamer.d.ts.map +1 -1
  28. package/out/zero-cache/src/server/change-streamer.js +2 -1
  29. package/out/zero-cache/src/server/change-streamer.js.map +1 -1
  30. package/out/zero-cache/src/server/runner/run-worker.d.ts.map +1 -1
  31. package/out/zero-cache/src/server/runner/run-worker.js +5 -2
  32. package/out/zero-cache/src/server/runner/run-worker.js.map +1 -1
  33. package/out/zero-cache/src/server/syncer.js +3 -3
  34. package/out/zero-cache/src/server/syncer.js.map +1 -1
  35. package/out/zero-cache/src/services/change-source/custom/change-source.js +2 -2
  36. package/out/zero-cache/src/services/change-source/custom/change-source.js.map +1 -1
  37. package/out/zero-cache/src/services/change-source/pg/change-source.d.ts.map +1 -1
  38. package/out/zero-cache/src/services/change-source/pg/change-source.js +24 -20
  39. package/out/zero-cache/src/services/change-source/pg/change-source.js.map +1 -1
  40. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts +258 -45
  41. package/out/zero-cache/src/services/change-source/pg/schema/ddl.d.ts.map +1 -1
  42. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js +119 -83
  43. package/out/zero-cache/src/services/change-source/pg/schema/ddl.js.map +1 -1
  44. package/out/zero-cache/src/services/change-source/pg/schema/init.d.ts.map +1 -1
  45. package/out/zero-cache/src/services/change-source/pg/schema/init.js +1 -1
  46. package/out/zero-cache/src/services/change-source/pg/schema/init.js.map +1 -1
  47. package/out/zero-cache/src/services/change-source/pg/schema/shard.d.ts.map +1 -1
  48. package/out/zero-cache/src/services/change-source/pg/schema/shard.js +2 -1
  49. package/out/zero-cache/src/services/change-source/pg/schema/shard.js.map +1 -1
  50. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts +1 -0
  51. package/out/zero-cache/src/services/change-streamer/change-streamer-http.d.ts.map +1 -1
  52. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js +3 -3
  53. package/out/zero-cache/src/services/change-streamer/change-streamer-http.js.map +1 -1
  54. package/out/zero-cache/src/services/http-service.d.ts +1 -0
  55. package/out/zero-cache/src/services/http-service.d.ts.map +1 -1
  56. package/out/zero-cache/src/services/http-service.js +5 -4
  57. package/out/zero-cache/src/services/http-service.js.map +1 -1
  58. package/out/zero-cache/src/services/life-cycle.d.ts +1 -1
  59. package/out/zero-cache/src/services/life-cycle.d.ts.map +1 -1
  60. package/out/zero-cache/src/services/life-cycle.js +1 -2
  61. package/out/zero-cache/src/services/life-cycle.js.map +1 -1
  62. package/out/zero-cache/src/services/mutagen/mutagen.d.ts +1 -1
  63. package/out/zero-cache/src/services/mutagen/mutagen.d.ts.map +1 -1
  64. package/out/zero-cache/src/services/mutagen/mutagen.js +1 -1
  65. package/out/zero-cache/src/services/mutagen/mutagen.js.map +1 -1
  66. package/out/zero-cache/src/services/mutagen/pusher.d.ts +4 -3
  67. package/out/zero-cache/src/services/mutagen/pusher.d.ts.map +1 -1
  68. package/out/zero-cache/src/services/mutagen/pusher.js +57 -38
  69. package/out/zero-cache/src/services/mutagen/pusher.js.map +1 -1
  70. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js +2 -1
  71. package/out/zero-cache/src/services/shadow-sync/shadow-sync-service.js.map +1 -1
  72. package/out/zero-cache/src/services/view-syncer/client-handler.js +1 -1
  73. package/out/zero-cache/src/services/view-syncer/client-handler.js.map +1 -1
  74. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts +41 -27
  75. package/out/zero-cache/src/services/view-syncer/connection-context-manager.d.ts.map +1 -1
  76. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js +147 -104
  77. package/out/zero-cache/src/services/view-syncer/connection-context-manager.js.map +1 -1
  78. package/out/zero-cache/src/services/view-syncer/cvr.d.ts +6 -0
  79. package/out/zero-cache/src/services/view-syncer/cvr.d.ts.map +1 -1
  80. package/out/zero-cache/src/services/view-syncer/cvr.js +8 -0
  81. package/out/zero-cache/src/services/view-syncer/cvr.js.map +1 -1
  82. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts +3 -3
  83. package/out/zero-cache/src/services/view-syncer/view-syncer.d.ts.map +1 -1
  84. package/out/zero-cache/src/services/view-syncer/view-syncer.js +119 -86
  85. package/out/zero-cache/src/services/view-syncer/view-syncer.js.map +1 -1
  86. package/out/zero-cache/src/workers/connection.js +2 -2
  87. package/out/zero-cache/src/workers/connection.js.map +1 -1
  88. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts +1 -1
  89. package/out/zero-cache/src/workers/syncer-ws-message-handler.d.ts.map +1 -1
  90. package/out/zero-cache/src/workers/syncer-ws-message-handler.js +7 -7
  91. package/out/zero-cache/src/workers/syncer-ws-message-handler.js.map +1 -1
  92. package/out/zero-cache/src/workers/syncer.d.ts +1 -1
  93. package/out/zero-cache/src/workers/syncer.d.ts.map +1 -1
  94. package/out/zero-cache/src/workers/syncer.js +11 -10
  95. package/out/zero-cache/src/workers/syncer.js.map +1 -1
  96. package/out/zero-client/src/client/connection.d.ts +15 -7
  97. package/out/zero-client/src/client/connection.d.ts.map +1 -1
  98. package/out/zero-client/src/client/connection.js.map +1 -1
  99. package/out/zero-client/src/client/crud-impl.d.ts +1 -1
  100. package/out/zero-client/src/client/crud-impl.d.ts.map +1 -1
  101. package/out/zero-client/src/client/crud-impl.js +1 -1
  102. package/out/zero-client/src/client/crud-impl.js.map +1 -1
  103. package/out/zero-client/src/client/crud.d.ts +1 -1
  104. package/out/zero-client/src/client/crud.d.ts.map +1 -1
  105. package/out/zero-client/src/client/crud.js +1 -1
  106. package/out/zero-client/src/client/crud.js.map +1 -1
  107. package/out/zero-client/src/client/keys.d.ts +1 -1
  108. package/out/zero-client/src/client/keys.d.ts.map +1 -1
  109. package/out/zero-client/src/client/keys.js.map +1 -1
  110. package/out/zero-client/src/client/make-replicache-mutators.js +1 -1
  111. package/out/zero-client/src/client/make-replicache-mutators.js.map +1 -1
  112. package/out/zero-client/src/client/mutation-tracker.d.ts +2 -1
  113. package/out/zero-client/src/client/mutation-tracker.d.ts.map +1 -1
  114. package/out/zero-client/src/client/mutation-tracker.js +3 -3
  115. package/out/zero-client/src/client/mutation-tracker.js.map +1 -1
  116. package/out/zero-client/src/client/version.js +1 -1
  117. package/out/zero-client/src/client/zero.d.ts.map +1 -1
  118. package/out/zero-client/src/client/zero.js +2 -2
  119. package/out/zero-client/src/client/zero.js.map +1 -1
  120. package/out/zero-client/src/types/client-state.d.ts +1 -1
  121. package/out/zero-client/src/types/client-state.d.ts.map +1 -1
  122. package/out/zero-protocol/src/custom-queries.js +1 -1
  123. package/out/zero-protocol/src/down.js +1 -1
  124. package/out/zero-protocol/src/error-kind-enum.d.ts +1 -2
  125. package/out/zero-protocol/src/error-kind-enum.d.ts.map +1 -1
  126. package/out/zero-protocol/src/error-kind-enum.js.map +1 -1
  127. package/out/zero-protocol/src/mutate-server.d.ts +165 -0
  128. package/out/zero-protocol/src/mutate-server.d.ts.map +1 -0
  129. package/out/zero-protocol/src/mutate-server.js +24 -0
  130. package/out/zero-protocol/src/mutate-server.js.map +1 -0
  131. package/out/zero-protocol/src/mutation.d.ts +229 -0
  132. package/out/zero-protocol/src/mutation.d.ts.map +1 -0
  133. package/out/zero-protocol/src/mutation.js +112 -0
  134. package/out/zero-protocol/src/mutation.js.map +1 -0
  135. package/out/zero-protocol/src/mutations-patch.js +1 -1
  136. package/out/zero-protocol/src/mutations-patch.js.map +1 -1
  137. package/out/zero-protocol/src/push.d.ts +3 -234
  138. package/out/zero-protocol/src/push.d.ts.map +1 -1
  139. package/out/zero-protocol/src/push.js +3 -114
  140. package/out/zero-protocol/src/push.js.map +1 -1
  141. package/out/zero-protocol/src/query-server.d.ts +150 -0
  142. package/out/zero-protocol/src/query-server.d.ts.map +1 -0
  143. package/out/zero-protocol/src/query-server.js +16 -0
  144. package/out/zero-protocol/src/query-server.js.map +1 -0
  145. package/out/zero-protocol/src/up.js +1 -1
  146. package/out/zero-server/src/mod.d.ts +4 -2
  147. package/out/zero-server/src/mod.d.ts.map +1 -1
  148. package/out/zero-server/src/process-mutations.d.ts +50 -4
  149. package/out/zero-server/src/process-mutations.d.ts.map +1 -1
  150. package/out/zero-server/src/process-mutations.js +73 -36
  151. package/out/zero-server/src/process-mutations.js.map +1 -1
  152. package/out/zero-server/src/push-processor.d.ts +3 -3
  153. package/out/zero-server/src/push-processor.d.ts.map +1 -1
  154. package/out/zero-server/src/push-processor.js.map +1 -1
  155. package/out/zero-server/src/queries/process-queries.d.ts +45 -53
  156. package/out/zero-server/src/queries/process-queries.d.ts.map +1 -1
  157. package/out/zero-server/src/queries/process-queries.js +72 -53
  158. package/out/zero-server/src/queries/process-queries.js.map +1 -1
  159. package/out/zero-server/src/zql-database.js.map +1 -1
  160. package/out/zero-types/src/default-types.d.ts +1 -0
  161. package/out/zero-types/src/default-types.d.ts.map +1 -1
  162. package/out/zql/src/builder/builder.d.ts.map +1 -1
  163. package/out/zql/src/builder/builder.js +17 -7
  164. package/out/zql/src/builder/builder.js.map +1 -1
  165. package/out/zql/src/ivm/cap.d.ts +32 -0
  166. package/out/zql/src/ivm/cap.d.ts.map +1 -0
  167. package/out/zql/src/ivm/cap.js +205 -0
  168. package/out/zql/src/ivm/cap.js.map +1 -0
  169. package/out/zql/src/ivm/constraint.js +1 -1
  170. package/out/zql/src/ivm/flipped-join.d.ts.map +1 -1
  171. package/out/zql/src/ivm/flipped-join.js +61 -15
  172. package/out/zql/src/ivm/flipped-join.js.map +1 -1
  173. package/out/zql/src/ivm/memory-source.d.ts.map +1 -1
  174. package/out/zql/src/ivm/memory-source.js +3 -4
  175. package/out/zql/src/ivm/memory-source.js.map +1 -1
  176. package/out/zql/src/ivm/schema.d.ts +8 -0
  177. package/out/zql/src/ivm/schema.d.ts.map +1 -1
  178. package/out/zql/src/ivm/take.js +2 -2
  179. package/out/zql/src/mutate/mutator-registry.js.map +1 -1
  180. package/out/zql/src/mutate/mutator.d.ts +11 -2
  181. package/out/zql/src/mutate/mutator.d.ts.map +1 -1
  182. package/out/zql/src/mutate/mutator.js.map +1 -1
  183. package/out/zql/src/query/query-registry.d.ts +9 -2
  184. package/out/zql/src/query/query-registry.d.ts.map +1 -1
  185. package/out/zql/src/query/query-registry.js.map +1 -1
  186. package/out/zqlite/src/table-source.d.ts.map +1 -1
  187. package/out/zqlite/src/table-source.js +4 -1
  188. package/out/zqlite/src/table-source.js.map +1 -1
  189. package/package.json +1 -1
@@ -9,66 +9,64 @@ var ddlEventSchema = valita_exports.object({ context: valita_exports.object({ qu
9
9
  schema: publishedSchema,
10
10
  event: valita_exports.object({ tag: valita_exports.string() })
11
11
  });
12
+ /**
13
+ * A {@link DdlStartEvent} message is emitted before every DDL event, containing
14
+ * the current `schema` and the command `tag`.
15
+ *
16
+ * In most cases, the `DdlStartEvent` itself will not be associated with a
17
+ * schema change, in which case `previousSchema` will be `null`. However, the
18
+ * message is still emitted, both for backwards compatibility and to provide
19
+ * the command `tag` context in case an immediately following `DdlStartEvent`
20
+ * tag is emitted with a schema change (which can happen when another event
21
+ * trigger results in a nested ddl statement).
22
+ *
23
+ * In such cases, the `previousSchema` and `schema` fields of the latter event
24
+ * are used to determine the necessary schema change operations (as they are
25
+ * with `ddlUpdate` and `schemaSnapshot` events), and the `tag` of the
26
+ * preceding start event indicates the command that precipitated the schema
27
+ * change (e.g. a CREATE vs ALTER) to determine whether a backfill is
28
+ * necessary.
29
+ */
12
30
  var ddlStartEventSchema = ddlEventSchema.extend({
13
31
  type: valita_exports.literal("ddlStart"),
32
+ previousSchema: publishedSchema.nullable().optional(),
14
33
  event: valita_exports.object({ tag: valita_exports.string() }).optional(() => ({ tag: "UNKNOWN" }))
15
34
  });
16
35
  /**
17
- * The {@link DdlUpdateEvent} contains an updated schema resulting from
18
- * a particular ddl event. The event type provides information
19
- * (i.e. constraints) on the difference from the schema of the preceding
20
- * {@link DdlStartEvent}.
21
- *
22
- * Note that in almost all cases (the exception being `CREATE` events),
23
- * it is possible that there is no relevant difference between the
24
- * ddl-start schema and the ddl-update schema, as many aspects of the
25
- * schema (e.g. column constraints) are not relevant to downstream
26
- * replication.
36
+ * A {@link DdlUpdateEvent} is emitted if there was a change in the schema.
37
+ * It always contains `previousSchema` and (current) `schema` fields, leaving
38
+ * it to the receiver to compute the necessary schema change operations.
27
39
  */
28
- var ddlUpdateEventSchema = ddlEventSchema.extend({ type: valita_exports.literal("ddlUpdate") });
40
+ var ddlUpdateEventSchema = ddlEventSchema.extend({
41
+ type: valita_exports.literal("ddlUpdate"),
42
+ previousSchema: publishedSchema.optional()
43
+ });
29
44
  /**
30
45
  * The `schemaSnapshot` message is a snapshot of a schema taken in response to
31
46
  * a `COMMENT ON PUBLICATION` command, which is a hook recognized by zero
32
- * to manually emit schema snapshots to support detection of schema changes
33
- * from `ALTER PUBLICATION` commands on supabase, which does not fire event
34
- * triggers for them (https://github.com/supabase/supautils/issues/123).
47
+ * to manually emit `previousSchema` and `schema` snapshots when a difference
48
+ * is detected. This is a workaround provided to support detection of schema
49
+ * changes from `ALTER PUBLICATION` commands on supabase, which does not fire
50
+ * event triggers for them (https://github.com/supabase/supautils/issues/123).
35
51
  *
36
- * The hook is exercised by bookmarking the publication change with
37
- * `COMMENT ON PUBLICATION` statements within e.g.
52
+ * The hook is exercised by trailing the publication change with a
53
+ * `COMMENT ON PUBLICATION` statement, e.g.
38
54
  *
39
55
  * ```sql
40
56
  * BEGIN;
41
- * COMMENT ON PUBLICATION my_publication IS 'whatever';
42
57
  * ALTER PUBLICATION my_publication ...;
43
58
  * COMMENT ON PUBLICATION my_publication IS 'whatever';
44
59
  * COMMIT;
45
60
  * ```
46
61
  *
47
- * The `change-source` will perform the diff between a `schemaSnapshot`
48
- * events and its preceding `schemaSnapshot` (or `ddlUpdate`) within the
49
- * transaction.
50
- *
51
- * In the case where event trigger support is missing, this results in
52
- * diffing the `schemaSnapshot`s before and after the `ALTER PUBLICATION`
53
- * statement, thus effecting the same logic that would have been exercised
54
- * between the `ddlStart` and `ddlEvent` events fired by a database with
55
- * fully functional event triggers.
56
- *
57
- * Note that if the same transaction is run on a database that *does*
58
- * support event triggers on `ALTER PUBLICATION` statements, the sequence
59
- * of emitted messages will be:
60
- *
61
- * * `schemaSnapshot`
62
- * * `ddlStart`
63
- * * `ddlUpdate`
64
- * * `schemaSnapshot`
65
- *
66
- * Since `schemaSnapshot` messages are diffed with the preceding
67
- * `schemaSnapshot` or `ddlUpdate` event (if any), there will be no schema
68
- * difference between the `ddlUpdate` and the second `schemaSnapshot`, and
69
- * thus the extra `COMMENT` statements will effectively be no-ops.
62
+ * Note that it is fine to invoke `COMMENT ON PUBLICATION` statements
63
+ * on a database that *does* support event triggers on
64
+ * `ALTER PUBLICATION` statements, as it will simply be a no-op.
70
65
  */
71
- var schemaSnapshotEventSchema = ddlEventSchema.extend({ type: valita_exports.literal("schemaSnapshot") });
66
+ var schemaSnapshotEventSchema = ddlEventSchema.extend({
67
+ type: valita_exports.literal("schemaSnapshot"),
68
+ previousSchema: publishedSchema.optional()
69
+ });
72
70
  var replicationEventSchema = valita_exports.union(ddlStartEventSchema, ddlUpdateEventSchema, schemaSnapshotEventSchema);
73
71
  function append(shardNum) {
74
72
  return (name) => id(name + "_" + String(shardNum));
@@ -108,47 +106,102 @@ END
108
106
  $$ LANGUAGE plpgsql;
109
107
 
110
108
 
111
- CREATE OR REPLACE FUNCTION ${schema}.notice_ignore(tag TEXT, target record)
109
+ CREATE OR REPLACE FUNCTION ${schema}.notice_ignore(reason TEXT, tag TEXT, target record)
112
110
  RETURNS void AS $$
113
111
  BEGIN
114
- RAISE NOTICE 'zero(%) ignoring % %', ${literal(shardNum)}, tag, row_to_json(target);
112
+ RAISE NOTICE '${appID}_${shardNum} ignoring % % %', reason, tag,
113
+ COALESCE(row_to_json(target)::text, '');
115
114
  END
116
115
  $$ LANGUAGE plpgsql;
117
116
 
118
117
 
119
- CREATE OR REPLACE FUNCTION ${schema}.schema_specs()
120
- RETURNS TEXT
118
+ -- Note: DROP and CREATE to upgrade from v20 to v21 because the
119
+ -- return type has changed. This can be simplified to CREATE OR REPLACE
120
+ -- once 1.5.0 is rollback safe.
121
+ DROP FUNCTION IF EXISTS ${schema}.schema_specs();
122
+ CREATE FUNCTION ${schema}.schema_specs()
123
+ RETURNS JSON
121
124
  STABLE
122
125
  AS $$
123
126
  ${publishedSchemaQuery(publications)}
124
127
  $$ LANGUAGE sql;
125
128
 
126
129
 
127
- CREATE OR REPLACE FUNCTION ${schema}.emit_ddl_start()
128
- RETURNS event_trigger AS $$
130
+ -- Stores the most recent published schema
131
+ CREATE TABLE IF NOT EXISTS ${schema}."publishedSchema" (
132
+ current JSON,
133
+ exists BOOL PRIMARY KEY DEFAULT true CHECK (exists)
134
+ );
135
+
136
+ INSERT INTO ${schema}."publishedSchema" (current) VALUES (${schema}.schema_specs())
137
+ ON CONFLICT (exists) DO
138
+ UPDATE SET current = excluded.current;
139
+
140
+
141
+ CREATE OR REPLACE FUNCTION ${schema}.update_schemas(event_type text, tag text, target record)
142
+ RETURNS void AS $$
129
143
  DECLARE
130
- schema_specs TEXT;
144
+ prev_schema_specs JSON;
145
+ schema_specs JSON;
131
146
  message TEXT;
132
147
  BEGIN
133
- -- serialize DDL statements to compute correct schema change diffs
134
- PERFORM pg_advisory_xact_lock(${DDL_SERIALIZATION_LOCK});
135
-
148
+ SELECT current FROM ${schema}."publishedSchema" INTO prev_schema_specs;
136
149
  SELECT ${schema}.schema_specs() INTO schema_specs;
150
+
151
+ IF prev_schema_specs::text != schema_specs::text THEN
152
+ UPDATE ${schema}."publishedSchema" SET current = schema_specs;
153
+ ELSIF event_type = 'ddlStart' THEN
154
+ -- ddlStart events are always be emitted to allow the zero-cache
155
+ -- to track the context of the current command tag in the face of
156
+ -- nested event triggers (e.g. start->start->end->end).
157
+ prev_schema_specs = NULL;
158
+ ELSIF event_type = 'ddlUpdate' THEN
159
+ -- TODO: fold 'schemaSnapshot' into this condition too (i.e. make it "ELSE")
160
+ -- when 1.5.0 is rollback safe. Until then, noop schemaSnapshots are sent
161
+ -- for compatibility with 1.0.0 ~ 1.4.0.
162
+ PERFORM ${schema}.notice_ignore('noop', tag, target);
163
+ RETURN;
164
+ END IF;
137
165
 
138
166
  SELECT json_build_object(
139
- 'type', 'ddlStart',
167
+ 'type', event_type,
140
168
  'version', 1,
141
- 'schema', schema_specs::json,
142
- 'event', json_build_object('tag', TG_TAG),
169
+ 'previousSchema', prev_schema_specs,
170
+ 'schema', schema_specs,
171
+ 'event', json_build_object('tag', tag),
143
172
  'context', ${schema}.get_trigger_context()
144
173
  ) INTO message;
145
174
 
146
- PERFORM pg_logical_emit_message(true, ${literal(`${appID}/${shardNum}`)}, message);
175
+ PERFORM pg_logical_emit_message(true, '${appID}/${shardNum}/ddl', message);
176
+
177
+ RAISE NOTICE 'Emitted ${appID}_${shardNum} % for % %', event_type, tag,
178
+ COALESCE(row_to_json(target)::text, '');
179
+ END
180
+ $$ LANGUAGE plpgsql;
181
+
182
+
183
+ -- Hook/workaround to manually trigger replication of schema changes on DBs
184
+ -- that do not support/allow event triggers.
185
+ CREATE OR REPLACE FUNCTION ${schema}.update_schemas()
186
+ RETURNS void AS $$
187
+ BEGIN
188
+ PERFORM ${schema}.update_schemas('schemaSnapshot', 'MANUAL', NULL);
189
+ END
190
+ $$ LANGUAGE plpgsql;
191
+
192
+
193
+ CREATE OR REPLACE FUNCTION ${schema}.emit_ddl_start()
194
+ RETURNS event_trigger AS $$
195
+ DECLARE
196
+ schema_specs JSON;
197
+ message TEXT;
198
+ BEGIN
199
+ -- serialize DDL statements to compute correct schema change diffs
200
+ PERFORM pg_advisory_xact_lock(${DDL_SERIALIZATION_LOCK});
201
+ PERFORM ${schema}.update_schemas('ddlStart', TG_TAG, NULL);
147
202
  END
148
203
  $$ LANGUAGE plpgsql;
149
204
 
150
- -- Delete legacy function (and dependent legacy triggers).
151
- DROP FUNCTION IF EXISTS ${schema}.emit_ddl_end(text) CASCADE;
152
205
 
153
206
  CREATE OR REPLACE FUNCTION ${schema}.emit_ddl_end()
154
207
  RETURNS event_trigger AS $$
@@ -156,11 +209,9 @@ DECLARE
156
209
  publications TEXT[];
157
210
  target RECORD;
158
211
  relevant RECORD;
159
- schema_specs TEXT;
212
+ schema_specs JSON;
160
213
  message TEXT;
161
214
  event TEXT;
162
- event_type TEXT;
163
- event_prefix TEXT;
164
215
  BEGIN
165
216
  publications := ARRAY[${literal(publications)}];
166
217
 
@@ -220,36 +271,21 @@ BEGIN
220
271
  END IF;
221
272
 
222
273
  IF relevant IS NULL THEN
223
- PERFORM ${schema}.notice_ignore(TG_TAG, target);
274
+ PERFORM ${schema}.notice_ignore('irrelevant', TG_TAG, target);
224
275
  RETURN;
225
276
  END IF;
226
277
 
227
278
  IF TG_TAG = 'COMMENT' THEN
228
279
  -- Only make schemaSnapshots for COMMENT ON PUBLICATION
229
280
  IF target.object_type != 'publication' THEN
230
- PERFORM ${schema}.notice_ignore(TG_TAG, target);
281
+ PERFORM ${schema}.notice_ignore('irrelevant', TG_TAG, target);
231
282
  RETURN;
232
283
  END IF;
233
- event_type := 'schemaSnapshot';
234
- event_prefix := '/ddl';
284
+ PERFORM ${schema}.update_schemas('schemaSnapshot', TG_TAG, target);
235
285
  ELSE
236
- event_type := 'ddlUpdate';
237
- event_prefix := ''; -- TODO: Use '/ddl' for both when rollback safe
286
+ PERFORM ${schema}.update_schemas('ddlUpdate', TG_TAG, target);
238
287
  END IF;
239
288
 
240
- RAISE INFO 'Creating % for % %', event_type, TG_TAG, row_to_json(target);
241
-
242
- SELECT ${schema}.schema_specs() INTO schema_specs;
243
-
244
- SELECT json_build_object(
245
- 'type', event_type,
246
- 'version', 1,
247
- 'schema', schema_specs::json,
248
- 'event', json_build_object('tag', TG_TAG),
249
- 'context', ${schema}.get_trigger_context()
250
- ) INTO message;
251
-
252
- PERFORM pg_logical_emit_message(true, ${literal(`${appID}/${shardNum}`)} || event_prefix, message);
253
289
  END
254
290
  $$ LANGUAGE plpgsql;
255
291
  `;
@@ -268,8 +304,7 @@ function createEventTriggerStatements(shard) {
268
304
  const { appID, shardNum } = shard;
269
305
  const sharded = append(shardNum);
270
306
  const schema = id(upstreamSchema(shard));
271
- const triggers = [dropEventTriggerStatements(shard.appID, shard.shardNum), createEventFunctionStatements(shard)];
272
- triggers.push(`
307
+ const triggers = [dropEventTriggerStatements(shard.appID, shard.shardNum), `
273
308
  CREATE EVENT TRIGGER ${sharded(`${appID}_ddl_start`)}
274
309
  ON ddl_command_start
275
310
  WHEN TAG IN (${literal(TAGS)})
@@ -279,7 +314,8 @@ CREATE EVENT TRIGGER ${sharded(`${appID}_ddl_end`)}
279
314
  ON ddl_command_end
280
315
  WHEN TAG IN (${literal([...TAGS, "COMMENT"])})
281
316
  EXECUTE PROCEDURE ${schema}.emit_ddl_end();
282
- `);
317
+ `];
318
+ triggers.push(`DROP FUNCTION IF EXISTS ${schema}.emit_ddl_end(text) CASCADE;`, `DROP FUNCTION IF EXISTS ${schema}.notice_ignore(text, record);`);
283
319
  for (const tag of [...TAGS, "COMMENT"]) {
284
320
  const tagID = tag.toLowerCase().replace(" ", "_");
285
321
  triggers.push(`DROP FUNCTION IF EXISTS ${schema}.emit_${tagID}() CASCADE;`);
@@ -293,6 +329,6 @@ function dropEventTriggerStatements(appID, shardID) {
293
329
  `;
294
330
  }
295
331
  //#endregion
296
- export { createEventTriggerStatements, replicationEventSchema };
332
+ export { createEventFunctionStatements, createEventTriggerStatements, replicationEventSchema };
297
333
 
298
334
  //# sourceMappingURL=ddl.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ddl.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/ddl.ts"],"sourcesContent":["import {literal as lit} from 'pg-format';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {upstreamSchema, type ShardConfig} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {publishedSchema, publishedSchemaQuery} from './published.ts';\n\n// Sent in the 'version' tag of \"ddlStart\" and \"ddlUpdate\" event messages.\n// This is used to ensure that the message constructed in the upstream\n// Trigger function is compatible with the code processing it in the zero-cache.\n//\n// Increment this when changing the format of the contents of the \"ddl\" events.\n// This will allow old / incompatible code to detect the change and abort.\nexport const PROTOCOL_VERSION = 1;\n\nconst triggerEvent = v.object({\n context: v.object({query: v.string()}).rest(v.string()),\n});\n\n// All DDL events contain a snapshot of the current tables and indexes that\n// are published / relevant to the shard.\nexport const ddlEventSchema = triggerEvent.extend({\n version: v.literal(PROTOCOL_VERSION),\n schema: publishedSchema,\n event: v.object({tag: v.string()}),\n});\n\n// The `ddlStart` message is computed before every DDL event, regardless of\n// whether the subsequent event affects the shard. Downstream processing should\n// capture the contained schema information in order to determine the schema\n// changes necessary to apply a subsequent `ddlUpdate` message. Note that a\n// `ddlUpdate` message may not follow, as updates determined to be irrelevant\n// to the shard will not result in a message. However, all `ddlUpdate` messages\n// are guaranteed to be preceded by a `ddlStart` message.\nexport const ddlStartEventSchema = ddlEventSchema.extend({\n type: v.literal('ddlStart'),\n // For backwards compatibility with previous versions of the trigger,\n // default an absent `event` field with a semantic equivalent. This\n // field override can be removed in a version that is rollback safe\n // with 1.4.0.\n event: v.object({tag: v.string()}).optional(() => ({tag: 'UNKNOWN'})),\n});\n\nexport type DdlStartEvent = v.Infer<typeof ddlStartEventSchema>;\n\n/**\n * The {@link DdlUpdateEvent} contains an updated schema resulting from\n * a particular ddl event. The event type provides information\n * (i.e. constraints) on the difference from the schema of the preceding\n * {@link DdlStartEvent}.\n *\n * Note that in almost all cases (the exception being `CREATE` events),\n * it is possible that there is no relevant difference between the\n * ddl-start schema and the ddl-update schema, as many aspects of the\n * schema (e.g. column constraints) are not relevant to downstream\n * replication.\n */\nexport const ddlUpdateEventSchema = ddlEventSchema.extend({\n type: v.literal('ddlUpdate'),\n});\n\nexport type DdlUpdateEvent = v.Infer<typeof ddlUpdateEventSchema>;\n\n/**\n * The `schemaSnapshot` message is a snapshot of a schema taken in response to\n * a `COMMENT ON PUBLICATION` command, which is a hook recognized by zero\n * to manually emit schema snapshots to support detection of schema changes\n * from `ALTER PUBLICATION` commands on supabase, which does not fire event\n * triggers for them (https://github.com/supabase/supautils/issues/123).\n *\n * The hook is exercised by bookmarking the publication change with\n * `COMMENT ON PUBLICATION` statements within e.g.\n *\n * ```sql\n * BEGIN;\n * COMMENT ON PUBLICATION my_publication IS 'whatever';\n * ALTER PUBLICATION my_publication ...;\n * COMMENT ON PUBLICATION my_publication IS 'whatever';\n * COMMIT;\n * ```\n *\n * The `change-source` will perform the diff between a `schemaSnapshot`\n * events and its preceding `schemaSnapshot` (or `ddlUpdate`) within the\n * transaction.\n *\n * In the case where event trigger support is missing, this results in\n * diffing the `schemaSnapshot`s before and after the `ALTER PUBLICATION`\n * statement, thus effecting the same logic that would have been exercised\n * between the `ddlStart` and `ddlEvent` events fired by a database with\n * fully functional event triggers.\n *\n * Note that if the same transaction is run on a database that *does*\n * support event triggers on `ALTER PUBLICATION` statements, the sequence\n * of emitted messages will be:\n *\n * * `schemaSnapshot`\n * * `ddlStart`\n * * `ddlUpdate`\n * * `schemaSnapshot`\n *\n * Since `schemaSnapshot` messages are diffed with the preceding\n * `schemaSnapshot` or `ddlUpdate` event (if any), there will be no schema\n * difference between the `ddlUpdate` and the second `schemaSnapshot`, and\n * thus the extra `COMMENT` statements will effectively be no-ops.\n */\nexport const schemaSnapshotEventSchema = ddlEventSchema.extend({\n type: v.literal('schemaSnapshot'),\n});\n\nexport type SchemaSnapshotEvent = v.Infer<typeof schemaSnapshotEventSchema>;\n\nexport const replicationEventSchema = v.union(\n ddlStartEventSchema,\n ddlUpdateEventSchema,\n schemaSnapshotEventSchema,\n);\n\nexport type ReplicationEvent = v.Infer<typeof replicationEventSchema>;\n\n// Creates a function that appends `_{shard-num}` to the input and\n// quotes the result to be a valid identifier.\nfunction append(shardNum: number) {\n return (name: string) => id(name + '_' + String(shardNum));\n}\n\n// pg_advisory_xact_lock key for serializing ddl statements in order to\n// produce correct schema change diffs.\nconst DDL_SERIALIZATION_LOCK = 0x3c6b8468f1bac0b0n;\n\n/**\n * Event trigger functions contain the core logic that are invoked by triggers.\n *\n * Note that although many of these functions can theoretically be parameterized and\n * shared across shards, it is advantageous to keep the functions in each shard\n * isolated from each other in order to avoid the complexity of shared-function\n * versioning.\n *\n * In a sense, shards (and their triggers and functions) should be thought of as\n * execution environments that can be updated at different schedules. If per-shard\n * triggers called into shared functions, we would have to consider versioning the\n * functions when changing their behavior, backwards compatibility, removal of\n * unused versions, etc. (not unlike versioning of npm packages).\n *\n * Instead, we opt for the simplicity and isolation of having each shard\n * completely own (and maintain) the entirety of its trigger/function stack.\n */\nfunction createEventFunctionStatements(shard: ShardConfig) {\n const {appID, shardNum, publications} = shard;\n const schema = id(upstreamSchema(shard)); // e.g. \"{APP_ID}_{SHARD_ID}\"\n return /*sql*/ `\nCREATE SCHEMA IF NOT EXISTS ${schema};\n\nCREATE OR REPLACE FUNCTION ${schema}.get_trigger_context()\nRETURNS record AS $$\nDECLARE\n result record;\nBEGIN\n SELECT current_query() AS \"query\" into result;\n RETURN result;\nEND\n$$ LANGUAGE plpgsql;\n\n\nCREATE OR REPLACE FUNCTION ${schema}.notice_ignore(tag TEXT, target record)\nRETURNS void AS $$\nBEGIN\n RAISE NOTICE 'zero(%) ignoring % %', ${lit(shardNum)}, tag, row_to_json(target);\nEND\n$$ LANGUAGE plpgsql;\n\n\nCREATE OR REPLACE FUNCTION ${schema}.schema_specs()\nRETURNS TEXT \nSTABLE\nAS $$\n ${publishedSchemaQuery(publications)}\n$$ LANGUAGE sql;\n\n\nCREATE OR REPLACE FUNCTION ${schema}.emit_ddl_start()\nRETURNS event_trigger AS $$\nDECLARE\n schema_specs TEXT;\n message TEXT;\nBEGIN\n -- serialize DDL statements to compute correct schema change diffs\n PERFORM pg_advisory_xact_lock(${DDL_SERIALIZATION_LOCK});\n\n SELECT ${schema}.schema_specs() INTO schema_specs;\n\n SELECT json_build_object(\n 'type', 'ddlStart',\n 'version', ${PROTOCOL_VERSION},\n 'schema', schema_specs::json,\n 'event', json_build_object('tag', TG_TAG),\n 'context', ${schema}.get_trigger_context()\n ) INTO message;\n\n PERFORM pg_logical_emit_message(true, ${lit(\n `${appID}/${shardNum}`,\n )}, message);\nEND\n$$ LANGUAGE plpgsql;\n\n-- Delete legacy function (and dependent legacy triggers).\nDROP FUNCTION IF EXISTS ${schema}.emit_ddl_end(text) CASCADE;\n\nCREATE OR REPLACE FUNCTION ${schema}.emit_ddl_end()\nRETURNS event_trigger AS $$\nDECLARE\n publications TEXT[];\n target RECORD;\n relevant RECORD;\n schema_specs TEXT;\n message TEXT;\n event TEXT;\n event_type TEXT;\n event_prefix TEXT;\nBEGIN\n publications := ARRAY[${lit(publications)}];\n\n SELECT objid, object_type, object_identity \n FROM pg_event_trigger_ddl_commands() \n LIMIT 1 INTO target;\n\n -- Filter DDL updates that are not relevant to the shard (i.e. publications) when possible.\n SELECT true INTO relevant;\n\n -- Note: ALTER TABLE statements may *remove* the table from the set of published\n -- tables, and there is no way to determine if the table \"used to be\" in the\n -- set. Thus, all ALTER TABLE statements must produce a ddl update, similar to\n -- any DROP * statement.\n IF (target.object_type = 'table' AND TG_TAG != 'ALTER TABLE') \n OR target.object_type = 'table column' THEN\n SELECT ns.nspname AS \"schema\", c.relname AS \"name\" FROM pg_class AS c\n JOIN pg_namespace AS ns ON c.relnamespace = ns.oid\n JOIN pg_publication_tables AS pb ON pb.schemaname = ns.nspname AND pb.tablename = c.relname\n WHERE c.oid = target.objid AND pb.pubname = ANY (publications)\n INTO relevant;\n\n ELSIF target.object_type = 'index' THEN\n SELECT ns.nspname AS \"schema\", c.relname AS \"name\" FROM pg_class AS c\n JOIN pg_namespace AS ns ON c.relnamespace = ns.oid\n JOIN pg_indexes as ind ON ind.schemaname = ns.nspname AND ind.indexname = c.relname\n JOIN pg_publication_tables AS pb ON pb.schemaname = ns.nspname AND pb.tablename = ind.tablename\n WHERE c.oid = target.objid AND pb.pubname = ANY (publications)\n INTO relevant;\n\n ELSIF target.object_type = 'publication relation' THEN\n SELECT pb.pubname FROM pg_publication_rel AS rel\n JOIN pg_publication AS pb ON pb.oid = rel.prpubid\n WHERE rel.oid = target.objid AND pb.pubname = ANY (publications) \n INTO relevant;\n\n ELSIF target.object_type = 'publication namespace' THEN\n SELECT pb.pubname FROM pg_publication_namespace AS ns\n JOIN pg_publication AS pb ON pb.oid = ns.pnpubid\n WHERE ns.oid = target.objid AND pb.pubname = ANY (publications) \n INTO relevant;\n\n ELSIF target.object_type = 'schema' THEN\n SELECT ns.nspname AS \"schema\", c.relname AS \"name\" FROM pg_class AS c\n JOIN pg_namespace AS ns ON c.relnamespace = ns.oid\n JOIN pg_publication_tables AS pb ON pb.schemaname = ns.nspname AND pb.tablename = c.relname\n WHERE ns.oid = target.objid AND pb.pubname = ANY (publications)\n INTO relevant;\n\n ELSIF target.object_type = 'publication' THEN\n SELECT 1 WHERE target.object_identity = ANY (publications)\n INTO relevant;\n\n -- no-op CREATE IF NOT EXIST statements\n ELSIF TG_TAG LIKE 'CREATE %' AND target.object_type IS NULL THEN\n relevant := NULL;\n END IF;\n\n IF relevant IS NULL THEN\n PERFORM ${schema}.notice_ignore(TG_TAG, target);\n RETURN;\n END IF;\n\n IF TG_TAG = 'COMMENT' THEN\n -- Only make schemaSnapshots for COMMENT ON PUBLICATION\n IF target.object_type != 'publication' THEN\n PERFORM ${schema}.notice_ignore(TG_TAG, target);\n RETURN;\n END IF;\n event_type := 'schemaSnapshot';\n event_prefix := '/ddl';\n ELSE\n event_type := 'ddlUpdate';\n event_prefix := ''; -- TODO: Use '/ddl' for both when rollback safe\n END IF;\n\n RAISE INFO 'Creating % for % %', event_type, TG_TAG, row_to_json(target);\n\n SELECT ${schema}.schema_specs() INTO schema_specs;\n\n SELECT json_build_object(\n 'type', event_type,\n 'version', ${PROTOCOL_VERSION},\n 'schema', schema_specs::json,\n 'event', json_build_object('tag', TG_TAG),\n 'context', ${schema}.get_trigger_context()\n ) INTO message;\n\n PERFORM pg_logical_emit_message(true, ${lit(\n `${appID}/${shardNum}`,\n )} || event_prefix, message);\nEND\n$$ LANGUAGE plpgsql;\n`;\n}\n\n// Exported for testing.\nexport const TAGS = [\n 'CREATE TABLE',\n 'ALTER TABLE',\n 'CREATE INDEX',\n 'DROP TABLE',\n 'DROP INDEX',\n 'ALTER PUBLICATION',\n 'ALTER SCHEMA',\n] as const;\n\nexport function createEventTriggerStatements(shard: ShardConfig) {\n // Better to assert here than get a cryptic syntax error from Postgres.\n assert(shard.publications.length, `shard publications must be non-empty`);\n\n // Unlike functions, which are namespaced in shard-specific schemas,\n // EVENT TRIGGER names are in the global namespace and thus must include\n // the appID and shardNum.\n const {appID, shardNum} = shard;\n const sharded = append(shardNum);\n const schema = id(upstreamSchema(shard));\n\n const triggers = [\n dropEventTriggerStatements(shard.appID, shard.shardNum),\n createEventFunctionStatements(shard),\n ];\n\n // A single ddl_command_start trigger covering all relevant tags.\n triggers.push(/*sql*/ `\nCREATE EVENT TRIGGER ${sharded(`${appID}_ddl_start`)}\n ON ddl_command_start\n WHEN TAG IN (${lit(TAGS)})\n EXECUTE PROCEDURE ${schema}.emit_ddl_start();\n\nCREATE EVENT TRIGGER ${sharded(`${appID}_ddl_end`)}\n ON ddl_command_end\n WHEN TAG IN (${lit([...TAGS, 'COMMENT'])})\n EXECUTE PROCEDURE ${schema}.emit_ddl_end();\n`);\n\n // Drop legacy functions / triggers.\n for (const tag of [...TAGS, 'COMMENT']) {\n const tagID = tag.toLowerCase().replace(' ', '_');\n triggers.push(`DROP FUNCTION IF EXISTS ${schema}.emit_${tagID}() CASCADE;`);\n }\n return triggers.join('');\n}\n\n// Exported for testing.\nexport function dropEventTriggerStatements(\n appID: string,\n shardID: string | number,\n) {\n return /*sql*/ `\n DROP EVENT TRIGGER IF EXISTS ${id(`${appID}_ddl_start_${shardID}`)};\n DROP EVENT TRIGGER IF EXISTS ${id(`${appID}_ddl_end_${shardID}`)};\n `;\n}\n"],"mappings":";;;;;;AAqBA,IAAa,iBANQ,eAAE,OAAO,EAC5B,SAAS,eAAE,OAAO,EAAC,OAAO,eAAE,QAAQ,EAAC,CAAC,CAAC,KAAK,eAAE,QAAQ,CAAC,EACxD,CAAC,CAIyC,OAAO;CAChD,SAAS,eAAE,QAAA,EAAyB;CACpC,QAAQ;CACR,OAAO,eAAE,OAAO,EAAC,KAAK,eAAE,QAAQ,EAAC,CAAC;CACnC,CAAC;AASF,IAAa,sBAAsB,eAAe,OAAO;CACvD,MAAM,eAAE,QAAQ,WAAW;CAK3B,OAAO,eAAE,OAAO,EAAC,KAAK,eAAE,QAAQ,EAAC,CAAC,CAAC,gBAAgB,EAAC,KAAK,WAAU,EAAE;CACtE,CAAC;;;;;;;;;;;;;AAgBF,IAAa,uBAAuB,eAAe,OAAO,EACxD,MAAM,eAAE,QAAQ,YAAY,EAC7B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CF,IAAa,4BAA4B,eAAe,OAAO,EAC7D,MAAM,eAAE,QAAQ,iBAAiB,EAClC,CAAC;AAIF,IAAa,yBAAyB,eAAE,MACtC,qBACA,sBACA,0BACD;AAMD,SAAS,OAAO,UAAkB;AAChC,SAAQ,SAAiB,GAAG,OAAO,MAAM,OAAO,SAAS,CAAC;;AAK5D,IAAM,yBAAyB;;;;;;;;;;;;;;;;;;AAmB/B,SAAS,8BAA8B,OAAoB;CACzD,MAAM,EAAC,OAAO,UAAU,iBAAgB;CACxC,MAAM,SAAS,GAAG,eAAe,MAAM,CAAC;AACxC,QAAe;8BACa,OAAO;;6BAER,OAAO;;;;;;;;;;;6BAWP,OAAO;;;yCAGK,QAAI,SAAS,CAAC;;;;;6BAK1B,OAAO;;;;IAIhC,qBAAqB,aAAa,CAAC;;;;6BAIV,OAAO;;;;;;;kCAOF,uBAAuB;;WAE9C,OAAO;;;;;;;iBAOD,OAAO;;;0CAGkB,QACtC,GAAG,MAAM,GAAG,WACb,CAAC;;;;;0BAKsB,OAAO;;6BAEJ,OAAO;;;;;;;;;;;;0BAYV,QAAI,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA0D9B,OAAO;;;;;;;gBAOL,OAAO;;;;;;;;;;;;WAYZ,OAAO;;;;;;;iBAOD,OAAO;;;0CAGkB,QACtC,GAAG,MAAM,GAAG,WACb,CAAC;;;;;AAOJ,IAAa,OAAO;CAClB;CACA;CACA;CACA;CACA;CACA;CACA;CACD;AAED,SAAgB,6BAA6B,OAAoB;AAE/D,QAAO,MAAM,aAAa,QAAQ,uCAAuC;CAKzE,MAAM,EAAC,OAAO,aAAY;CAC1B,MAAM,UAAU,OAAO,SAAS;CAChC,MAAM,SAAS,GAAG,eAAe,MAAM,CAAC;CAExC,MAAM,WAAW,CACf,2BAA2B,MAAM,OAAO,MAAM,SAAS,EACvD,8BAA8B,MAAM,CACrC;AAGD,UAAS,KAAa;uBACD,QAAQ,GAAG,MAAM,YAAY,CAAC;;iBAEpC,QAAI,KAAK,CAAC;sBACL,OAAO;;uBAEN,QAAQ,GAAG,MAAM,UAAU,CAAC;;iBAElC,QAAI,CAAC,GAAG,MAAM,UAAU,CAAC,CAAC;sBACrB,OAAO;EAC3B;AAGA,MAAK,MAAM,OAAO,CAAC,GAAG,MAAM,UAAU,EAAE;EACtC,MAAM,QAAQ,IAAI,aAAa,CAAC,QAAQ,KAAK,IAAI;AACjD,WAAS,KAAK,2BAA2B,OAAO,QAAQ,MAAM,aAAa;;AAE7E,QAAO,SAAS,KAAK,GAAG;;AAI1B,SAAgB,2BACd,OACA,SACA;AACA,QAAe;mCACkB,GAAG,GAAG,MAAM,aAAa,UAAU,CAAC;mCACpC,GAAG,GAAG,MAAM,WAAW,UAAU,CAAC"}
1
+ {"version":3,"file":"ddl.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/ddl.ts"],"sourcesContent":["import {literal as lit} from 'pg-format';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {upstreamSchema, type ShardConfig} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {publishedSchema, publishedSchemaQuery} from './published.ts';\n\n// Sent in the 'version' tag of \"ddlStart\" and \"ddlUpdate\" event messages.\n// This is used to ensure that the message constructed in the upstream\n// Trigger function is compatible with the code processing it in the zero-cache.\n//\n// Increment this when changing the format of the contents of the \"ddl\" events.\n// This will allow old / incompatible code to detect the change and abort.\nexport const PROTOCOL_VERSION = 1;\n\nconst triggerEvent = v.object({\n context: v.object({query: v.string()}).rest(v.string()),\n});\n\n// All DDL events contain a snapshot of the current tables and indexes that\n// are published / relevant to the shard.\nexport const ddlEventSchema = triggerEvent.extend({\n version: v.literal(PROTOCOL_VERSION),\n schema: publishedSchema,\n event: v.object({tag: v.string()}),\n});\n\n/**\n * A {@link DdlStartEvent} message is emitted before every DDL event, containing\n * the current `schema` and the command `tag`.\n *\n * In most cases, the `DdlStartEvent` itself will not be associated with a\n * schema change, in which case `previousSchema` will be `null`. However, the\n * message is still emitted, both for backwards compatibility and to provide\n * the command `tag` context in case an immediately following `DdlStartEvent`\n * tag is emitted with a schema change (which can happen when another event\n * trigger results in a nested ddl statement).\n *\n * In such cases, the `previousSchema` and `schema` fields of the latter event\n * are used to determine the necessary schema change operations (as they are\n * with `ddlUpdate` and `schemaSnapshot` events), and the `tag` of the\n * preceding start event indicates the command that precipitated the schema\n * change (e.g. a CREATE vs ALTER) to determine whether a backfill is\n * necessary.\n */\nexport const ddlStartEventSchema = ddlEventSchema.extend({\n type: v.literal('ddlStart'),\n // For ddlStart messages, previousSchema is `null` if there was no change\n // in schema detected. The `schema` field is always set to the current\n // schema.\n //\n // In 1.5.0 it is always present, and can be made non-optional when\n // rollback safe.\n previousSchema: publishedSchema.nullable().optional(),\n // For backwards compatibility with previous versions of the trigger,\n // default an absent `event` field with a semantic equivalent. This\n // field override can be removed in a version that is rollback safe\n // with 1.4.0.\n event: v.object({tag: v.string()}).optional(() => ({tag: 'UNKNOWN'})),\n});\n\nexport type DdlStartEvent = v.Infer<typeof ddlStartEventSchema>;\n\n/**\n * A {@link DdlUpdateEvent} is emitted if there was a change in the schema.\n * It always contains `previousSchema` and (current) `schema` fields, leaving\n * it to the receiver to compute the necessary schema change operations.\n */\nexport const ddlUpdateEventSchema = ddlEventSchema.extend({\n type: v.literal('ddlUpdate'),\n // ddlUpdate messages are only emitted if the schema changed, with the\n // `previousSchema` containing the schema before the change.\n //\n // In 1.5.0 it is always set, and can be made non-optional when\n // rollback safe.\n previousSchema: publishedSchema.optional(),\n});\n\nexport type DdlUpdateEvent = v.Infer<typeof ddlUpdateEventSchema>;\n\n/**\n * The `schemaSnapshot` message is a snapshot of a schema taken in response to\n * a `COMMENT ON PUBLICATION` command, which is a hook recognized by zero\n * to manually emit `previousSchema` and `schema` snapshots when a difference\n * is detected. This is a workaround provided to support detection of schema\n * changes from `ALTER PUBLICATION` commands on supabase, which does not fire\n * event triggers for them (https://github.com/supabase/supautils/issues/123).\n *\n * The hook is exercised by trailing the publication change with a\n * `COMMENT ON PUBLICATION` statement, e.g.\n *\n * ```sql\n * BEGIN;\n * ALTER PUBLICATION my_publication ...;\n * COMMENT ON PUBLICATION my_publication IS 'whatever';\n * COMMIT;\n * ```\n *\n * Note that it is fine to invoke `COMMENT ON PUBLICATION` statements\n * on a database that *does* support event triggers on\n * `ALTER PUBLICATION` statements, as it will simply be a no-op.\n */\nexport const schemaSnapshotEventSchema = ddlEventSchema.extend({\n type: v.literal('schemaSnapshot'),\n previousSchema: publishedSchema.optional(),\n});\n\nexport type SchemaSnapshotEvent = v.Infer<typeof schemaSnapshotEventSchema>;\n\nexport const replicationEventSchema = v.union(\n ddlStartEventSchema,\n ddlUpdateEventSchema,\n schemaSnapshotEventSchema,\n);\n\nexport type ReplicationEvent = v.Infer<typeof replicationEventSchema>;\n\n// Creates a function that appends `_{shard-num}` to the input and\n// quotes the result to be a valid identifier.\nfunction append(shardNum: number) {\n return (name: string) => id(name + '_' + String(shardNum));\n}\n\n// pg_advisory_xact_lock key for serializing ddl statements in order to\n// produce correct schema change diffs.\nconst DDL_SERIALIZATION_LOCK = 0x3c6b8468f1bac0b0n;\n\n/**\n * Event trigger functions contain the core logic that are invoked by triggers.\n *\n * Note that although many of these functions can theoretically be parameterized and\n * shared across shards, it is advantageous to keep the functions in each shard\n * isolated from each other in order to avoid the complexity of shared-function\n * versioning.\n *\n * In a sense, shards (and their triggers and functions) should be thought of as\n * execution environments that can be updated at different schedules. If per-shard\n * triggers called into shared functions, we would have to consider versioning the\n * functions when changing their behavior, backwards compatibility, removal of\n * unused versions, etc. (not unlike versioning of npm packages).\n *\n * Instead, we opt for the simplicity and isolation of having each shard\n * completely own (and maintain) the entirety of its trigger/function stack.\n */\nexport function createEventFunctionStatements(shard: ShardConfig) {\n const {appID, shardNum, publications} = shard;\n const schema = id(upstreamSchema(shard)); // e.g. \"{APP_ID}_{SHARD_ID}\"\n return /*sql*/ `\nCREATE SCHEMA IF NOT EXISTS ${schema};\n\nCREATE OR REPLACE FUNCTION ${schema}.get_trigger_context()\nRETURNS record AS $$\nDECLARE\n result record;\nBEGIN\n SELECT current_query() AS \"query\" into result;\n RETURN result;\nEND\n$$ LANGUAGE plpgsql;\n\n\nCREATE OR REPLACE FUNCTION ${schema}.notice_ignore(reason TEXT, tag TEXT, target record)\nRETURNS void AS $$\nBEGIN\n RAISE NOTICE '${appID}_${shardNum} ignoring % % %', reason, tag, \n COALESCE(row_to_json(target)::text, '');\nEND\n$$ LANGUAGE plpgsql;\n\n\n-- Note: DROP and CREATE to upgrade from v20 to v21 because the\n-- return type has changed. This can be simplified to CREATE OR REPLACE\n-- once 1.5.0 is rollback safe.\nDROP FUNCTION IF EXISTS ${schema}.schema_specs();\nCREATE FUNCTION ${schema}.schema_specs()\nRETURNS JSON \nSTABLE\nAS $$\n ${publishedSchemaQuery(publications)}\n$$ LANGUAGE sql;\n\n\n-- Stores the most recent published schema\nCREATE TABLE IF NOT EXISTS ${schema}.\"publishedSchema\" (\n current JSON,\n exists BOOL PRIMARY KEY DEFAULT true CHECK (exists)\n);\n\nINSERT INTO ${schema}.\"publishedSchema\" (current) VALUES (${schema}.schema_specs())\n ON CONFLICT (exists) DO \n UPDATE SET current = excluded.current;\n\n\nCREATE OR REPLACE FUNCTION ${schema}.update_schemas(event_type text, tag text, target record)\nRETURNS void AS $$\nDECLARE\n prev_schema_specs JSON;\n schema_specs JSON;\n message TEXT;\nBEGIN\n SELECT current FROM ${schema}.\"publishedSchema\" INTO prev_schema_specs;\n SELECT ${schema}.schema_specs() INTO schema_specs;\n \n IF prev_schema_specs::text != schema_specs::text THEN\n UPDATE ${schema}.\"publishedSchema\" SET current = schema_specs;\n ELSIF event_type = 'ddlStart' THEN\n -- ddlStart events are always be emitted to allow the zero-cache\n -- to track the context of the current command tag in the face of\n -- nested event triggers (e.g. start->start->end->end).\n prev_schema_specs = NULL;\n ELSIF event_type = 'ddlUpdate' THEN\n -- TODO: fold 'schemaSnapshot' into this condition too (i.e. make it \"ELSE\")\n -- when 1.5.0 is rollback safe. Until then, noop schemaSnapshots are sent\n -- for compatibility with 1.0.0 ~ 1.4.0.\n PERFORM ${schema}.notice_ignore('noop', tag, target);\n RETURN;\n END IF;\n\n SELECT json_build_object(\n 'type', event_type,\n 'version', ${PROTOCOL_VERSION},\n 'previousSchema', prev_schema_specs,\n 'schema', schema_specs,\n 'event', json_build_object('tag', tag),\n 'context', ${schema}.get_trigger_context()\n ) INTO message;\n\n PERFORM pg_logical_emit_message(true, '${appID}/${shardNum}/ddl', message);\n\n RAISE NOTICE 'Emitted ${appID}_${shardNum} % for % %', event_type, tag, \n COALESCE(row_to_json(target)::text, '');\nEND\n$$ LANGUAGE plpgsql;\n\n\n-- Hook/workaround to manually trigger replication of schema changes on DBs \n-- that do not support/allow event triggers.\nCREATE OR REPLACE FUNCTION ${schema}.update_schemas()\nRETURNS void AS $$\nBEGIN\n PERFORM ${schema}.update_schemas('schemaSnapshot', 'MANUAL', NULL);\nEND\n$$ LANGUAGE plpgsql;\n\n\nCREATE OR REPLACE FUNCTION ${schema}.emit_ddl_start()\nRETURNS event_trigger AS $$\nDECLARE\n schema_specs JSON;\n message TEXT;\nBEGIN\n -- serialize DDL statements to compute correct schema change diffs\n PERFORM pg_advisory_xact_lock(${DDL_SERIALIZATION_LOCK});\n PERFORM ${schema}.update_schemas('ddlStart', TG_TAG, NULL);\nEND\n$$ LANGUAGE plpgsql;\n\n\nCREATE OR REPLACE FUNCTION ${schema}.emit_ddl_end()\nRETURNS event_trigger AS $$\nDECLARE\n publications TEXT[];\n target RECORD;\n relevant RECORD;\n schema_specs JSON;\n message TEXT;\n event TEXT;\nBEGIN\n publications := ARRAY[${lit(publications)}];\n\n SELECT objid, object_type, object_identity \n FROM pg_event_trigger_ddl_commands() \n LIMIT 1 INTO target;\n\n -- Filter DDL updates that are not relevant to the shard (i.e. publications) when possible.\n SELECT true INTO relevant;\n\n -- Note: ALTER TABLE statements may *remove* the table from the set of published\n -- tables, and there is no way to determine if the table \"used to be\" in the\n -- set. Thus, all ALTER TABLE statements must produce a ddl update, similar to\n -- any DROP * statement.\n IF (target.object_type = 'table' AND TG_TAG != 'ALTER TABLE') \n OR target.object_type = 'table column' THEN\n SELECT ns.nspname AS \"schema\", c.relname AS \"name\" FROM pg_class AS c\n JOIN pg_namespace AS ns ON c.relnamespace = ns.oid\n JOIN pg_publication_tables AS pb ON pb.schemaname = ns.nspname AND pb.tablename = c.relname\n WHERE c.oid = target.objid AND pb.pubname = ANY (publications)\n INTO relevant;\n\n ELSIF target.object_type = 'index' THEN\n SELECT ns.nspname AS \"schema\", c.relname AS \"name\" FROM pg_class AS c\n JOIN pg_namespace AS ns ON c.relnamespace = ns.oid\n JOIN pg_indexes as ind ON ind.schemaname = ns.nspname AND ind.indexname = c.relname\n JOIN pg_publication_tables AS pb ON pb.schemaname = ns.nspname AND pb.tablename = ind.tablename\n WHERE c.oid = target.objid AND pb.pubname = ANY (publications)\n INTO relevant;\n\n ELSIF target.object_type = 'publication relation' THEN\n SELECT pb.pubname FROM pg_publication_rel AS rel\n JOIN pg_publication AS pb ON pb.oid = rel.prpubid\n WHERE rel.oid = target.objid AND pb.pubname = ANY (publications) \n INTO relevant;\n\n ELSIF target.object_type = 'publication namespace' THEN\n SELECT pb.pubname FROM pg_publication_namespace AS ns\n JOIN pg_publication AS pb ON pb.oid = ns.pnpubid\n WHERE ns.oid = target.objid AND pb.pubname = ANY (publications) \n INTO relevant;\n\n ELSIF target.object_type = 'schema' THEN\n SELECT ns.nspname AS \"schema\", c.relname AS \"name\" FROM pg_class AS c\n JOIN pg_namespace AS ns ON c.relnamespace = ns.oid\n JOIN pg_publication_tables AS pb ON pb.schemaname = ns.nspname AND pb.tablename = c.relname\n WHERE ns.oid = target.objid AND pb.pubname = ANY (publications)\n INTO relevant;\n\n ELSIF target.object_type = 'publication' THEN\n SELECT 1 WHERE target.object_identity = ANY (publications)\n INTO relevant;\n\n -- no-op CREATE IF NOT EXIST statements\n ELSIF TG_TAG LIKE 'CREATE %' AND target.object_type IS NULL THEN\n relevant := NULL;\n END IF;\n\n IF relevant IS NULL THEN\n PERFORM ${schema}.notice_ignore('irrelevant', TG_TAG, target);\n RETURN;\n END IF;\n\n IF TG_TAG = 'COMMENT' THEN\n -- Only make schemaSnapshots for COMMENT ON PUBLICATION\n IF target.object_type != 'publication' THEN\n PERFORM ${schema}.notice_ignore('irrelevant', TG_TAG, target);\n RETURN;\n END IF;\n PERFORM ${schema}.update_schemas('schemaSnapshot', TG_TAG, target);\n ELSE\n PERFORM ${schema}.update_schemas('ddlUpdate', TG_TAG, target);\n END IF;\n\nEND\n$$ LANGUAGE plpgsql;\n`;\n}\n\n// Exported for testing.\nexport const TAGS = [\n 'CREATE TABLE',\n 'ALTER TABLE',\n 'CREATE INDEX',\n 'DROP TABLE',\n 'DROP INDEX',\n 'ALTER PUBLICATION',\n 'ALTER SCHEMA',\n] as const;\n\nexport function createEventTriggerStatements(shard: ShardConfig) {\n // Better to assert here than get a cryptic syntax error from Postgres.\n assert(shard.publications.length, `shard publications must be non-empty`);\n\n // Unlike functions, which are namespaced in shard-specific schemas,\n // EVENT TRIGGER names are in the global namespace and thus must include\n // the appID and shardNum.\n const {appID, shardNum} = shard;\n const sharded = append(shardNum);\n const schema = id(upstreamSchema(shard));\n\n const triggers = [\n dropEventTriggerStatements(shard.appID, shard.shardNum),\n /*sql*/ `\nCREATE EVENT TRIGGER ${sharded(`${appID}_ddl_start`)}\n ON ddl_command_start\n WHEN TAG IN (${lit(TAGS)})\n EXECUTE PROCEDURE ${schema}.emit_ddl_start();\n\nCREATE EVENT TRIGGER ${sharded(`${appID}_ddl_end`)}\n ON ddl_command_end\n WHEN TAG IN (${lit([...TAGS, 'COMMENT'])})\n EXECUTE PROCEDURE ${schema}.emit_ddl_end();\n`,\n ];\n\n // Drop legacy functions / triggers.\n triggers.push(\n `DROP FUNCTION IF EXISTS ${schema}.emit_ddl_end(text) CASCADE;`,\n `DROP FUNCTION IF EXISTS ${schema}.notice_ignore(text, record);`,\n );\n for (const tag of [...TAGS, 'COMMENT']) {\n const tagID = tag.toLowerCase().replace(' ', '_');\n triggers.push(`DROP FUNCTION IF EXISTS ${schema}.emit_${tagID}() CASCADE;`);\n }\n return triggers.join('');\n}\n\n// Exported for testing.\nexport function dropEventTriggerStatements(\n appID: string,\n shardID: string | number,\n) {\n return /*sql*/ `\n DROP EVENT TRIGGER IF EXISTS ${id(`${appID}_ddl_start_${shardID}`)};\n DROP EVENT TRIGGER IF EXISTS ${id(`${appID}_ddl_end_${shardID}`)};\n `;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AASjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAiB,KAAK,WAAW,EAAC,MAAM,6BAA6B,CAAC;AAa7E;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,WAAW,EAClB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAqBf;AA4LD,eAAO,MAAM,sBAAsB,QAMwB,CAAC;AAE5D,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,WAAW,iBAYnB"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AASjD,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAiB,KAAK,WAAW,EAAC,MAAM,6BAA6B,CAAC;AAa7E;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,WAAW,EAClB,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAqBf;AAsLD,eAAO,MAAM,sBAAsB,QAMwB,CAAC;AAE5D,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,WAAW,iBAYnB"}
@@ -109,7 +109,7 @@ function getIncrementalMigrations(shard, replicaVersion) {
109
109
  ADD COLUMN "subscriberContext" JSON
110
110
  `;
111
111
  } },
112
- 20: { migrateSchema: async (lc, sql) => {
112
+ 21: { migrateSchema: async (lc, sql) => {
113
113
  const [{ publications }] = await sql`
114
114
  SELECT publications FROM ${sql(shardConfigTable)}`;
115
115
  await setupTriggers(lc, sql, {
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {\n getVersionHistory,\n runSchemaMigrations,\n type IncrementalMigrationMap,\n type Migration,\n} from '../../../../db/migration.ts';\nimport type {PostgresDB} from '../../../../types/pg.ts';\nimport {upstreamSchema, type ShardConfig} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {decommissionShard} from '../decommission.ts';\nimport {publishedSchema} from './published.ts';\nimport {\n getMutationsTableDefinition,\n legacyReplicationSlot,\n metadataPublicationName,\n setupTablesAndReplication,\n setupTriggers,\n} from './shard.ts';\n\n/**\n * Ensures that a shard is set up for initial sync.\n */\nexport async function ensureShardSchema(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n): Promise<void> {\n const initialSetup: Migration = {\n migrateSchema: (lc, tx) => setupTablesAndReplication(lc, tx, shard),\n minSafeVersion: 1,\n };\n await runSchemaMigrations(\n lc,\n `upstream-shard-${shard.appID}`,\n upstreamSchema(shard),\n db,\n initialSetup,\n // The incremental migration of any existing replicas will be replaced by\n // the incoming replica being synced, so the replicaVersion here is\n // unnecessary.\n getIncrementalMigrations(shard, 'obsolete'),\n );\n}\n\n/**\n * Updates the schema for an existing shard.\n */\nexport async function updateShardSchema(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n replicaVersion: string,\n): Promise<void> {\n await runSchemaMigrations(\n lc,\n `upstream-shard-${shard.appID}`,\n upstreamSchema(shard),\n db,\n {\n // If the expected existing shard is absent, throw an\n // AutoResetSignal to backtrack and initial sync.\n migrateSchema: () => {\n throw new AutoResetSignal(\n `upstream shard ${upstreamSchema(shard)} is not initialized`,\n );\n },\n },\n getIncrementalMigrations(shard, replicaVersion),\n );\n\n // The decommission check is run in updateShardSchema so that it happens\n // after initial sync, and not when the shard schema is initially set up.\n await decommissionLegacyShard(lc, db, shard);\n}\n\nfunction getIncrementalMigrations(\n shard: ShardConfig,\n replicaVersion?: string,\n): IncrementalMigrationMap {\n const shardConfigTable = `${upstreamSchema(shard)}.shardConfig`;\n\n return {\n 4: {\n migrateSchema: () => {\n throw new AutoResetSignal('resetting to upgrade shard schema');\n },\n minSafeVersion: 3,\n },\n\n // v5: changes the upstream schema organization from \"zero_{SHARD_ID}\" to\n // the \"{APP_ID}_0\". An incremental migration indicates that the previous\n // SHARD_ID was \"0\" and the new APP_ID is \"zero\" (i.e. the default values\n // for those options). In this case, the upstream format is identical, and\n // no migration is necessary. However, the version is bumped to v5 to\n // indicate that it was created with the {APP_ID} configuration and should\n // not be decommissioned as a legacy shard.\n\n 6: {\n migrateSchema: async (lc, sql) => {\n assert(\n replicaVersion,\n `replicaVersion is always passed for incremental migrations`,\n );\n await Promise.all([\n sql`\n ALTER TABLE ${sql(shardConfigTable)} ADD \"replicaVersion\" TEXT`,\n sql`\n UPDATE ${sql(shardConfigTable)} SET ${sql({replicaVersion})}`,\n ]);\n lc.info?.(\n `Recorded replicaVersion ${replicaVersion} in upstream shardConfig`,\n );\n },\n },\n\n // Updates the DDL event trigger protocol to v2, and adds support for\n // ALTER SCHEMA x RENAME TO y\n 7: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded to v2 event triggers`);\n },\n },\n\n // Adds support for non-disruptive resyncs, which tracks multiple\n // replicas with different slot names.\n 8: {\n migrateSchema: async (lc, sql) => {\n const legacyShardConfigSchema = v.object({\n replicaVersion: v.string().nullable(),\n initialSchema: publishedSchema.nullable(),\n });\n const result = await sql`\n SELECT \"replicaVersion\", \"initialSchema\" FROM ${sql(shardConfigTable)}`;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n const {replicaVersion, initialSchema} = v.parse(\n result[0],\n legacyShardConfigSchema,\n 'passthrough',\n );\n\n await Promise.all([\n sql`\n CREATE TABLE ${sql(upstreamSchema(shard))}.replicas (\n \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL\n );\n `,\n sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.replicas ${sql({\n slot: legacyReplicationSlot(shard),\n version: replicaVersion,\n initialSchema,\n })}\n `,\n sql`\n ALTER TABLE ${sql(shardConfigTable)} DROP \"replicaVersion\", DROP \"initialSchema\"\n `,\n ]);\n lc.info?.(`Upgraded schema to support non-disruptive resyncs`);\n },\n },\n\n // v9: Fixes field ordering of compound indexes. This incremental migration\n // only fixes indexes resulting from new schema changes. A full resync is\n // required to fix existing indexes.\n //\n // The migration has been subsumed by the identical logic for migrating\n // to v12 (i.e. a trigger upgrade).\n\n // Adds the `mutations` table used to track mutation results.\n 10: {\n migrateSchema: async (lc, sql) => {\n await sql.unsafe(/*sql*/ `\n ${getMutationsTableDefinition(upstreamSchema(shard))}\n ALTER PUBLICATION ${id(metadataPublicationName(shard.appID, shard.shardNum))} ADD TABLE ${id(upstreamSchema(shard))}.\"mutations\";\n `);\n lc.info?.('Upgraded schema with new mutations table');\n },\n },\n\n // v11: Formerly dropped the schemaVersions table, but restored in the v13\n // migration for rollback safety.\n\n // v12: Upgrade DDL trigger to query schemaOID, needed information for auto-backfill.\n // (subsumed by v14)\n\n // Recreates the legacy schemaVersions table that was prematurely dropped\n // in the (former) v11 migration. It needs to remain present for at least one\n // release in order to be rollback safe.\n //\n // TODO: Drop the table once a release that no longer reads the table has\n // been rolled out.\n 13: {\n migrateSchema: async (_, sql) => {\n await sql`\n CREATE TABLE IF NOT EXISTS ${sql(upstreamSchema(shard))}.\"schemaVersions\" (\n \"minSupportedVersion\" INT4,\n \"maxSupportedVersion\" INT4,\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );`;\n await sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.\"schemaVersions\" \n (\"lock\", \"minSupportedVersion\", \"maxSupportedVersion\")\n VALUES (true, 1, 1)\n ON CONFLICT DO NOTHING;\n `;\n },\n },\n\n // v14: Upgrade DDL trigger to log more info to PG logs.\n // (subsumed by v16)\n\n // Add initialSyncContext column to replicas table.\n 15: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas\n ADD COLUMN \"initialSyncContext\" JSON,\n ADD COLUMN \"subscriberContext\" JSON\n `;\n },\n },\n\n // v16: Upgrade DDL trigger to fire on all ALTER TABLE statements\n // to catch the *removal* of a table from the published set.\n // (subsumed by v17)\n\n // v17: Upgrade DDL triggers to support the COMMENT ON PUBLICATION hook for\n // working around the lack of event trigger support for PUBLICATION\n // changes in supabase.\n //\n // This also adds forwards-compatible support for hierarchical logical\n // message prefixes and unknown ddl event types.\n // (subsumed by v18)\n\n // v18: Pure refactoring of event trigger code.\n // (subsumed by v19)\n\n // v19: Correctly handle concurrently issued DDL statements.\n // (subsumed by v20)\n\n // v20: Handle nested DDL triggers\n 20: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n };\n}\n\n// Referenced in tests.\nexport const CURRENT_SCHEMA_VERSION = Object.keys(\n getIncrementalMigrations({\n appID: 'unused',\n shardNum: 0,\n publications: ['foo'],\n }),\n).reduce((prev, curr) => Math.max(prev, parseInt(curr)), 0);\n\nexport async function decommissionLegacyShard(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n) {\n if (shard.appID !== 'zero') {\n // When migration from non-default shard ids, e.g. \"zero_prod\" => \"prod_0\",\n // clean up the old \"zero_prod\" shard if it is pre-v5. Note that the v5\n // check is important to guard against cleaning up a **new** \"zero_0\" app\n // that coexists with the current App (with app-id === \"0\").\n const versionHistory = await getVersionHistory(db, `zero_${shard.appID}`);\n if (versionHistory !== null && versionHistory.schemaVersion < 5) {\n await decommissionShard(lc, db, 'zero', shard.appID);\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"init.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/init.ts"],"sourcesContent":["import type {LogContext} from '@rocicorp/logger';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {\n getVersionHistory,\n runSchemaMigrations,\n type IncrementalMigrationMap,\n type Migration,\n} from '../../../../db/migration.ts';\nimport type {PostgresDB} from '../../../../types/pg.ts';\nimport {upstreamSchema, type ShardConfig} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {AutoResetSignal} from '../../../change-streamer/schema/tables.ts';\nimport {decommissionShard} from '../decommission.ts';\nimport {publishedSchema} from './published.ts';\nimport {\n getMutationsTableDefinition,\n legacyReplicationSlot,\n metadataPublicationName,\n setupTablesAndReplication,\n setupTriggers,\n} from './shard.ts';\n\n/**\n * Ensures that a shard is set up for initial sync.\n */\nexport async function ensureShardSchema(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n): Promise<void> {\n const initialSetup: Migration = {\n migrateSchema: (lc, tx) => setupTablesAndReplication(lc, tx, shard),\n minSafeVersion: 1,\n };\n await runSchemaMigrations(\n lc,\n `upstream-shard-${shard.appID}`,\n upstreamSchema(shard),\n db,\n initialSetup,\n // The incremental migration of any existing replicas will be replaced by\n // the incoming replica being synced, so the replicaVersion here is\n // unnecessary.\n getIncrementalMigrations(shard, 'obsolete'),\n );\n}\n\n/**\n * Updates the schema for an existing shard.\n */\nexport async function updateShardSchema(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n replicaVersion: string,\n): Promise<void> {\n await runSchemaMigrations(\n lc,\n `upstream-shard-${shard.appID}`,\n upstreamSchema(shard),\n db,\n {\n // If the expected existing shard is absent, throw an\n // AutoResetSignal to backtrack and initial sync.\n migrateSchema: () => {\n throw new AutoResetSignal(\n `upstream shard ${upstreamSchema(shard)} is not initialized`,\n );\n },\n },\n getIncrementalMigrations(shard, replicaVersion),\n );\n\n // The decommission check is run in updateShardSchema so that it happens\n // after initial sync, and not when the shard schema is initially set up.\n await decommissionLegacyShard(lc, db, shard);\n}\n\nfunction getIncrementalMigrations(\n shard: ShardConfig,\n replicaVersion?: string,\n): IncrementalMigrationMap {\n const shardConfigTable = `${upstreamSchema(shard)}.shardConfig`;\n\n return {\n 4: {\n migrateSchema: () => {\n throw new AutoResetSignal('resetting to upgrade shard schema');\n },\n minSafeVersion: 3,\n },\n\n // v5: changes the upstream schema organization from \"zero_{SHARD_ID}\" to\n // the \"{APP_ID}_0\". An incremental migration indicates that the previous\n // SHARD_ID was \"0\" and the new APP_ID is \"zero\" (i.e. the default values\n // for those options). In this case, the upstream format is identical, and\n // no migration is necessary. However, the version is bumped to v5 to\n // indicate that it was created with the {APP_ID} configuration and should\n // not be decommissioned as a legacy shard.\n\n 6: {\n migrateSchema: async (lc, sql) => {\n assert(\n replicaVersion,\n `replicaVersion is always passed for incremental migrations`,\n );\n await Promise.all([\n sql`\n ALTER TABLE ${sql(shardConfigTable)} ADD \"replicaVersion\" TEXT`,\n sql`\n UPDATE ${sql(shardConfigTable)} SET ${sql({replicaVersion})}`,\n ]);\n lc.info?.(\n `Recorded replicaVersion ${replicaVersion} in upstream shardConfig`,\n );\n },\n },\n\n // Updates the DDL event trigger protocol to v2, and adds support for\n // ALTER SCHEMA x RENAME TO y\n 7: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded to v2 event triggers`);\n },\n },\n\n // Adds support for non-disruptive resyncs, which tracks multiple\n // replicas with different slot names.\n 8: {\n migrateSchema: async (lc, sql) => {\n const legacyShardConfigSchema = v.object({\n replicaVersion: v.string().nullable(),\n initialSchema: publishedSchema.nullable(),\n });\n const result = await sql`\n SELECT \"replicaVersion\", \"initialSchema\" FROM ${sql(shardConfigTable)}`;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n const {replicaVersion, initialSchema} = v.parse(\n result[0],\n legacyShardConfigSchema,\n 'passthrough',\n );\n\n await Promise.all([\n sql`\n CREATE TABLE ${sql(upstreamSchema(shard))}.replicas (\n \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL\n );\n `,\n sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.replicas ${sql({\n slot: legacyReplicationSlot(shard),\n version: replicaVersion,\n initialSchema,\n })}\n `,\n sql`\n ALTER TABLE ${sql(shardConfigTable)} DROP \"replicaVersion\", DROP \"initialSchema\"\n `,\n ]);\n lc.info?.(`Upgraded schema to support non-disruptive resyncs`);\n },\n },\n\n // v9: Fixes field ordering of compound indexes. This incremental migration\n // only fixes indexes resulting from new schema changes. A full resync is\n // required to fix existing indexes.\n //\n // The migration has been subsumed by the identical logic for migrating\n // to v12 (i.e. a trigger upgrade).\n\n // Adds the `mutations` table used to track mutation results.\n 10: {\n migrateSchema: async (lc, sql) => {\n await sql.unsafe(/*sql*/ `\n ${getMutationsTableDefinition(upstreamSchema(shard))}\n ALTER PUBLICATION ${id(metadataPublicationName(shard.appID, shard.shardNum))} ADD TABLE ${id(upstreamSchema(shard))}.\"mutations\";\n `);\n lc.info?.('Upgraded schema with new mutations table');\n },\n },\n\n // v11: Formerly dropped the schemaVersions table, but restored in the v13\n // migration for rollback safety.\n\n // v12: Upgrade DDL trigger to query schemaOID, needed information for auto-backfill.\n // (subsumed by v14)\n\n // Recreates the legacy schemaVersions table that was prematurely dropped\n // in the (former) v11 migration. It needs to remain present for at least one\n // release in order to be rollback safe.\n //\n // TODO: Drop the table once a release that no longer reads the table has\n // been rolled out.\n 13: {\n migrateSchema: async (_, sql) => {\n await sql`\n CREATE TABLE IF NOT EXISTS ${sql(upstreamSchema(shard))}.\"schemaVersions\" (\n \"minSupportedVersion\" INT4,\n \"maxSupportedVersion\" INT4,\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );`;\n await sql`\n INSERT INTO ${sql(upstreamSchema(shard))}.\"schemaVersions\" \n (\"lock\", \"minSupportedVersion\", \"maxSupportedVersion\")\n VALUES (true, 1, 1)\n ON CONFLICT DO NOTHING;\n `;\n },\n },\n\n // v14: Upgrade DDL trigger to log more info to PG logs.\n // (subsumed by v16)\n\n // Add initialSyncContext column to replicas table.\n 15: {\n migrateSchema: async (_, sql) => {\n await sql`\n ALTER TABLE ${sql(upstreamSchema(shard))}.replicas\n ADD COLUMN \"initialSyncContext\" JSON,\n ADD COLUMN \"subscriberContext\" JSON\n `;\n },\n },\n\n // v16: Upgrade DDL trigger to fire on all ALTER TABLE statements\n // to catch the *removal* of a table from the published set.\n // v17 (1.0.0): Upgrade DDL triggers to support the COMMENT ON PUBLICATION hook for\n // working around the lack of event trigger support for PUBLICATION\n // changes in supabase.\n // This also adds forwards-compatible support for hierarchical logical\n // message prefixes and unknown ddl event types.\n // v18: Pure refactoring of event trigger code.\n // v19 (1.4.0): Correctly handle concurrently issued DDL statements.\n // v20 (1.4.0): Handle nested DDL triggers\n\n // v21 (1.5.0): Handle cross-transaction DDL operations (i.e. concurrent\n // index operations), and support manual invocation of update_schemas().\n 21: {\n migrateSchema: async (lc, sql) => {\n const [{publications}] = await sql<{publications: string[]}[]>`\n SELECT publications FROM ${sql(shardConfigTable)}`;\n await setupTriggers(lc, sql, {...shard, publications});\n lc.info?.(`Upgraded DDL event triggers`);\n },\n },\n };\n}\n\n// Referenced in tests.\nexport const CURRENT_SCHEMA_VERSION = Object.keys(\n getIncrementalMigrations({\n appID: 'unused',\n shardNum: 0,\n publications: ['foo'],\n }),\n).reduce((prev, curr) => Math.max(prev, parseInt(curr)), 0);\n\nexport async function decommissionLegacyShard(\n lc: LogContext,\n db: PostgresDB,\n shard: ShardConfig,\n) {\n if (shard.appID !== 'zero') {\n // When migration from non-default shard ids, e.g. \"zero_prod\" => \"prod_0\",\n // clean up the old \"zero_prod\" shard if it is pre-v5. Note that the v5\n // check is important to guard against cleaning up a **new** \"zero_0\" app\n // that coexists with the current App (with app-id === \"0\").\n const versionHistory = await getVersionHistory(db, `zero_${shard.appID}`);\n if (versionHistory !== null && versionHistory.schemaVersion < 5) {\n await decommissionShard(lc, db, 'zero', shard.appID);\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;;AA0BA,eAAsB,kBACpB,IACA,IACA,OACe;AAKf,OAAM,oBACJ,IACA,kBAAkB,MAAM,SACxB,eAAe,MAAM,EACrB,IAR8B;EAC9B,gBAAgB,IAAI,OAAO,0BAA0B,IAAI,IAAI,MAAM;EACnE,gBAAgB;EACjB,EAUC,yBAAyB,OAAO,WAAW,CAC5C;;;;;AAMH,eAAsB,kBACpB,IACA,IACA,OACA,gBACe;AACf,OAAM,oBACJ,IACA,kBAAkB,MAAM,SACxB,eAAe,MAAM,EACrB,IACA,EAGE,qBAAqB;AACnB,QAAM,IAAI,gBACR,kBAAkB,eAAe,MAAM,CAAC,qBACzC;IAEJ,EACD,yBAAyB,OAAO,eAAe,CAChD;AAID,OAAM,wBAAwB,IAAI,IAAI,MAAM;;AAG9C,SAAS,yBACP,OACA,gBACyB;CACzB,MAAM,mBAAmB,GAAG,eAAe,MAAM,CAAC;AAElD,QAAO;EACL,GAAG;GACD,qBAAqB;AACnB,UAAM,IAAI,gBAAgB,oCAAoC;;GAEhE,gBAAgB;GACjB;EAUD,GAAG,EACD,eAAe,OAAO,IAAI,QAAQ;AAChC,UACE,gBACA,6DACD;AACD,SAAM,QAAQ,IAAI,CAChB,GAAG;wBACW,IAAI,iBAAiB,CAAC,6BACpC,GAAG;mBACM,IAAI,iBAAiB,CAAC,OAAO,IAAI,EAAC,gBAAe,CAAC,GAC5D,CAAC;AACF,MAAG,OACD,2BAA2B,eAAe,0BAC3C;KAEJ;EAID,GAAG,EACD,eAAe,OAAO,IAAI,QAAQ;GAChC,MAAM,CAAC,EAAC,kBAAiB,MAAM,GAA+B;qCACjC,IAAI,iBAAiB;AAClD,SAAM,cAAc,IAAI,KAAK;IAAC,GAAG;IAAO;IAAa,CAAC;AACtD,MAAG,OAAO,gCAAgC;KAE7C;EAID,GAAG,EACD,eAAe,OAAO,IAAI,QAAQ;GAChC,MAAM,0BAA0B,eAAE,OAAO;IACvC,gBAAgB,eAAE,QAAQ,CAAC,UAAU;IACrC,eAAe,gBAAgB,UAAU;IAC1C,CAAC;GACF,MAAM,SAAS,MAAM,GAAG;0DAC0B,IAAI,iBAAiB;AACvE,UACE,OAAO,WAAW,SACZ,6CAA6C,OAAO,SAC3D;GACD,MAAM,EAAC,gBAAgB,kBAAiB,MACtC,OAAO,IACP,yBACA,cACD;AAED,SAAM,QAAQ,IAAI;IAChB,GAAG;yBACY,IAAI,eAAe,MAAM,CAAC,CAAC;;;;;;IAM1C,GAAG;wBACW,IAAI,eAAe,MAAM,CAAC,CAAC,YAAY,IAAI;KACvD,MAAM,sBAAsB,MAAM;KAClC,SAAS;KACT;KACD,CAAC,CAAC;;IAEH,GAAG;wBACW,IAAI,iBAAiB,CAAC;;IAErC,CAAC;AACF,MAAG,OAAO,oDAAoD;KAEjE;EAUD,IAAI,EACF,eAAe,OAAO,IAAI,QAAQ;AAChC,SAAM,IAAI,OAAe;YACrB,4BAA4B,eAAe,MAAM,CAAC,CAAC;8BACjC,GAAG,wBAAwB,MAAM,OAAO,MAAM,SAAS,CAAC,CAAC,aAAa,GAAG,eAAe,MAAM,CAAC,CAAC;UACpH;AACF,MAAG,OAAO,2CAA2C;KAExD;EAcD,IAAI,EACF,eAAe,OAAO,GAAG,QAAQ;AAC/B,SAAM,GAAG;uCACsB,IAAI,eAAe,MAAM,CAAC,CAAC;;;;;AAK1D,SAAM,GAAG;wBACO,IAAI,eAAe,MAAM,CAAC,CAAC;;;;;KAM9C;EAMD,IAAI,EACF,eAAe,OAAO,GAAG,QAAQ;AAC/B,SAAM,GAAG;wBACO,IAAI,eAAe,MAAM,CAAC,CAAC;;;;KAK9C;EAeD,IAAI,EACF,eAAe,OAAO,IAAI,QAAQ;GAChC,MAAM,CAAC,EAAC,kBAAiB,MAAM,GAA+B;qCACjC,IAAI,iBAAiB;AAClD,SAAM,cAAc,IAAI,KAAK;IAAC,GAAG;IAAO;IAAa,CAAC;AACtD,MAAG,OAAO,8BAA8B;KAE3C;EACF;;AAImC,OAAO,KAC3C,yBAAyB;CACvB,OAAO;CACP,UAAU;CACV,cAAc,CAAC,MAAM;CACtB,CAAC,CACH,CAAC,QAAQ,MAAM,SAAS,KAAK,IAAI,MAAM,SAAS,KAAK,CAAC,EAAE,EAAE;AAE3D,eAAsB,wBACpB,IACA,IACA,OACA;AACA,KAAI,MAAM,UAAU,QAAQ;EAK1B,MAAM,iBAAiB,MAAM,kBAAkB,IAAI,QAAQ,MAAM,QAAQ;AACzE,MAAI,mBAAmB,QAAQ,eAAe,gBAAgB,EAC5D,OAAM,kBAAkB,IAAI,IAAI,QAAQ,MAAM,MAAM"}
@@ -1 +1 @@
1
- {"version":3,"file":"shard.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/shard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"shard.d.ts","sourceRoot":"","sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/shard.ts"],"names":[],"mappings":"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"}
@@ -4,7 +4,7 @@ import { jsonObjectSchema, stringify } from "../../../../../../shared/src/bigint
4
4
  import { appSchema, check, upstreamSchema } from "../../../../types/shards.js";
5
5
  import { id } from "../../../../types/sql.js";
6
6
  import { getPublicationInfo, publishedSchema } from "./published.js";
7
- import { createEventTriggerStatements } from "./ddl.js";
7
+ import { createEventFunctionStatements, createEventTriggerStatements } from "./ddl.js";
8
8
  import { validate } from "./validation.js";
9
9
  import postgres from "postgres";
10
10
  import { literal } from "pg-format";
@@ -258,6 +258,7 @@ async function setupTablesAndReplication(lc, sql, requested) {
258
258
  async function setupTriggers(lc, tx, shard) {
259
259
  const [{ ddlDetection }] = await tx`
260
260
  SELECT "ddlDetection" FROM ${tx(upstreamSchema(shard))}."shardConfig"`;
261
+ await tx.unsafe(createEventFunctionStatements(shard));
261
262
  try {
262
263
  await tx.savepoint((sub) => sub.unsafe(triggerSetup(shard)));
263
264
  } catch (e) {
@@ -1 +1 @@
1
- {"version":3,"file":"shard.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/shard.ts"],"sourcesContent":["import {PG_INSUFFICIENT_PRIVILEGE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {literal} from 'pg-format';\nimport postgres from 'postgres';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport {\n jsonObjectSchema,\n stringify,\n type JSONObject,\n} from '../../../../../../shared/src/bigint-json.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {Default} from '../../../../db/postgres-replica-identity-enum.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../../../types/pg.ts';\nimport type {AppID, ShardConfig, ShardID} from '../../../../types/shards.ts';\nimport {appSchema, check, upstreamSchema} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {createEventTriggerStatements} from './ddl.ts';\nimport {\n getPublicationInfo,\n publishedSchema,\n type PublicationInfo,\n type PublishedSchema,\n} from './published.ts';\nimport {validate} from './validation.ts';\n\n/**\n * PostgreSQL unquoted identifiers must start with a letter or underscore\n * and contain only letters, digits, and underscores.\n */\nconst VALID_PUBLICATION_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Validates that a publication name is a valid PostgreSQL identifier.\n * This provides defense-in-depth against SQL injection when publication\n * names are used in replication commands.\n */\nexport function validatePublicationName(name: string): void {\n if (!VALID_PUBLICATION_NAME.test(name)) {\n throw new Error(\n `Invalid publication name \"${name}\". Publication names must start with a letter or underscore ` +\n `and contain only letters, digits, and underscores.`,\n );\n }\n if (name.length > 63) {\n throw new Error(\n `Publication name \"${name}\" exceeds PostgreSQL's 63-character identifier limit.`,\n );\n }\n}\n\nexport function internalPublicationPrefix({appID}: AppID) {\n return `_${appID}_`;\n}\n\nexport function legacyReplicationSlot({appID, shardNum}: ShardID) {\n return `${appID}_${shardNum}`;\n}\n\nexport function replicationSlotPrefix(shard: ShardID) {\n const {appID, shardNum} = check(shard);\n return `${appID}_${shardNum}_`;\n}\n\n/**\n * An expression used to match replication slots in the shard\n * in a Postgres `LIKE` operator.\n */\nexport function replicationSlotExpression(shard: ShardID) {\n // Underscores have a special meaning in LIKE values\n // so they have to be escaped.\n return `${replicationSlotPrefix(shard)}%`.replaceAll('_', '\\\\_');\n}\n\nexport function newReplicationSlot(shard: ShardID) {\n return replicationSlotPrefix(shard) + Date.now();\n}\n\nfunction defaultPublicationName(appID: string, shardID: string | number) {\n return `_${appID}_public_${shardID}`;\n}\n\nexport function metadataPublicationName(\n appID: string,\n shardID: string | number,\n) {\n return `_${appID}_metadata_${shardID}`;\n}\n\n// The GLOBAL_SETUP must be idempotent as it can be run multiple times for different shards.\nfunction globalSetup(appID: AppID): string {\n const app = id(appSchema(appID));\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${app};\n\n CREATE TABLE IF NOT EXISTS ${app}.permissions (\n \"permissions\" JSONB,\n \"hash\" TEXT,\n\n -- Ensure that there is only a single row in the table.\n -- Application code can be agnostic to this column, and\n -- simply invoke UPDATE statements on the version columns.\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );\n\n CREATE OR REPLACE FUNCTION ${app}.set_permissions_hash()\n RETURNS TRIGGER AS $$\n BEGIN\n NEW.hash = md5(NEW.permissions::text);\n RETURN NEW;\n END;\n $$ LANGUAGE plpgsql;\n\n CREATE OR REPLACE TRIGGER on_set_permissions \n BEFORE INSERT OR UPDATE ON ${app}.permissions\n FOR EACH ROW\n EXECUTE FUNCTION ${app}.set_permissions_hash();\n\n INSERT INTO ${app}.permissions (permissions) VALUES (NULL) ON CONFLICT DO NOTHING;\n`;\n}\n\nexport async function ensureGlobalTables(db: PostgresDB, appID: AppID) {\n await db.unsafe(globalSetup(appID));\n}\n\nexport function getClientsTableDefinition(schema: string) {\n return /*sql*/ `\n CREATE TABLE ${schema}.\"clients\" (\n \"clientGroupID\" TEXT NOT NULL,\n \"clientID\" TEXT NOT NULL,\n \"lastMutationID\" BIGINT NOT NULL,\n \"userID\" TEXT,\n PRIMARY KEY(\"clientGroupID\", \"clientID\")\n );`;\n}\n\n/**\n * Tracks the results of mutations.\n * 1. It is an error for the same mutation ID to be used twice.\n * 2. The result is JSONB to allow for arbitrary results.\n *\n * The tables must be cleaned up as the clients\n * receive the mutation responses and as clients are removed.\n */\nexport function getMutationsTableDefinition(schema: string) {\n return /*sql*/ `\n CREATE TABLE ${schema}.\"mutations\" (\n \"clientGroupID\" TEXT NOT NULL,\n \"clientID\" TEXT NOT NULL,\n \"mutationID\" BIGINT NOT NULL,\n \"result\" JSON NOT NULL,\n PRIMARY KEY(\"clientGroupID\", \"clientID\", \"mutationID\")\n );`;\n}\n\nexport const SHARD_CONFIG_TABLE = 'shardConfig';\n\nexport function shardSetup(\n shardConfig: ShardConfig,\n metadataPublication: string,\n): string {\n const app = id(appSchema(shardConfig));\n const shard = id(upstreamSchema(shardConfig));\n\n const pubs = shardConfig.publications.toSorted();\n assert(\n pubs.includes(metadataPublication),\n () => `Publications must include ${metadataPublication}`,\n );\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${shard};\n\n ${getClientsTableDefinition(shard)}\n ${getMutationsTableDefinition(shard)}\n\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n CREATE PUBLICATION ${id(metadataPublication)}\n FOR TABLE ${app}.\"permissions\", TABLE ${shard}.\"clients\", ${shard}.\"mutations\";\n\n CREATE TABLE ${shard}.\"${SHARD_CONFIG_TABLE}\" (\n \"publications\" TEXT[] NOT NULL,\n \"ddlDetection\" BOOL NOT NULL,\n\n -- Ensure that there is only a single row in the table.\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );\n\n INSERT INTO ${shard}.\"${SHARD_CONFIG_TABLE}\" (\n \"publications\",\n \"ddlDetection\" \n ) VALUES (\n ARRAY[${literal(pubs)}], \n false -- set in SAVEPOINT with triggerSetup() statements\n );\n\n CREATE TABLE ${shard}.replicas (\n \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL,\n \"initialSyncContext\" JSON,\n \"subscriberContext\" JSON\n );\n `;\n}\n\nexport function dropShard(appID: string, shardID: string | number): string {\n const schema = `${appID}_${shardID}`;\n const metadataPublication = metadataPublicationName(appID, shardID);\n const defaultPublication = defaultPublicationName(appID, shardID);\n\n // DROP SCHEMA ... CASCADE does not drop dependent PUBLICATIONS,\n // so PUBLICATIONs must be dropped explicitly.\n return /*sql*/ `\n DROP PUBLICATION IF EXISTS ${id(defaultPublication)};\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n DROP SCHEMA IF EXISTS ${id(schema)} CASCADE;\n `;\n}\n\nconst internalShardConfigSchema = v.object({\n publications: v.array(v.string()),\n ddlDetection: v.boolean(),\n});\n\nexport type InternalShardConfig = v.Infer<typeof internalShardConfigSchema>;\n\nconst replicaSchema = internalShardConfigSchema.extend({\n slot: v.string(),\n version: v.string(),\n initialSchema: publishedSchema,\n initialSyncContext: jsonObjectSchema.nullable(),\n subscriberContext: jsonObjectSchema.nullable(),\n});\n\nexport type Replica = v.Infer<typeof replicaSchema>;\n\n// triggerSetup is run separately in a sub-transaction (i.e. SAVEPOINT) so\n// that a failure (e.g. due to lack of superuser permissions) can be handled\n// by continuing in a degraded mode (ddlDetection = false).\nfunction triggerSetup(shard: ShardConfig): string {\n const schema = id(upstreamSchema(shard));\n return (\n createEventTriggerStatements(shard) +\n /*sql*/ `UPDATE ${schema}.\"shardConfig\" SET \"ddlDetection\" = true;`\n );\n}\n\n// Called in initial-sync to store the exact schema that was initially synced.\nexport async function addReplica(\n sql: PostgresDB,\n shard: ShardID,\n slot: string,\n replicaVersion: string,\n {tables, indexes}: PublishedSchema,\n initialSyncContext: JSONObject,\n) {\n const schema = upstreamSchema(shard);\n const synced: PublishedSchema = {tables, indexes};\n await sql`\n INSERT INTO ${sql(schema)}.replicas\n (\"slot\", \"version\", \"initialSchema\", \"initialSyncContext\")\n VALUES (${slot}, ${replicaVersion}, ${synced}, ${initialSyncContext})`;\n}\n\nexport async function getReplicaAtVersion(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n replicaVersion: string,\n context?: JSONObject,\n): Promise<Replica | null> {\n const schema = sql(upstreamSchema(shard));\n const result = await sql`\n SELECT\n replicas.\"slot\",\n replicas.\"version\",\n replicas.\"initialSchema\",\n replicas.\"initialSyncContext\",\n replicas.\"subscriberContext\",\n \"shardConfig\".\"publications\",\n \"shardConfig\".\"ddlDetection\"\n FROM ${schema}.replicas JOIN ${schema}.\"shardConfig\" ON true\n WHERE version = ${replicaVersion};\n `;\n if (result.length === 0) {\n // log out all the replicas and the joined shardConfig\n const allReplicas = await sql`\n SELECT slot, version, \"initialSyncContext\", \"subscriberContext\" \n FROM ${schema}.replicas`;\n lc.info?.(\n `Replica ${replicaVersion} ` +\n (context ? `(context: ${stringify(context)}) ` : '') +\n `not found in: ${stringify(allReplicas)}`,\n );\n return null;\n }\n return v.parse(result[0], replicaSchema, 'passthrough');\n}\n\nexport async function getInternalShardConfig(\n sql: PostgresDB,\n shard: ShardID,\n): Promise<InternalShardConfig> {\n const result = await sql`\n SELECT \"publications\", \"ddlDetection\"\n FROM ${sql(upstreamSchema(shard))}.\"shardConfig\";\n `;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n return v.parse(result[0], internalShardConfigSchema, 'passthrough');\n}\n\n/**\n * Sets up and returns all publications (including internal ones) for\n * the given shard.\n */\nexport async function setupTablesAndReplication(\n lc: LogContext,\n sql: PostgresTransaction,\n requested: ShardConfig,\n) {\n const {publications} = requested;\n // Validate requested publications.\n for (const pub of publications) {\n validatePublicationName(pub);\n if (pub.startsWith('_')) {\n throw new Error(\n `Publication names starting with \"_\" are reserved for internal use.\\n` +\n `Please use a different name for publication \"${pub}\".`,\n );\n }\n }\n const allPublications: string[] = [];\n\n // Setup application publications.\n if (publications.length) {\n const results = await sql<{pubname: string}[]>`\n SELECT pubname from pg_publication WHERE pubname IN ${sql(\n publications,\n )}`.values();\n\n if (results.length !== publications.length) {\n throw new Error(\n `Unknown or invalid publications. Specified: [${publications}]. Found: [${results.flat()}]`,\n );\n }\n allPublications.push(...publications);\n } else {\n const defaultPublication = defaultPublicationName(\n requested.appID,\n requested.shardNum,\n );\n await sql`\n DROP PUBLICATION IF EXISTS ${sql(defaultPublication)}`;\n await sql`\n CREATE PUBLICATION ${sql(defaultPublication)} \n FOR TABLES IN SCHEMA public\n WITH (publish_via_partition_root = true)`;\n allPublications.push(defaultPublication);\n }\n\n const metadataPublication = metadataPublicationName(\n requested.appID,\n requested.shardNum,\n );\n allPublications.push(metadataPublication);\n\n const shard = {...requested, publications: allPublications};\n\n // Setup the global tables and shard tables / publications.\n await sql.unsafe(globalSetup(shard) + shardSetup(shard, metadataPublication));\n\n const pubs = await getPublicationInfo(sql, allPublications);\n await replicaIdentitiesForTablesWithoutPrimaryKeys(pubs)?.apply(lc, sql);\n\n await setupTriggers(lc, sql, shard);\n}\n\nexport async function setupTriggers(\n lc: LogContext,\n tx: PostgresTransaction,\n shard: ShardConfig,\n) {\n const schema = upstreamSchema(shard);\n const [{ddlDetection}] = await tx<InternalShardConfig[]> /*sql*/ `\n SELECT \"ddlDetection\" FROM ${tx(schema)}.\"shardConfig\"`;\n try {\n await tx.savepoint(sub => sub.unsafe(triggerSetup(shard)));\n } catch (e) {\n if (ddlDetection) {\n // If ddlDetection has already been enabled, subsequent failures to\n // upgrade the trigger should be propagated rather than swallowed.\n throw e;\n }\n if (\n !(\n e instanceof postgres.PostgresError &&\n e.code === PG_INSUFFICIENT_PRIVILEGE\n )\n ) {\n throw e;\n }\n // If triggerSetup() fails, replication continues in ddlDetection=false mode.\n lc.warn?.(\n `Unable to create event triggers for schema change detection:\\n\\n` +\n `\"${e.hint ?? e.message}\"\\n\\n` +\n `Proceeding in degraded mode: schema changes will halt replication,\\n` +\n `requiring the replica to be reset (manually or with --auto-reset).`,\n );\n }\n}\n\nexport function validatePublications(\n lc: LogContext,\n published: PublicationInfo,\n) {\n // Verify that all publications export the proper events.\n published.publications.forEach(pub => {\n if (\n !pub.pubinsert ||\n !pub.pubupdate ||\n !pub.pubdelete ||\n !pub.pubtruncate\n ) {\n // TODO: Make APIError?\n throw new Error(\n `PUBLICATION ${pub.pubname} must publish insert, update, delete, and truncate`,\n );\n }\n });\n\n published.tables.forEach(table => validate(lc, table));\n}\n\ntype ReplicaIdentities = {\n apply(lc: LogContext, db: PostgresDB): Promise<void>;\n};\n\nexport function replicaIdentitiesForTablesWithoutPrimaryKeys(\n pubs: PublishedSchema,\n): ReplicaIdentities | undefined {\n const replicaIdentities: {\n schema: string;\n tableName: string;\n indexName: string;\n }[] = [];\n for (const table of pubs.tables) {\n if (!table.primaryKey?.length && table.replicaIdentity === Default) {\n // Look for an index that can serve as the REPLICA IDENTITY USING INDEX. It must be:\n // - UNIQUE\n // - NOT NULL columns\n // - not deferrable (i.e. isImmediate)\n // - not partial (are already filtered out)\n //\n // https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-REPLICA-IDENTITY\n const {schema, name: tableName} = table;\n for (const {columns, name: indexName} of pubs.indexes.filter(\n idx =>\n idx.schema === schema &&\n idx.tableName === tableName &&\n idx.unique &&\n idx.isImmediate,\n )) {\n if (Object.keys(columns).some(col => !table.columns[col].notNull)) {\n continue; // Only indexes with all NOT NULL columns are suitable.\n }\n replicaIdentities.push({schema, tableName, indexName});\n break;\n }\n }\n }\n\n if (replicaIdentities.length === 0) {\n return undefined;\n }\n return {\n apply: async (lc: LogContext, sql: PostgresDB) => {\n for (const {schema, tableName, indexName} of replicaIdentities) {\n lc.info?.(\n `setting \"${indexName}\" as the REPLICA IDENTITY for \"${tableName}\"`,\n );\n await sql`\n ALTER TABLE ${sql(schema)}.${sql(tableName)} \n REPLICA IDENTITY USING INDEX ${sql(indexName)}`;\n }\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shard.js","names":[],"sources":["../../../../../../../../zero-cache/src/services/change-source/pg/schema/shard.ts"],"sourcesContent":["import {PG_INSUFFICIENT_PRIVILEGE} from '@drdgvhbh/postgres-error-codes';\nimport type {LogContext} from '@rocicorp/logger';\nimport {literal} from 'pg-format';\nimport postgres from 'postgres';\nimport {assert} from '../../../../../../shared/src/asserts.ts';\nimport {\n jsonObjectSchema,\n stringify,\n type JSONObject,\n} from '../../../../../../shared/src/bigint-json.ts';\nimport * as v from '../../../../../../shared/src/valita.ts';\nimport {Default} from '../../../../db/postgres-replica-identity-enum.ts';\nimport type {PostgresDB, PostgresTransaction} from '../../../../types/pg.ts';\nimport type {AppID, ShardConfig, ShardID} from '../../../../types/shards.ts';\nimport {appSchema, check, upstreamSchema} from '../../../../types/shards.ts';\nimport {id} from '../../../../types/sql.ts';\nimport {\n createEventFunctionStatements,\n createEventTriggerStatements,\n} from './ddl.ts';\nimport {\n getPublicationInfo,\n publishedSchema,\n type PublicationInfo,\n type PublishedSchema,\n} from './published.ts';\nimport {validate} from './validation.ts';\n\n/**\n * PostgreSQL unquoted identifiers must start with a letter or underscore\n * and contain only letters, digits, and underscores.\n */\nconst VALID_PUBLICATION_NAME = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * Validates that a publication name is a valid PostgreSQL identifier.\n * This provides defense-in-depth against SQL injection when publication\n * names are used in replication commands.\n */\nexport function validatePublicationName(name: string): void {\n if (!VALID_PUBLICATION_NAME.test(name)) {\n throw new Error(\n `Invalid publication name \"${name}\". Publication names must start with a letter or underscore ` +\n `and contain only letters, digits, and underscores.`,\n );\n }\n if (name.length > 63) {\n throw new Error(\n `Publication name \"${name}\" exceeds PostgreSQL's 63-character identifier limit.`,\n );\n }\n}\n\nexport function internalPublicationPrefix({appID}: AppID) {\n return `_${appID}_`;\n}\n\nexport function legacyReplicationSlot({appID, shardNum}: ShardID) {\n return `${appID}_${shardNum}`;\n}\n\nexport function replicationSlotPrefix(shard: ShardID) {\n const {appID, shardNum} = check(shard);\n return `${appID}_${shardNum}_`;\n}\n\n/**\n * An expression used to match replication slots in the shard\n * in a Postgres `LIKE` operator.\n */\nexport function replicationSlotExpression(shard: ShardID) {\n // Underscores have a special meaning in LIKE values\n // so they have to be escaped.\n return `${replicationSlotPrefix(shard)}%`.replaceAll('_', '\\\\_');\n}\n\nexport function newReplicationSlot(shard: ShardID) {\n return replicationSlotPrefix(shard) + Date.now();\n}\n\nfunction defaultPublicationName(appID: string, shardID: string | number) {\n return `_${appID}_public_${shardID}`;\n}\n\nexport function metadataPublicationName(\n appID: string,\n shardID: string | number,\n) {\n return `_${appID}_metadata_${shardID}`;\n}\n\n// The GLOBAL_SETUP must be idempotent as it can be run multiple times for different shards.\nfunction globalSetup(appID: AppID): string {\n const app = id(appSchema(appID));\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${app};\n\n CREATE TABLE IF NOT EXISTS ${app}.permissions (\n \"permissions\" JSONB,\n \"hash\" TEXT,\n\n -- Ensure that there is only a single row in the table.\n -- Application code can be agnostic to this column, and\n -- simply invoke UPDATE statements on the version columns.\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );\n\n CREATE OR REPLACE FUNCTION ${app}.set_permissions_hash()\n RETURNS TRIGGER AS $$\n BEGIN\n NEW.hash = md5(NEW.permissions::text);\n RETURN NEW;\n END;\n $$ LANGUAGE plpgsql;\n\n CREATE OR REPLACE TRIGGER on_set_permissions \n BEFORE INSERT OR UPDATE ON ${app}.permissions\n FOR EACH ROW\n EXECUTE FUNCTION ${app}.set_permissions_hash();\n\n INSERT INTO ${app}.permissions (permissions) VALUES (NULL) ON CONFLICT DO NOTHING;\n`;\n}\n\nexport async function ensureGlobalTables(db: PostgresDB, appID: AppID) {\n await db.unsafe(globalSetup(appID));\n}\n\nexport function getClientsTableDefinition(schema: string) {\n return /*sql*/ `\n CREATE TABLE ${schema}.\"clients\" (\n \"clientGroupID\" TEXT NOT NULL,\n \"clientID\" TEXT NOT NULL,\n \"lastMutationID\" BIGINT NOT NULL,\n \"userID\" TEXT,\n PRIMARY KEY(\"clientGroupID\", \"clientID\")\n );`;\n}\n\n/**\n * Tracks the results of mutations.\n * 1. It is an error for the same mutation ID to be used twice.\n * 2. The result is JSONB to allow for arbitrary results.\n *\n * The tables must be cleaned up as the clients\n * receive the mutation responses and as clients are removed.\n */\nexport function getMutationsTableDefinition(schema: string) {\n return /*sql*/ `\n CREATE TABLE ${schema}.\"mutations\" (\n \"clientGroupID\" TEXT NOT NULL,\n \"clientID\" TEXT NOT NULL,\n \"mutationID\" BIGINT NOT NULL,\n \"result\" JSON NOT NULL,\n PRIMARY KEY(\"clientGroupID\", \"clientID\", \"mutationID\")\n );`;\n}\n\nexport const SHARD_CONFIG_TABLE = 'shardConfig';\n\nexport function shardSetup(\n shardConfig: ShardConfig,\n metadataPublication: string,\n): string {\n const app = id(appSchema(shardConfig));\n const shard = id(upstreamSchema(shardConfig));\n\n const pubs = shardConfig.publications.toSorted();\n assert(\n pubs.includes(metadataPublication),\n () => `Publications must include ${metadataPublication}`,\n );\n\n return /*sql*/ `\n CREATE SCHEMA IF NOT EXISTS ${shard};\n\n ${getClientsTableDefinition(shard)}\n ${getMutationsTableDefinition(shard)}\n\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n CREATE PUBLICATION ${id(metadataPublication)}\n FOR TABLE ${app}.\"permissions\", TABLE ${shard}.\"clients\", ${shard}.\"mutations\";\n\n CREATE TABLE ${shard}.\"${SHARD_CONFIG_TABLE}\" (\n \"publications\" TEXT[] NOT NULL,\n \"ddlDetection\" BOOL NOT NULL,\n\n -- Ensure that there is only a single row in the table.\n \"lock\" BOOL PRIMARY KEY DEFAULT true CHECK (lock)\n );\n\n INSERT INTO ${shard}.\"${SHARD_CONFIG_TABLE}\" (\n \"publications\",\n \"ddlDetection\" \n ) VALUES (\n ARRAY[${literal(pubs)}], \n false -- set in SAVEPOINT with triggerSetup() statements\n );\n\n CREATE TABLE ${shard}.replicas (\n \"slot\" TEXT PRIMARY KEY,\n \"version\" TEXT NOT NULL,\n \"initialSchema\" JSON NOT NULL,\n \"initialSyncContext\" JSON,\n \"subscriberContext\" JSON\n );\n `;\n}\n\nexport function dropShard(appID: string, shardID: string | number): string {\n const schema = `${appID}_${shardID}`;\n const metadataPublication = metadataPublicationName(appID, shardID);\n const defaultPublication = defaultPublicationName(appID, shardID);\n\n // DROP SCHEMA ... CASCADE does not drop dependent PUBLICATIONS,\n // so PUBLICATIONs must be dropped explicitly.\n return /*sql*/ `\n DROP PUBLICATION IF EXISTS ${id(defaultPublication)};\n DROP PUBLICATION IF EXISTS ${id(metadataPublication)};\n DROP SCHEMA IF EXISTS ${id(schema)} CASCADE;\n `;\n}\n\nconst internalShardConfigSchema = v.object({\n publications: v.array(v.string()),\n ddlDetection: v.boolean(),\n});\n\nexport type InternalShardConfig = v.Infer<typeof internalShardConfigSchema>;\n\nconst replicaSchema = internalShardConfigSchema.extend({\n slot: v.string(),\n version: v.string(),\n initialSchema: publishedSchema,\n initialSyncContext: jsonObjectSchema.nullable(),\n subscriberContext: jsonObjectSchema.nullable(),\n});\n\nexport type Replica = v.Infer<typeof replicaSchema>;\n\n// triggerSetup is run separately in a sub-transaction (i.e. SAVEPOINT) so\n// that a failure (e.g. due to lack of superuser permissions) can be handled\n// by continuing in a degraded mode (ddlDetection = false).\nfunction triggerSetup(shard: ShardConfig): string {\n const schema = id(upstreamSchema(shard));\n return (\n createEventTriggerStatements(shard) +\n /*sql*/ `UPDATE ${schema}.\"shardConfig\" SET \"ddlDetection\" = true;`\n );\n}\n\n// Called in initial-sync to store the exact schema that was initially synced.\nexport async function addReplica(\n sql: PostgresDB,\n shard: ShardID,\n slot: string,\n replicaVersion: string,\n {tables, indexes}: PublishedSchema,\n initialSyncContext: JSONObject,\n) {\n const schema = upstreamSchema(shard);\n const synced: PublishedSchema = {tables, indexes};\n await sql`\n INSERT INTO ${sql(schema)}.replicas\n (\"slot\", \"version\", \"initialSchema\", \"initialSyncContext\")\n VALUES (${slot}, ${replicaVersion}, ${synced}, ${initialSyncContext})`;\n}\n\nexport async function getReplicaAtVersion(\n lc: LogContext,\n sql: PostgresDB,\n shard: ShardID,\n replicaVersion: string,\n context?: JSONObject,\n): Promise<Replica | null> {\n const schema = sql(upstreamSchema(shard));\n const result = await sql`\n SELECT\n replicas.\"slot\",\n replicas.\"version\",\n replicas.\"initialSchema\",\n replicas.\"initialSyncContext\",\n replicas.\"subscriberContext\",\n \"shardConfig\".\"publications\",\n \"shardConfig\".\"ddlDetection\"\n FROM ${schema}.replicas JOIN ${schema}.\"shardConfig\" ON true\n WHERE version = ${replicaVersion};\n `;\n if (result.length === 0) {\n // log out all the replicas and the joined shardConfig\n const allReplicas = await sql`\n SELECT slot, version, \"initialSyncContext\", \"subscriberContext\" \n FROM ${schema}.replicas`;\n lc.info?.(\n `Replica ${replicaVersion} ` +\n (context ? `(context: ${stringify(context)}) ` : '') +\n `not found in: ${stringify(allReplicas)}`,\n );\n return null;\n }\n return v.parse(result[0], replicaSchema, 'passthrough');\n}\n\nexport async function getInternalShardConfig(\n sql: PostgresDB,\n shard: ShardID,\n): Promise<InternalShardConfig> {\n const result = await sql`\n SELECT \"publications\", \"ddlDetection\"\n FROM ${sql(upstreamSchema(shard))}.\"shardConfig\";\n `;\n assert(\n result.length === 1,\n () => `Expected exactly one shardConfig row, got ${result.length}`,\n );\n return v.parse(result[0], internalShardConfigSchema, 'passthrough');\n}\n\n/**\n * Sets up and returns all publications (including internal ones) for\n * the given shard.\n */\nexport async function setupTablesAndReplication(\n lc: LogContext,\n sql: PostgresTransaction,\n requested: ShardConfig,\n) {\n const {publications} = requested;\n // Validate requested publications.\n for (const pub of publications) {\n validatePublicationName(pub);\n if (pub.startsWith('_')) {\n throw new Error(\n `Publication names starting with \"_\" are reserved for internal use.\\n` +\n `Please use a different name for publication \"${pub}\".`,\n );\n }\n }\n const allPublications: string[] = [];\n\n // Setup application publications.\n if (publications.length) {\n const results = await sql<{pubname: string}[]>`\n SELECT pubname from pg_publication WHERE pubname IN ${sql(\n publications,\n )}`.values();\n\n if (results.length !== publications.length) {\n throw new Error(\n `Unknown or invalid publications. Specified: [${publications}]. Found: [${results.flat()}]`,\n );\n }\n allPublications.push(...publications);\n } else {\n const defaultPublication = defaultPublicationName(\n requested.appID,\n requested.shardNum,\n );\n await sql`\n DROP PUBLICATION IF EXISTS ${sql(defaultPublication)}`;\n await sql`\n CREATE PUBLICATION ${sql(defaultPublication)} \n FOR TABLES IN SCHEMA public\n WITH (publish_via_partition_root = true)`;\n allPublications.push(defaultPublication);\n }\n\n const metadataPublication = metadataPublicationName(\n requested.appID,\n requested.shardNum,\n );\n allPublications.push(metadataPublication);\n\n const shard = {...requested, publications: allPublications};\n\n // Setup the global tables and shard tables / publications.\n await sql.unsafe(globalSetup(shard) + shardSetup(shard, metadataPublication));\n\n const pubs = await getPublicationInfo(sql, allPublications);\n await replicaIdentitiesForTablesWithoutPrimaryKeys(pubs)?.apply(lc, sql);\n\n await setupTriggers(lc, sql, shard);\n}\n\nexport async function setupTriggers(\n lc: LogContext,\n tx: PostgresTransaction,\n shard: ShardConfig,\n) {\n const schema = upstreamSchema(shard);\n const [{ddlDetection}] = await tx<InternalShardConfig[]> /*sql*/ `\n SELECT \"ddlDetection\" FROM ${tx(schema)}.\"shardConfig\"`;\n\n // The functions invoked by event triggers are installed even if\n // event triggers are not supported/allowed by the db provider.\n // This allows users to manually invoke the update_schemas() function\n // as a workaround.\n await tx.unsafe(createEventFunctionStatements(shard));\n try {\n await tx.savepoint(sub => sub.unsafe(triggerSetup(shard)));\n } catch (e) {\n if (ddlDetection) {\n // If ddlDetection has already been enabled, subsequent failures to\n // upgrade the trigger should be propagated rather than swallowed.\n throw e;\n }\n if (\n !(\n e instanceof postgres.PostgresError &&\n e.code === PG_INSUFFICIENT_PRIVILEGE\n )\n ) {\n throw e;\n }\n // If triggerSetup() fails, replication continues in ddlDetection=false mode.\n lc.warn?.(\n `Unable to create event triggers for schema change detection:\\n\\n` +\n `\"${e.hint ?? e.message}\"\\n\\n` +\n `Proceeding in degraded mode: schema changes will halt replication,\\n` +\n `requiring the replica to be reset (manually or with --auto-reset).`,\n );\n }\n}\n\nexport function validatePublications(\n lc: LogContext,\n published: PublicationInfo,\n) {\n // Verify that all publications export the proper events.\n published.publications.forEach(pub => {\n if (\n !pub.pubinsert ||\n !pub.pubupdate ||\n !pub.pubdelete ||\n !pub.pubtruncate\n ) {\n // TODO: Make APIError?\n throw new Error(\n `PUBLICATION ${pub.pubname} must publish insert, update, delete, and truncate`,\n );\n }\n });\n\n published.tables.forEach(table => validate(lc, table));\n}\n\ntype ReplicaIdentities = {\n apply(lc: LogContext, db: PostgresDB): Promise<void>;\n};\n\nexport function replicaIdentitiesForTablesWithoutPrimaryKeys(\n pubs: PublishedSchema,\n): ReplicaIdentities | undefined {\n const replicaIdentities: {\n schema: string;\n tableName: string;\n indexName: string;\n }[] = [];\n for (const table of pubs.tables) {\n if (!table.primaryKey?.length && table.replicaIdentity === Default) {\n // Look for an index that can serve as the REPLICA IDENTITY USING INDEX. It must be:\n // - UNIQUE\n // - NOT NULL columns\n // - not deferrable (i.e. isImmediate)\n // - not partial (are already filtered out)\n //\n // https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-REPLICA-IDENTITY\n const {schema, name: tableName} = table;\n for (const {columns, name: indexName} of pubs.indexes.filter(\n idx =>\n idx.schema === schema &&\n idx.tableName === tableName &&\n idx.unique &&\n idx.isImmediate,\n )) {\n if (Object.keys(columns).some(col => !table.columns[col].notNull)) {\n continue; // Only indexes with all NOT NULL columns are suitable.\n }\n replicaIdentities.push({schema, tableName, indexName});\n break;\n }\n }\n }\n\n if (replicaIdentities.length === 0) {\n return undefined;\n }\n return {\n apply: async (lc: LogContext, sql: PostgresDB) => {\n for (const {schema, tableName, indexName} of replicaIdentities) {\n lc.info?.(\n `setting \"${indexName}\" as the REPLICA IDENTITY for \"${tableName}\"`,\n );\n await sql`\n ALTER TABLE ${sql(schema)}.${sql(tableName)} \n REPLICA IDENTITY USING INDEX ${sql(indexName)}`;\n }\n },\n };\n}\n"],"mappings":"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"}
@@ -11,6 +11,7 @@ import { type ChangeStreamer, type Downstream, type SubscriberContext } from './
11
11
  import { type SnapshotMessage } from './snapshot.ts';
12
12
  type Options = {
13
13
  port: number;
14
+ keepaliveTimeoutMs: number | undefined;
14
15
  startupDelayMs: number;
15
16
  };
16
17
  export declare class ChangeStreamerHttpServer extends HttpService {