envio 2.31.0-alpha.0 → 2.31.0-alpha.2

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.
@@ -3,183 +3,48 @@ open Table
3
3
  module RowAction = {
4
4
  type t = SET | DELETE
5
5
  let variants = [SET, DELETE]
6
- let name = "ENTITY_HISTORY_ROW_ACTION"
6
+ let name = "ENVIO_HISTORY_CHANGE"
7
7
  let schema = S.enum(variants)
8
8
  }
9
9
 
10
- type historyFieldsGeneral<'a> = {
11
- chain_id: 'a,
12
- block_timestamp: 'a,
13
- block_number: 'a,
14
- log_index: 'a,
15
- }
16
-
17
- type historyFields = historyFieldsGeneral<int>
10
+ type entityUpdateAction<'entityType> =
11
+ | Set('entityType)
12
+ | Delete
18
13
 
19
- type entityIdOnly = {id: string}
20
- let entityIdOnlySchema = S.schema(s => {id: s.matches(S.string)})
21
- type entityData<'entity> = Delete(entityIdOnly) | Set('entity)
22
-
23
- type historyRow<'entity> = {
24
- current: historyFields,
25
- previous: option<historyFields>,
26
- entityData: entityData<'entity>,
27
- // In the event of a rollback, some entity updates may have been
28
- // been affected by a rollback diff. If there was no rollback diff
29
- // this will always be false.
30
- // If there was a rollback diff, this will be false in the case of a
31
- // new entity update (where entity affected is not present in the diff) b
32
- // but true if the update is related to an entity that is
33
- // currently present in the diff
34
- // Optional since it's discarded during parsing/serialization
35
- containsRollbackDiffChange?: bool,
14
+ type entityUpdate<'entityType> = {
15
+ entityId: string,
16
+ entityUpdateAction: entityUpdateAction<'entityType>,
17
+ checkpointId: int,
36
18
  }
37
19
 
38
- type previousHistoryFields = historyFieldsGeneral<option<int>>
39
-
40
- //For flattening the optional previous fields into their own individual nullable fields
41
- let previousHistoryFieldsSchema = S.object(s => {
42
- chain_id: s.field("previous_entity_history_chain_id", S.null(S.int)),
43
- block_timestamp: s.field("previous_entity_history_block_timestamp", S.null(S.int)),
44
- block_number: s.field("previous_entity_history_block_number", S.null(S.int)),
45
- log_index: s.field("previous_entity_history_log_index", S.null(S.int)),
46
- })
47
-
48
- let currentHistoryFieldsSchema = S.object(s => {
49
- chain_id: s.field("entity_history_chain_id", S.int),
50
- block_timestamp: s.field("entity_history_block_timestamp", S.int),
51
- block_number: s.field("entity_history_block_number", S.int),
52
- log_index: s.field("entity_history_log_index", S.int),
53
- })
54
-
55
- let makeHistoryRowSchema: S.t<'entity> => S.t<historyRow<'entity>> = entitySchema => {
56
- //Maps a schema object for the given entity with all fields nullable except for the id field
57
- //Keeps any original nullable fields
58
- let nullableEntitySchema: S.t<Js.Dict.t<unknown>> = S.schema(s =>
59
- switch entitySchema->S.classify {
60
- | Object({items}) =>
61
- let nulldict = Js.Dict.empty()
62
- items->Belt.Array.forEach(({location, schema}) => {
63
- let nullableFieldSchema = switch (location, schema->S.classify) {
64
- | ("id", _)
65
- | (_, Null(_)) => schema //TODO double check this works for array types
66
- | _ => S.null(schema)->S.toUnknown
67
- }
68
-
69
- nulldict->Js.Dict.set(location, s.matches(nullableFieldSchema))
70
- })
71
- nulldict
72
- | _ =>
73
- Js.Exn.raiseError(
74
- "Failed creating nullableEntitySchema. Expected an object schema for entity",
75
- )
76
- }
77
- )
78
-
79
- let previousWithNullFields = {
80
- chain_id: None,
81
- block_timestamp: None,
82
- block_number: None,
83
- log_index: None,
84
- }
20
+ // Prefix with envio_ to avoid colleasions
21
+ let changeFieldName = "envio_change"
22
+ let checkpointIdFieldName = "checkpoint_id"
85
23
 
24
+ let makeSetUpdateSchema: S.t<'entity> => S.t<entityUpdate<'entity>> = entitySchema => {
86
25
  S.object(s => {
26
+ s.tag(changeFieldName, RowAction.SET)
87
27
  {
88
- "current": s.flatten(currentHistoryFieldsSchema),
89
- "previous": s.flatten(previousHistoryFieldsSchema),
90
- "entityData": s.flatten(nullableEntitySchema),
91
- "action": s.field("action", RowAction.schema),
28
+ checkpointId: s.field(checkpointIdFieldName, S.int),
29
+ entityId: s.field("id", S.string),
30
+ entityUpdateAction: Set(s.flatten(entitySchema)),
92
31
  }
93
- })->S.transform(s => {
94
- parser: v => {
95
- current: v["current"],
96
- previous: switch v["previous"] {
97
- | {
98
- chain_id: Some(chain_id),
99
- block_timestamp: Some(block_timestamp),
100
- block_number: Some(block_number),
101
- log_index: Some(log_index),
102
- } =>
103
- Some({
104
- chain_id,
105
- block_timestamp,
106
- block_number,
107
- log_index,
108
- })
109
- | {chain_id: None, block_timestamp: None, block_number: None, log_index: None} => None
110
- | _ => s.fail("Unexpected mix of null and non-null values in previous history fields")
111
- },
112
- entityData: switch v["action"] {
113
- | SET => v["entityData"]->(Utils.magic: Js.Dict.t<unknown> => 'entity)->Set
114
- | DELETE =>
115
- let {id} = v["entityData"]->(Utils.magic: Js.Dict.t<unknown> => entityIdOnly)
116
- Delete({id: id})
117
- },
118
- },
119
- serializer: v => {
120
- let (entityData, action) = switch v.entityData {
121
- | Set(entityData) => (entityData->(Utils.magic: 'entity => Js.Dict.t<unknown>), RowAction.SET)
122
- | Delete(entityIdOnly) => (
123
- entityIdOnly->(Utils.magic: entityIdOnly => Js.Dict.t<unknown>),
124
- DELETE,
125
- )
126
- }
127
-
128
- {
129
- "current": v.current,
130
- "entityData": entityData,
131
- "action": action,
132
- "previous": switch v.previous {
133
- | Some(historyFields) =>
134
- historyFields->(Utils.magic: historyFields => previousHistoryFields) //Cast to previousHistoryFields (with "Some" field values)
135
- | None => previousWithNullFields
136
- },
137
- }
138
- },
139
32
  })
140
33
  }
141
34
 
142
35
  type t<'entity> = {
143
36
  table: table,
144
- makeInsertFnQuery: (~pgSchema: string) => string,
145
- schema: S.t<historyRow<'entity>>,
37
+ setUpdateSchema: S.t<entityUpdate<'entity>>,
146
38
  // Used for parsing
147
- schemaRows: S.t<array<historyRow<'entity>>>,
148
- insertFn: (Postgres.sql, Js.Json.t, ~shouldCopyCurrentEntity: bool) => promise<unit>,
39
+ setUpdateSchemaRows: S.t<array<entityUpdate<'entity>>>,
40
+ makeInsertDeleteUpdatesQuery: (~pgSchema: string) => string,
41
+ makeGetRollbackRemovedIdsQuery: (~pgSchema: string) => string,
42
+ makeGetRollbackRestoredEntitiesQuery: (~pgSchema: string) => string,
149
43
  }
150
44
 
151
- type entityInternal
152
-
153
- external castInternal: t<'entity> => t<entityInternal> = "%identity"
154
- external eval: string => 'a = "eval"
45
+ let historyTableName = (~entityName) => "envio_history_" ++ entityName
155
46
 
156
47
  let fromTable = (table: table, ~schema: S.t<'entity>): t<'entity> => {
157
- let entity_history_block_timestamp = "entity_history_block_timestamp"
158
- let entity_history_chain_id = "entity_history_chain_id"
159
- let entity_history_block_number = "entity_history_block_number"
160
- let entity_history_log_index = "entity_history_log_index"
161
-
162
- //NB: Ordered by hirarchy of event ordering
163
- let currentChangeFieldNames = [
164
- entity_history_block_timestamp,
165
- entity_history_chain_id,
166
- entity_history_block_number,
167
- entity_history_log_index,
168
- ]
169
-
170
- let currentHistoryFields =
171
- currentChangeFieldNames->Belt.Array.map(fieldName =>
172
- mkField(fieldName, Integer, ~fieldSchema=S.never, ~isPrimaryKey=true)
173
- )
174
-
175
- let previousChangeFieldNames =
176
- currentChangeFieldNames->Belt.Array.map(fieldName => "previous_" ++ fieldName)
177
-
178
- let previousHistoryFields =
179
- previousChangeFieldNames->Belt.Array.map(fieldName =>
180
- mkField(fieldName, Integer, ~fieldSchema=S.never, ~isNullable=true)
181
- )
182
-
183
48
  let id = "id"
184
49
 
185
50
  let dataFields = table.fields->Belt.Array.keepMap(field =>
@@ -202,118 +67,97 @@ let fromTable = (table: table, ~schema: S.t<'entity>): t<'entity> => {
202
67
  }
203
68
  )
204
69
 
205
- let actionFieldName = "action"
206
-
207
- let actionField = mkField(actionFieldName, Custom(RowAction.name), ~fieldSchema=S.never)
70
+ let actionField = mkField(changeFieldName, Custom(RowAction.name), ~fieldSchema=S.never)
208
71
 
209
- let serialField = mkField("serial", Serial, ~fieldSchema=S.never, ~isNullable=true, ~isIndex=true)
72
+ let checkpointIdField = mkField(
73
+ checkpointIdFieldName,
74
+ Integer,
75
+ ~fieldSchema=S.int,
76
+ ~isPrimaryKey=true,
77
+ )
210
78
 
211
- let dataFieldNames = dataFields->Belt.Array.map(field => field->getFieldName)
79
+ // let dataFieldNames = dataFields->Belt.Array.map(field => field->getFieldName)
212
80
 
213
- let originTableName = table.tableName
214
- let historyTableName = originTableName ++ "_history"
81
+ let entityTableName = table.tableName
82
+ let historyTableName = historyTableName(~entityName=entityTableName)
215
83
  //ignore composite indices
216
84
  let table = mkTable(
217
85
  historyTableName,
218
- ~fields=Belt.Array.concatMany([
219
- currentHistoryFields,
220
- previousHistoryFields,
221
- dataFields,
222
- [actionField, serialField],
223
- ]),
86
+ ~fields=dataFields->Belt.Array.concat([checkpointIdField, actionField]),
224
87
  )
225
88
 
226
- let insertFnName = `"insert_${table.tableName}"`
227
-
228
- let allFieldNamesDoubleQuoted =
229
- Belt.Array.concatMany([
230
- currentChangeFieldNames,
231
- previousChangeFieldNames,
232
- dataFieldNames,
233
- [actionFieldName],
234
- ])->Belt.Array.map(fieldName => `"${fieldName}"`)
235
-
236
- let makeInsertFnQuery = (~pgSchema) => {
237
- let historyRowArg = "history_row"
238
- let historyTablePath = `"${pgSchema}"."${historyTableName}"`
239
- let originTablePath = `"${pgSchema}"."${originTableName}"`
240
-
241
- let previousHistoryFieldsAreNullStr =
242
- previousChangeFieldNames
243
- ->Belt.Array.map(fieldName => `${historyRowArg}.${fieldName} IS NULL`)
244
- ->Js.Array2.joinWith(" OR ")
245
-
246
- let currentChangeFieldNamesCommaSeparated = currentChangeFieldNames->Js.Array2.joinWith(", ")
247
-
248
- let dataFieldNamesDoubleQuoted = dataFieldNames->Belt.Array.map(fieldName => `"${fieldName}"`)
249
- let dataFieldNamesCommaSeparated = dataFieldNamesDoubleQuoted->Js.Array2.joinWith(", ")
250
-
251
- `CREATE OR REPLACE FUNCTION ${insertFnName}(${historyRowArg} ${historyTablePath}, should_copy_current_entity BOOLEAN)
252
- RETURNS void AS $$
253
- DECLARE
254
- v_previous_record RECORD;
255
- v_origin_record RECORD;
256
- BEGIN
257
- -- Check if previous values are not provided
258
- IF ${previousHistoryFieldsAreNullStr} THEN
259
- -- Find the most recent record for the same id
260
- SELECT ${currentChangeFieldNamesCommaSeparated} INTO v_previous_record
261
- FROM ${historyTablePath}
262
- WHERE ${id} = ${historyRowArg}.${id}
263
- ORDER BY ${currentChangeFieldNames
264
- ->Belt.Array.map(fieldName => fieldName ++ " DESC")
265
- ->Js.Array2.joinWith(", ")}
266
- LIMIT 1;
267
-
268
- -- If a previous record exists, use its values
269
- IF FOUND THEN
270
- ${Belt.Array.zip(currentChangeFieldNames, previousChangeFieldNames)
271
- ->Belt.Array.map(((currentFieldName, previousFieldName)) => {
272
- `${historyRowArg}.${previousFieldName} := v_previous_record.${currentFieldName};`
273
- })
274
- ->Js.Array2.joinWith(" ")}
275
- ElSIF should_copy_current_entity THEN
276
- -- Check if a value for the id exists in the origin table and if so, insert a history row for it.
277
- SELECT ${dataFieldNamesCommaSeparated} FROM ${originTablePath} WHERE id = ${historyRowArg}.${id} INTO v_origin_record;
278
- IF FOUND THEN
279
- INSERT INTO ${historyTablePath} (${currentChangeFieldNamesCommaSeparated}, ${dataFieldNamesCommaSeparated}, "${actionFieldName}")
280
- -- SET the current change data fields to 0 since we don't know what they were
281
- -- and it doesn't matter provided they are less than any new values
282
- VALUES (${currentChangeFieldNames
283
- ->Belt.Array.map(_ => "0")
284
- ->Js.Array2.joinWith(", ")}, ${dataFieldNames
285
- ->Belt.Array.map(fieldName => `v_origin_record."${fieldName}"`)
286
- ->Js.Array2.joinWith(", ")}, 'SET');
287
-
288
- ${previousChangeFieldNames
289
- ->Belt.Array.map(previousFieldName => {
290
- `${historyRowArg}.${previousFieldName} := 0;`
291
- })
292
- ->Js.Array2.joinWith(" ")}
293
- END IF;
294
- END IF;
295
- END IF;
296
-
297
- INSERT INTO ${historyTablePath} (${allFieldNamesDoubleQuoted->Js.Array2.joinWith(", ")})
298
- VALUES (${allFieldNamesDoubleQuoted
299
- ->Belt.Array.map(fieldName => `${historyRowArg}.${fieldName}`)
300
- ->Js.Array2.joinWith(", ")});
301
- END;
302
- $$ LANGUAGE plpgsql;`
89
+ let setUpdateSchema = makeSetUpdateSchema(schema)
90
+
91
+ let makeInsertDeleteUpdatesQuery = {
92
+ // Get all field names for the INSERT statement
93
+ let allFieldNames = table.fields->Belt.Array.map(field => field->getFieldName)
94
+ let allFieldNamesStr =
95
+ allFieldNames->Belt.Array.map(name => `"${name}"`)->Js.Array2.joinWith(", ")
96
+
97
+ // Build the SELECT part: id from unnest, checkpoint_id from unnest, 'DELETE' for action, NULL for all other fields
98
+ let selectParts = allFieldNames->Belt.Array.map(fieldName => {
99
+ switch fieldName {
100
+ | "id" => "u.id"
101
+ | field if field == checkpointIdFieldName => "u.checkpoint_id"
102
+ | field if field == changeFieldName => "'DELETE'"
103
+ | _ => "NULL"
104
+ }
105
+ })
106
+ let selectPartsStr = selectParts->Js.Array2.joinWith(", ")
107
+ (~pgSchema) => {
108
+ `INSERT INTO "${pgSchema}"."${historyTableName}" (${allFieldNamesStr})
109
+ SELECT ${selectPartsStr}
110
+ FROM UNNEST($1::text[], $2::int[]) AS u(id, checkpoint_id)`
111
+ }
303
112
  }
304
113
 
305
- let insertFnString = `(sql, rowArgs, shouldCopyCurrentEntity) =>
306
- sql\`select ${insertFnName}(ROW(${allFieldNamesDoubleQuoted
307
- ->Belt.Array.map(fieldNameDoubleQuoted => `\${rowArgs[${fieldNameDoubleQuoted}]\}`)
308
- ->Js.Array2.joinWith(", ")}, NULL), --NULL argument for SERIAL field
309
- \${shouldCopyCurrentEntity});\``
310
-
311
- let insertFn: (Postgres.sql, Js.Json.t, ~shouldCopyCurrentEntity: bool) => promise<unit> =
312
- insertFnString->eval
114
+ // Get data field names for rollback queries (exclude changeFieldName and checkpointIdFieldName)
115
+ let dataFieldNames =
116
+ table.fields
117
+ ->Belt.Array.map(field => field->getFieldName)
118
+ ->Belt.Array.keep(fieldName =>
119
+ fieldName != changeFieldName && fieldName != checkpointIdFieldName
120
+ )
121
+ let dataFieldsCommaSeparated =
122
+ dataFieldNames->Belt.Array.map(name => `"${name}"`)->Js.Array2.joinWith(", ")
123
+
124
+ // Returns entity IDs that were created after the rollback target and have no history before it.
125
+ // These entities should be deleted during rollback.
126
+ let makeGetRollbackRemovedIdsQuery = (~pgSchema) => {
127
+ `SELECT DISTINCT id
128
+ FROM "${pgSchema}"."${historyTableName}"
129
+ WHERE "${checkpointIdFieldName}" > $1
130
+ AND NOT EXISTS (
131
+ SELECT 1
132
+ FROM "${pgSchema}"."${historyTableName}" h
133
+ WHERE h.id = "${historyTableName}".id
134
+ AND h."${checkpointIdFieldName}" <= $1
135
+ )`
136
+ }
313
137
 
314
- let schema = makeHistoryRowSchema(schema)
138
+ // Returns the most recent entity state for IDs that need to be restored during rollback.
139
+ // For each ID modified after the rollback target, retrieves its latest state at or before the target.
140
+ let makeGetRollbackRestoredEntitiesQuery = (~pgSchema) => {
141
+ `SELECT DISTINCT ON (id) ${dataFieldsCommaSeparated}
142
+ FROM "${pgSchema}"."${historyTableName}"
143
+ WHERE "${checkpointIdFieldName}" <= $1
144
+ AND EXISTS (
145
+ SELECT 1
146
+ FROM "${pgSchema}"."${historyTableName}" h
147
+ WHERE h.id = "${historyTableName}".id
148
+ AND h."${checkpointIdFieldName}" > $1
149
+ )
150
+ ORDER BY id, "${checkpointIdFieldName}" DESC`
151
+ }
315
152
 
316
- {table, makeInsertFnQuery, schema, schemaRows: S.array(schema), insertFn}
153
+ {
154
+ table,
155
+ setUpdateSchema,
156
+ setUpdateSchemaRows: S.array(setUpdateSchema),
157
+ makeInsertDeleteUpdatesQuery,
158
+ makeGetRollbackRemovedIdsQuery,
159
+ makeGetRollbackRestoredEntitiesQuery,
160
+ }
317
161
  }
318
162
 
319
163
  type safeReorgBlocks = {
@@ -323,60 +167,94 @@ type safeReorgBlocks = {
323
167
 
324
168
  // We want to keep only the minimum history needed to survive chain reorgs and delete everything older.
325
169
  // Each chain gives us a "safe block": we assume reorgs will never happen at that block.
170
+ // The latest checkpoint belonging to safe blocks of all chains is the safe checkpoint id.
326
171
  //
327
172
  // What we keep per entity id:
328
- // - The latest history row at or before the safe block (the "anchor"). This is the last state that could
329
- // ever be relevant during a rollback.
330
173
  // - If there are history rows in reorg threshold (after the safe block), we keep the anchor and delete all older rows.
331
174
  // - If there are no history rows in reorg threshold (after the safe block), even the anchor is redundant, so we delete it too.
175
+ // Anchor is the latest history row at or before the safe checkpoint id.
176
+ // This is the last state that could ever be relevant during a rollback.
332
177
  //
333
178
  // Why this is safe:
334
- // - Rollbacks will not cross the safe block, so rows older than the anchor can never be referenced again.
335
- // - If nothing changed in reorg threshold (after the safe block), the current state for that id can be reconstructed from the
179
+ // - Rollbacks will not cross the safe checkpoint id, so rows older than the anchor can never be referenced again.
180
+ // - If nothing changed in reorg threshold (after the safe checkpoint), the current state for that id can be reconstructed from the
336
181
  // origin table; we do not need a pre-safe anchor for it.
337
- //
338
- // Performance notes:
339
- // - Multi-chain batching: inputs are expanded with unnest, letting one prepared statement prune many chains and
340
- // enabling the planner to use indexes per chain_id efficiently.
341
- // - Minimal row touches: we only compute keep_serial per id and delete strictly older rows; this reduces write
342
- // amplification and vacuum pressure compared to broad time-based purges.
343
- // - Contention-awareness: the DELETE joins on ids first, narrowing target rows early to limit locking and buffer churn.
344
182
  let makePruneStaleEntityHistoryQuery = (~entityName, ~pgSchema) => {
345
- let historyTableName = entityName ++ "_history"
346
- let historyTableRef = `"${pgSchema}"."${historyTableName}"`
183
+ let historyTableRef = `"${pgSchema}"."${historyTableName(~entityName)}"`
347
184
 
348
- `WITH safe AS (
349
- SELECT s.chain_id, s.block_number
350
- FROM unnest($1::int[], $2::bigint[]) AS s(chain_id, block_number)
351
- ),
352
- max_before_safe AS (
353
- SELECT t.id, MAX(t.serial) AS keep_serial
354
- FROM ${historyTableRef} t
355
- JOIN safe s
356
- ON s.chain_id = t.entity_history_chain_id
357
- AND t.entity_history_block_number <= s.block_number
185
+ `WITH anchors AS (
186
+ SELECT t.id, MAX(t.${checkpointIdFieldName}) AS keep_checkpoint_id
187
+ FROM ${historyTableRef} t WHERE t.${checkpointIdFieldName} <= $1
358
188
  GROUP BY t.id
359
- ),
360
- post_safe AS (
361
- SELECT DISTINCT t.id
362
- FROM ${historyTableRef} t
363
- JOIN safe s
364
- ON s.chain_id = t.entity_history_chain_id
365
- AND t.entity_history_block_number > s.block_number
366
189
  )
367
190
  DELETE FROM ${historyTableRef} d
368
- USING max_before_safe m
369
- LEFT JOIN post_safe p ON p.id = m.id
370
- WHERE d.id = m.id
191
+ USING anchors a
192
+ WHERE d.id = a.id
371
193
  AND (
372
- d.serial < m.keep_serial
373
- OR (p.id IS NULL AND d.serial = m.keep_serial)
194
+ d.${checkpointIdFieldName} < a.keep_checkpoint_id
195
+ OR (
196
+ d.${checkpointIdFieldName} = a.keep_checkpoint_id AND
197
+ NOT EXISTS (
198
+ SELECT 1 FROM ${historyTableRef} ps
199
+ WHERE ps.id = d.id AND ps.${checkpointIdFieldName} > $1
200
+ )
201
+ )
374
202
  );`
375
203
  }
376
204
 
377
- let pruneStaleEntityHistory = (sql, ~entityName, ~pgSchema, ~safeReorgBlocks): promise<unit> => {
205
+ let pruneStaleEntityHistory = (sql, ~entityName, ~pgSchema, ~safeCheckpointId): promise<unit> => {
378
206
  sql->Postgres.preparedUnsafe(
379
207
  makePruneStaleEntityHistoryQuery(~entityName, ~pgSchema),
380
- (safeReorgBlocks.chainIds, safeReorgBlocks.blockNumbers)->Utils.magic,
208
+ [safeCheckpointId]->Utils.magic,
209
+ )
210
+ }
211
+
212
+ // If an entity doesn't have a history before the update
213
+ // we create it automatically with checkpoint_id 0
214
+ let makeBackfillHistoryQuery = (~pgSchema, ~entityName) => {
215
+ `WITH target_ids AS (
216
+ SELECT UNNEST($1::${(Text: Table.fieldType :> string)}[]) AS id
217
+ ),
218
+ missing_history AS (
219
+ SELECT e.*
220
+ FROM "${pgSchema}"."${entityName}" e
221
+ JOIN target_ids t ON e.id = t.id
222
+ LEFT JOIN "${pgSchema}"."${historyTableName(~entityName)}" h ON h.id = e.id
223
+ WHERE h.id IS NULL
224
+ )
225
+ INSERT INTO "${pgSchema}"."${historyTableName(~entityName)}"
226
+ SELECT *, 0 AS ${checkpointIdFieldName}, '${(RowAction.SET :> string)}' as ${changeFieldName}
227
+ FROM missing_history;`
228
+ }
229
+
230
+ let backfillHistory = (sql, ~pgSchema, ~entityName, ~ids: array<string>) => {
231
+ sql
232
+ ->Postgres.preparedUnsafe(makeBackfillHistoryQuery(~entityName, ~pgSchema), [ids]->Obj.magic)
233
+ ->Promise.ignoreValue
234
+ }
235
+
236
+ let insertDeleteUpdates = (
237
+ sql,
238
+ ~pgSchema,
239
+ ~entityHistory,
240
+ ~batchDeleteEntityIds,
241
+ ~batchDeleteCheckpointIds,
242
+ ) => {
243
+ sql
244
+ ->Postgres.preparedUnsafe(
245
+ entityHistory.makeInsertDeleteUpdatesQuery(~pgSchema),
246
+ (batchDeleteEntityIds, batchDeleteCheckpointIds)->Obj.magic,
247
+ )
248
+ ->Promise.ignoreValue
249
+ }
250
+
251
+ let rollback = (sql, ~pgSchema, ~entityName, ~rollbackTargetCheckpointId: int) => {
252
+ sql
253
+ ->Postgres.preparedUnsafe(
254
+ `DELETE FROM "${pgSchema}"."${historyTableName(
255
+ ~entityName,
256
+ )}" WHERE "${checkpointIdFieldName}" > $1;`,
257
+ [rollbackTargetCheckpointId]->Utils.magic,
381
258
  )
259
+ ->Promise.ignoreValue
382
260
  }