ponder 0.11.21 → 0.11.22

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 (123) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/esm/bin/commands/createViews.js +9 -20
  3. package/dist/esm/bin/commands/createViews.js.map +1 -1
  4. package/dist/esm/bin/commands/dev.js +1 -1
  5. package/dist/esm/bin/commands/dev.js.map +1 -1
  6. package/dist/esm/bin/commands/list.js +4 -7
  7. package/dist/esm/bin/commands/list.js.map +1 -1
  8. package/dist/esm/bin/commands/prune.js +9 -21
  9. package/dist/esm/bin/commands/prune.js.map +1 -1
  10. package/dist/esm/bin/commands/serve.js +1 -1
  11. package/dist/esm/bin/commands/serve.js.map +1 -1
  12. package/dist/esm/bin/commands/start.js +3 -3
  13. package/dist/esm/bin/commands/start.js.map +1 -1
  14. package/dist/esm/bin/utils/run.js +159 -180
  15. package/dist/esm/bin/utils/run.js.map +1 -1
  16. package/dist/esm/build/index.js +1 -48
  17. package/dist/esm/build/index.js.map +1 -1
  18. package/dist/esm/build/plugin.js +1 -1
  19. package/dist/esm/client/index.js +9 -13
  20. package/dist/esm/client/index.js.map +1 -1
  21. package/dist/esm/database/index.js +429 -141
  22. package/dist/esm/database/index.js.map +1 -1
  23. package/dist/esm/drizzle/index.js.map +1 -1
  24. package/dist/esm/drizzle/kit/index.js.map +1 -1
  25. package/dist/esm/drizzle/onchain.js +1 -8
  26. package/dist/esm/drizzle/onchain.js.map +1 -1
  27. package/dist/esm/graphql/index.js +16 -19
  28. package/dist/esm/graphql/index.js.map +1 -1
  29. package/dist/esm/graphql/middleware.js +7 -3
  30. package/dist/esm/graphql/middleware.js.map +1 -1
  31. package/dist/esm/indexing-store/cache.js +32 -26
  32. package/dist/esm/indexing-store/cache.js.map +1 -1
  33. package/dist/esm/indexing-store/historical.js +32 -23
  34. package/dist/esm/indexing-store/historical.js.map +1 -1
  35. package/dist/esm/indexing-store/index.js +18 -1
  36. package/dist/esm/indexing-store/index.js.map +1 -1
  37. package/dist/esm/indexing-store/realtime.js +140 -89
  38. package/dist/esm/indexing-store/realtime.js.map +1 -1
  39. package/dist/esm/internal/errors.js +0 -12
  40. package/dist/esm/internal/errors.js.map +1 -1
  41. package/dist/esm/server/index.js +2 -10
  42. package/dist/esm/server/index.js.map +1 -1
  43. package/dist/esm/sync-store/index.js +432 -403
  44. package/dist/esm/sync-store/index.js.map +1 -1
  45. package/dist/esm/utils/wait.js +0 -2
  46. package/dist/esm/utils/wait.js.map +1 -1
  47. package/dist/types/bin/commands/createViews.d.ts.map +1 -1
  48. package/dist/types/bin/commands/list.d.ts.map +1 -1
  49. package/dist/types/bin/commands/prune.d.ts.map +1 -1
  50. package/dist/types/bin/commands/start.d.ts +0 -2
  51. package/dist/types/bin/commands/start.d.ts.map +1 -1
  52. package/dist/types/bin/utils/run.d.ts +1 -1
  53. package/dist/types/bin/utils/run.d.ts.map +1 -1
  54. package/dist/types/build/index.d.ts +1 -1
  55. package/dist/types/build/index.d.ts.map +1 -1
  56. package/dist/types/client/index.d.ts.map +1 -1
  57. package/dist/types/database/index.d.ts +73 -25
  58. package/dist/types/database/index.d.ts.map +1 -1
  59. package/dist/types/drizzle/index.d.ts +3 -2
  60. package/dist/types/drizzle/index.d.ts.map +1 -1
  61. package/dist/types/drizzle/kit/index.d.ts +4 -3
  62. package/dist/types/drizzle/kit/index.d.ts.map +1 -1
  63. package/dist/types/drizzle/onchain.d.ts +5 -12
  64. package/dist/types/drizzle/onchain.d.ts.map +1 -1
  65. package/dist/types/graphql/index.d.ts +4 -2
  66. package/dist/types/graphql/index.d.ts.map +1 -1
  67. package/dist/types/graphql/middleware.d.ts +1 -1
  68. package/dist/types/graphql/middleware.d.ts.map +1 -1
  69. package/dist/types/indexing-store/cache.d.ts +12 -5
  70. package/dist/types/indexing-store/cache.d.ts.map +1 -1
  71. package/dist/types/indexing-store/historical.d.ts +7 -2
  72. package/dist/types/indexing-store/historical.d.ts.map +1 -1
  73. package/dist/types/indexing-store/index.d.ts +2 -4
  74. package/dist/types/indexing-store/index.d.ts.map +1 -1
  75. package/dist/types/indexing-store/realtime.d.ts +3 -1
  76. package/dist/types/indexing-store/realtime.d.ts.map +1 -1
  77. package/dist/types/internal/errors.d.ts +0 -4
  78. package/dist/types/internal/errors.d.ts.map +1 -1
  79. package/dist/types/server/index.d.ts +1 -1
  80. package/dist/types/server/index.d.ts.map +1 -1
  81. package/dist/types/sync/index.d.ts +1 -1
  82. package/dist/types/sync-store/index.d.ts.map +1 -1
  83. package/dist/types/utils/wait.d.ts.map +1 -1
  84. package/package.json +2 -2
  85. package/src/bin/commands/createViews.ts +26 -37
  86. package/src/bin/commands/dev.ts +1 -1
  87. package/src/bin/commands/list.ts +4 -7
  88. package/src/bin/commands/prune.ts +17 -31
  89. package/src/bin/commands/serve.ts +1 -1
  90. package/src/bin/commands/start.ts +3 -4
  91. package/src/bin/utils/run.ts +210 -256
  92. package/src/build/index.ts +2 -53
  93. package/src/build/plugin.ts +1 -1
  94. package/src/client/index.ts +10 -21
  95. package/src/database/index.ts +742 -331
  96. package/src/drizzle/index.ts +3 -2
  97. package/src/drizzle/kit/index.ts +5 -2
  98. package/src/drizzle/onchain.ts +2 -26
  99. package/src/graphql/index.ts +26 -31
  100. package/src/graphql/middleware.ts +7 -5
  101. package/src/indexing-store/cache.ts +52 -35
  102. package/src/indexing-store/historical.ts +40 -28
  103. package/src/indexing-store/index.ts +27 -2
  104. package/src/indexing-store/realtime.ts +220 -176
  105. package/src/internal/errors.ts +0 -9
  106. package/src/server/index.ts +3 -14
  107. package/src/sync-store/index.ts +997 -870
  108. package/src/utils/wait.ts +0 -1
  109. package/dist/esm/database/queryBuilder.js +0 -206
  110. package/dist/esm/database/queryBuilder.js.map +0 -1
  111. package/dist/esm/database/utils.js +0 -100
  112. package/dist/esm/database/utils.js.map +0 -1
  113. package/dist/esm/drizzle/json.js +0 -119
  114. package/dist/esm/drizzle/json.js.map +0 -1
  115. package/dist/types/database/queryBuilder.d.ts +0 -37
  116. package/dist/types/database/queryBuilder.d.ts.map +0 -1
  117. package/dist/types/database/utils.d.ts +0 -25
  118. package/dist/types/database/utils.d.ts.map +0 -1
  119. package/dist/types/drizzle/json.d.ts +0 -51
  120. package/dist/types/drizzle/json.d.ts.map +0 -1
  121. package/src/database/queryBuilder.ts +0 -319
  122. package/src/database/utils.ts +0 -140
  123. package/src/drizzle/json.ts +0 -154
@@ -1,5 +1,10 @@
1
- import { getTableNames } from "@/drizzle/index.js";
2
- import { sqlToReorgTableName } from "@/drizzle/kit/index.js";
1
+ import crypto from "node:crypto";
2
+ import { getPrimaryKeyColumns, getTableNames } from "@/drizzle/index.js";
3
+ import {
4
+ getColumnCasing,
5
+ getReorgTable,
6
+ sqlToReorgTableName,
7
+ } from "@/drizzle/kit/index.js";
3
8
  import type { Common } from "@/internal/common.js";
4
9
  import { NonRetryableError, ShutdownError } from "@/internal/errors.js";
5
10
  import type {
@@ -7,33 +12,63 @@ import type {
7
12
  IndexingBuild,
8
13
  NamespaceBuild,
9
14
  PreBuild,
15
+ Schema,
10
16
  SchemaBuild,
11
17
  } from "@/internal/types.js";
12
18
  import { buildMigrationProvider } from "@/sync-store/migrations.js";
13
- import * as PONDER_SYNC from "@/sync-store/schema.js";
14
- import { min } from "@/utils/checkpoint.js";
19
+ import * as ponderSyncSchema from "@/sync-store/schema.js";
20
+ import type { Drizzle } from "@/types/db.js";
21
+ import {
22
+ MAX_CHECKPOINT_STRING,
23
+ decodeCheckpoint,
24
+ min,
25
+ } from "@/utils/checkpoint.js";
15
26
  import { formatEta } from "@/utils/format.js";
16
27
  import { createPool, createReadonlyPool } from "@/utils/pg.js";
17
28
  import { createPglite, createPgliteKyselyDialect } from "@/utils/pglite.js";
18
29
  import { startClock } from "@/utils/timer.js";
19
30
  import { wait } from "@/utils/wait.js";
20
31
  import type { PGlite } from "@electric-sql/pglite";
21
- import { eq, getTableName, is, sql } from "drizzle-orm";
32
+ import {
33
+ type TableConfig,
34
+ eq,
35
+ getTableColumns,
36
+ getTableName,
37
+ is,
38
+ lte,
39
+ sql,
40
+ } from "drizzle-orm";
22
41
  import { drizzle as drizzleNodePg } from "drizzle-orm/node-postgres";
23
- import { PgTable, pgSchema, pgTable } from "drizzle-orm/pg-core";
42
+ import {
43
+ type PgQueryResultHKT,
44
+ PgTable,
45
+ type PgTableWithColumns,
46
+ type PgTransaction,
47
+ pgSchema,
48
+ pgTable,
49
+ } from "drizzle-orm/pg-core";
24
50
  import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
25
51
  import { Kysely, Migrator, PostgresDialect, WithSchemaPlugin } from "kysely";
26
52
  import type { Pool, PoolClient } from "pg";
27
53
  import prometheus from "prom-client";
28
- import { type QB, createQB, parseSqlError } from "./queryBuilder.js";
29
- import { revert } from "./utils.js";
30
54
 
31
55
  export type Database = {
32
56
  driver: PostgresDriver | PGliteDriver;
33
- syncQB: QB<typeof PONDER_SYNC>;
34
- adminQB: QB;
35
- userQB: QB;
36
- readonlyQB: QB;
57
+ qb: QueryBuilder;
58
+ PONDER_META: ReturnType<typeof getPonderMetaTable>;
59
+ PONDER_CHECKPOINT: ReturnType<typeof getPonderCheckpointTable>;
60
+ retry: <T>(fn: () => Promise<T>) => Promise<T>;
61
+ record: <T>(
62
+ options: { method: string; includeTraceLogs?: boolean },
63
+ fn: () => Promise<T>,
64
+ ) => Promise<T>;
65
+ wrap: <T>(
66
+ options: { method: string; includeTraceLogs?: boolean },
67
+ fn: () => Promise<T>,
68
+ ) => Promise<T>;
69
+ transaction: <T>(
70
+ fn: (client: PoolClient | PGlite, tx: Drizzle<Schema>) => Promise<T>,
71
+ ) => Promise<T>;
37
72
  /** Migrate the `ponder_sync` schema. */
38
73
  migrateSync(): Promise<void>;
39
74
  /**
@@ -44,6 +79,43 @@ export type Database = {
44
79
  migrate({
45
80
  buildId,
46
81
  }: Pick<IndexingBuild, "buildId">): Promise<CrashRecoveryCheckpoint>;
82
+ createIndexes(): Promise<void>;
83
+ createTriggers(): Promise<void>;
84
+ removeTriggers(): Promise<void>;
85
+ /**
86
+ * - "safe" checkpoint: The closest-to-tip finalized and completed checkpoint.
87
+ * - "latest" checkpoint: The closest-to-tip completed checkpoint.
88
+ *
89
+ * @dev It is an invariant that every "latest" checkpoint is specific to that chain.
90
+ * In other words, `chainId === latestCheckpoint.chainId`.
91
+ */
92
+ setCheckpoints: ({
93
+ checkpoints,
94
+ }: {
95
+ checkpoints: {
96
+ chainName: string;
97
+ chainId: number;
98
+ safeCheckpoint: string;
99
+ latestCheckpoint: string;
100
+ }[];
101
+ db: Drizzle<Schema>;
102
+ }) => Promise<void>;
103
+ getCheckpoints: () => Promise<
104
+ {
105
+ chainName: string;
106
+ chainId: number;
107
+ safeCheckpoint: string;
108
+ latestCheckpoint: string;
109
+ }[]
110
+ >;
111
+ setReady(): Promise<void>;
112
+ getReady(): Promise<boolean>;
113
+ revert(args: {
114
+ checkpoint: string;
115
+ tx: PgTransaction<PgQueryResultHKT, Schema>;
116
+ }): Promise<void>;
117
+ finalize(args: { checkpoint: string; db: Drizzle<Schema> }): Promise<void>;
118
+ commitBlock(args: { checkpoint: string; db: Drizzle<Schema> }): Promise<void>;
47
119
  };
48
120
 
49
121
  export const SCHEMATA = pgSchema("information_schema").table(
@@ -76,22 +148,27 @@ export type PonderApp = {
76
148
 
77
149
  const VERSION = "2";
78
150
 
79
- type PGliteDriver = {
80
- dialect: "pglite";
81
- instance: PGlite;
82
- };
151
+ type PGliteDriver = { instance: PGlite };
83
152
 
84
153
  type PostgresDriver = {
85
- dialect: "postgres";
86
- sync: Pool;
87
- admin: Pool;
154
+ internal: Pool;
88
155
  user: Pool;
156
+ sync: Pool;
89
157
  readonly: Pool;
90
158
  listen: PoolClient | undefined;
91
159
  };
92
160
 
93
- export const getPonderMetaTable = (schema?: string) => {
94
- if (schema === undefined || schema === "public") {
161
+ type QueryBuilder = {
162
+ /** For interacting with the sync schema (extract) */
163
+ sync: Drizzle<typeof ponderSyncSchema>;
164
+ /** For interacting with the user schema (transform) */
165
+ drizzle: Drizzle<Schema>;
166
+ /** For interacting with the user schema (load) */
167
+ drizzleReadonly: Drizzle<Schema>;
168
+ };
169
+
170
+ export const getPonderMetaTable = (schema: string) => {
171
+ if (schema === "public") {
95
172
  return pgTable("_ponder_meta", (t) => ({
96
173
  key: t.text().primaryKey().$type<"app">(),
97
174
  value: t.jsonb().$type<PonderApp>().notNull(),
@@ -104,15 +181,8 @@ export const getPonderMetaTable = (schema?: string) => {
104
181
  }));
105
182
  };
106
183
 
107
- /**
108
- * - "safe" checkpoint: The closest-to-tip finalized and completed checkpoint.
109
- * - "latest" checkpoint: The closest-to-tip completed checkpoint.
110
- *
111
- * @dev It is an invariant that every "latest" checkpoint is specific to that chain.
112
- * In other words, `chainId === latestCheckpoint.chainId`.
113
- */
114
- export const getPonderCheckpointTable = (schema?: string) => {
115
- if (schema === undefined || schema === "public") {
184
+ export const getPonderCheckpointTable = (schema: string) => {
185
+ if (schema === "public") {
116
186
  return pgTable("_ponder_checkpoint", (t) => ({
117
187
  chainName: t.text().primaryKey(),
118
188
  chainId: t.bigint({ mode: "number" }).notNull(),
@@ -150,10 +220,7 @@ export const createDatabase = async ({
150
220
  ////////
151
221
 
152
222
  let driver: PGliteDriver | PostgresDriver;
153
- let syncQB: QB<typeof PONDER_SYNC>;
154
- let adminQB: QB;
155
- let userQB: QB;
156
- let readonlyQB: QB;
223
+ let qb: Database["qb"];
157
224
 
158
225
  const dialect = preBuild.databaseConfig.kind;
159
226
 
@@ -171,7 +238,6 @@ export const createDatabase = async ({
171
238
 
172
239
  if (dialect === "pglite" || dialect === "pglite_test") {
173
240
  driver = {
174
- dialect: "pglite",
175
241
  instance:
176
242
  dialect === "pglite"
177
243
  ? createPglite(preBuild.databaseConfig.options)
@@ -182,7 +248,7 @@ export const createDatabase = async ({
182
248
  clearInterval(heartbeatInterval);
183
249
 
184
250
  if (["start", "dev"].includes(common.options.command)) {
185
- await adminQB("unlock")
251
+ await qb.drizzle
186
252
  .update(PONDER_META)
187
253
  .set({ value: sql`jsonb_set(value, '{is_locked}', to_jsonb(0))` });
188
254
  }
@@ -197,38 +263,20 @@ export const createDatabase = async ({
197
263
  );
198
264
  await driver.instance.query(`SET search_path TO "${namespace.schema}"`);
199
265
 
200
- syncQB = createQB(
201
- () =>
202
- drizzlePglite((driver as PGliteDriver).instance, {
203
- casing: "snake_case",
204
- schema: PONDER_SYNC,
205
- }),
206
- { common },
207
- );
208
- adminQB = createQB(
209
- () =>
210
- drizzlePglite((driver as PGliteDriver).instance, {
211
- casing: "snake_case",
212
- schema: schemaBuild.schema,
213
- }),
214
- { common, isAdmin: true },
215
- );
216
- userQB = createQB(
217
- () =>
218
- drizzlePglite((driver as PGliteDriver).instance, {
219
- casing: "snake_case",
220
- schema: schemaBuild.schema,
221
- }),
222
- { common },
223
- );
224
- readonlyQB = createQB(
225
- () =>
226
- drizzlePglite((driver as PGliteDriver).instance, {
227
- casing: "snake_case",
228
- schema: schemaBuild.schema,
229
- }),
230
- { common },
231
- );
266
+ qb = {
267
+ sync: drizzlePglite((driver as PGliteDriver).instance, {
268
+ casing: "snake_case",
269
+ schema: ponderSyncSchema,
270
+ }),
271
+ drizzle: drizzlePglite((driver as PGliteDriver).instance, {
272
+ casing: "snake_case",
273
+ schema: schemaBuild.schema,
274
+ }),
275
+ drizzleReadonly: drizzlePglite((driver as PGliteDriver).instance, {
276
+ casing: "snake_case",
277
+ schema: schemaBuild.schema,
278
+ }),
279
+ };
232
280
  } else {
233
281
  const internalMax = 2;
234
282
  const equalMax = Math.floor(
@@ -240,15 +288,7 @@ export const createDatabase = async ({
240
288
  : [equalMax, equalMax, equalMax];
241
289
 
242
290
  driver = {
243
- sync: createPool(
244
- {
245
- ...preBuild.databaseConfig.poolConfig,
246
- application_name: "ponder_sync",
247
- max: syncMax,
248
- },
249
- common.logger,
250
- ),
251
- admin: createPool(
291
+ internal: createPool(
252
292
  {
253
293
  ...preBuild.databaseConfig.poolConfig,
254
294
  application_name: `${namespace.schema}_internal`,
@@ -274,55 +314,41 @@ export const createDatabase = async ({
274
314
  common.logger,
275
315
  namespace.schema,
276
316
  ),
317
+ sync: createPool(
318
+ {
319
+ ...preBuild.databaseConfig.poolConfig,
320
+ application_name: "ponder_sync",
321
+ max: syncMax,
322
+ },
323
+ common.logger,
324
+ ),
277
325
  listen: undefined,
278
326
  } as PostgresDriver;
279
327
 
280
- await driver.admin.query(
328
+ await driver.internal.query(
281
329
  `CREATE SCHEMA IF NOT EXISTS "${namespace.schema}"`,
282
330
  );
283
331
 
284
- syncQB = createQB(
285
- () =>
286
- // @ts-expect-error
287
- drizzleNodePg(driver.sync, {
288
- casing: "snake_case",
289
- schema: PONDER_SYNC,
290
- }),
291
- { common },
292
- );
293
- adminQB = createQB(
294
- () =>
295
- // @ts-expect-error
296
- drizzleNodePg(driver.admin, {
297
- casing: "snake_case",
298
- schema: schemaBuild.schema,
299
- }),
300
- { common, isAdmin: true },
301
- );
302
- userQB = createQB(
303
- () =>
304
- // @ts-expect-error
305
- drizzleNodePg(driver.user, {
306
- casing: "snake_case",
307
- schema: schemaBuild.schema,
308
- }),
309
- { common },
310
- );
311
- readonlyQB = createQB(
312
- () =>
313
- // @ts-expect-error
314
- drizzleNodePg(driver.readonly, {
315
- casing: "snake_case",
316
- schema: schemaBuild.schema,
317
- }),
318
- { common },
319
- );
332
+ qb = {
333
+ sync: drizzleNodePg(driver.sync, {
334
+ casing: "snake_case",
335
+ schema: ponderSyncSchema,
336
+ }),
337
+ drizzle: drizzleNodePg(driver.user, {
338
+ casing: "snake_case",
339
+ schema: schemaBuild.schema,
340
+ }),
341
+ drizzleReadonly: drizzleNodePg(driver.readonly, {
342
+ casing: "snake_case",
343
+ schema: schemaBuild.schema,
344
+ }),
345
+ };
320
346
 
321
347
  common.shutdown.add(async () => {
322
348
  clearInterval(heartbeatInterval);
323
349
 
324
350
  if (["start", "dev"].includes(common.options.command)) {
325
- await adminQB("unlock")
351
+ await qb.drizzle
326
352
  .update(PONDER_META)
327
353
  .set({ value: sql`jsonb_set(value, '{is_locked}', to_jsonb(0))` });
328
354
  }
@@ -330,10 +356,10 @@ export const createDatabase = async ({
330
356
  const d = driver as PostgresDriver;
331
357
  d.listen?.release();
332
358
  await Promise.all([
333
- d.sync.end(),
334
- d.admin.end(),
359
+ d.internal.end(),
335
360
  d.user.end(),
336
361
  d.readonly.end(),
362
+ d.sync.end(),
337
363
  ]);
338
364
  });
339
365
 
@@ -348,8 +374,8 @@ export const createDatabase = async ({
348
374
  labelNames: ["pool", "kind"] as const,
349
375
  registers: [common.metrics.registry],
350
376
  collect() {
351
- this.set({ pool: "admin", kind: "idle" }, d.admin.idleCount);
352
- this.set({ pool: "admin", kind: "total" }, d.admin.totalCount);
377
+ this.set({ pool: "internal", kind: "idle" }, d.internal.idleCount);
378
+ this.set({ pool: "internal", kind: "total" }, d.internal.totalCount);
353
379
  this.set({ pool: "sync", kind: "idle" }, d.sync.idleCount);
354
380
  this.set({ pool: "sync", kind: "total" }, d.sync.totalCount);
355
381
  this.set({ pool: "user", kind: "idle" }, d.user.idleCount);
@@ -368,7 +394,7 @@ export const createDatabase = async ({
368
394
  labelNames: ["pool"] as const,
369
395
  registers: [common.metrics.registry],
370
396
  collect() {
371
- this.set({ pool: "admin" }, d.admin.waitingCount);
397
+ this.set({ pool: "internal" }, d.internal.waitingCount);
372
398
  this.set({ pool: "sync" }, d.sync.waitingCount);
373
399
  this.set({ pool: "user" }, d.user.waitingCount);
374
400
  this.set({ pool: "readonly" }, d.readonly.waitingCount);
@@ -377,116 +403,306 @@ export const createDatabase = async ({
377
403
  }
378
404
 
379
405
  const tables = Object.values(schemaBuild.schema).filter(
380
- (table): table is PgTable => is(table, PgTable),
406
+ (table): table is PgTableWithColumns<TableConfig> => is(table, PgTable),
381
407
  );
382
408
 
383
- return {
409
+ const database = {
384
410
  driver,
385
- syncQB,
386
- adminQB,
387
- userQB,
388
- readonlyQB,
389
- async migrateSync() {
390
- const kysely = new Kysely({
391
- dialect:
392
- dialect === "postgres"
393
- ? new PostgresDialect({ pool: (driver as PostgresDriver).admin })
394
- : createPgliteKyselyDialect((driver as PGliteDriver).instance),
395
- log(event) {
396
- if (event.level === "query") {
397
- common.metrics.ponder_postgres_query_total.inc({ pool: "migrate" });
411
+ qb,
412
+ PONDER_META,
413
+ PONDER_CHECKPOINT,
414
+ async retry(fn) {
415
+ const RETRY_COUNT = 9;
416
+ const BASE_DURATION = 125;
417
+
418
+ // First error thrown is often the most useful
419
+ let firstError: any;
420
+ let hasError = false;
421
+
422
+ for (let i = 0; i <= RETRY_COUNT; i++) {
423
+ try {
424
+ if (common.shutdown.isKilled) {
425
+ throw new ShutdownError();
398
426
  }
399
- },
400
- plugins: [new WithSchemaPlugin("ponder_sync")],
401
- });
402
427
 
403
- const migrationProvider = buildMigrationProvider(common.logger);
404
- const migrator = new Migrator({
405
- db: kysely,
406
- provider: migrationProvider,
407
- migrationTableSchema: "ponder_sync",
408
- });
428
+ const result = await fn();
429
+
430
+ if (common.shutdown.isKilled) {
431
+ throw new ShutdownError();
432
+ }
433
+ return result;
434
+ } catch (_error) {
435
+ const error = _error as Error;
436
+
437
+ if (common.shutdown.isKilled) {
438
+ throw new ShutdownError();
439
+ }
440
+
441
+ if (!hasError) {
442
+ hasError = true;
443
+ firstError = error;
444
+ }
445
+
446
+ if (error instanceof NonRetryableError) {
447
+ throw error;
448
+ }
449
+
450
+ if (i === RETRY_COUNT) {
451
+ throw firstError;
452
+ }
453
+
454
+ const duration = BASE_DURATION * 2 ** i;
455
+
456
+ await wait(duration);
457
+ }
458
+ }
459
+
460
+ throw "unreachable";
461
+ },
462
+ async record(options, fn) {
463
+ const endClock = startClock();
409
464
 
410
- for (let i = 0; i <= 9; i++) {
465
+ const id = crypto.randomUUID().slice(0, 8);
466
+ if (options.includeTraceLogs) {
467
+ common.logger.trace({
468
+ service: "database",
469
+ msg: `Started '${options.method}' database method (id=${id})`,
470
+ });
471
+ }
472
+
473
+ try {
474
+ if (common.shutdown.isKilled) {
475
+ throw new ShutdownError();
476
+ }
477
+
478
+ const result = await fn();
479
+ common.metrics.ponder_database_method_duration.observe(
480
+ { method: options.method },
481
+ endClock(),
482
+ );
483
+
484
+ if (common.shutdown.isKilled) {
485
+ throw new ShutdownError();
486
+ }
487
+ return result;
488
+ } catch (_error) {
489
+ const error = _error as Error;
490
+
491
+ if (common.shutdown.isKilled) {
492
+ throw new ShutdownError();
493
+ }
494
+
495
+ common.metrics.ponder_database_method_duration.observe(
496
+ { method: options.method },
497
+ endClock(),
498
+ );
499
+ common.metrics.ponder_database_method_error_total.inc({
500
+ method: options.method,
501
+ });
502
+
503
+ common.logger.warn({
504
+ service: "database",
505
+ msg: `Failed '${options.method}' database method (id=${id})`,
506
+ error,
507
+ });
508
+
509
+ throw error;
510
+ } finally {
511
+ if (options.includeTraceLogs) {
512
+ common.logger.trace({
513
+ service: "database",
514
+ msg: `Completed '${options.method}' database method in ${Math.round(endClock())}ms (id=${id})`,
515
+ });
516
+ }
517
+ }
518
+ },
519
+ async wrap(options, fn) {
520
+ const RETRY_COUNT = 9;
521
+ const BASE_DURATION = 125;
522
+
523
+ // First error thrown is often the most useful
524
+ let firstError: any;
525
+ let hasError = false;
526
+
527
+ for (let i = 0; i <= RETRY_COUNT; i++) {
411
528
  const endClock = startClock();
529
+
530
+ const id = crypto.randomUUID().slice(0, 8);
531
+ if (options.includeTraceLogs) {
532
+ common.logger.trace({
533
+ service: "database",
534
+ msg: `Started '${options.method}' database method (id=${id})`,
535
+ });
536
+ }
537
+
412
538
  try {
413
- const { error } = await migrator.migrateToLatest();
414
- if (error) throw error;
539
+ if (common.shutdown.isKilled) {
540
+ throw new ShutdownError();
541
+ }
415
542
 
543
+ const result = await fn();
416
544
  common.metrics.ponder_database_method_duration.observe(
417
- { method: "migrate_sync" },
545
+ { method: options.method },
418
546
  endClock(),
419
547
  );
420
548
 
421
- return;
549
+ if (common.shutdown.isKilled) {
550
+ throw new ShutdownError();
551
+ }
552
+ return result;
422
553
  } catch (_error) {
423
- const error = parseSqlError(_error);
554
+ const error = _error as Error;
424
555
 
425
556
  if (common.shutdown.isKilled) {
426
557
  throw new ShutdownError();
427
558
  }
428
559
 
429
560
  common.metrics.ponder_database_method_duration.observe(
430
- { method: "migrate_sync" },
561
+ { method: options.method },
431
562
  endClock(),
432
563
  );
433
564
  common.metrics.ponder_database_method_error_total.inc({
434
- method: "migrate_sync",
565
+ method: options.method,
435
566
  });
436
567
 
568
+ if (!hasError) {
569
+ hasError = true;
570
+ firstError = error;
571
+ }
572
+
437
573
  if (error instanceof NonRetryableError) {
438
574
  common.logger.warn({
439
575
  service: "database",
440
- msg: `Failed 'migrate_sync' database query`,
576
+ msg: `Failed '${options.method}' database method (id=${id})`,
441
577
  error,
442
578
  });
443
579
  throw error;
444
580
  }
445
581
 
446
- if (i === 9) {
582
+ if (i === RETRY_COUNT) {
447
583
  common.logger.warn({
448
584
  service: "database",
449
- msg: `Failed 'migrate_sync' database query after '${i + 1}' attempts`,
585
+ msg: `Failed '${options.method}' database method after '${i + 1}' attempts (id=${id})`,
450
586
  error,
451
587
  });
452
- throw error;
588
+ throw firstError;
453
589
  }
454
590
 
455
- const duration = 125 * 2 ** i;
591
+ const duration = BASE_DURATION * 2 ** i;
456
592
  common.logger.debug({
457
593
  service: "database",
458
- msg: `Failed 'migrate_sync' database query, retrying after ${duration} milliseconds`,
594
+ msg: `Failed '${options.method}' database method, retrying after ${duration} milliseconds (id=${id})`,
459
595
  error,
460
596
  });
461
597
  await wait(duration);
598
+ } finally {
599
+ if (options.includeTraceLogs) {
600
+ common.logger.trace({
601
+ service: "database",
602
+ msg: `Completed '${options.method}' database method in ${Math.round(endClock())}ms (id=${id})`,
603
+ });
604
+ }
462
605
  }
463
606
  }
607
+
608
+ throw "unreachable";
609
+ },
610
+ async transaction(fn) {
611
+ if (dialect === "postgres") {
612
+ const client = await (database.driver as { user: Pool }).user.connect();
613
+ try {
614
+ await client.query("BEGIN");
615
+ const tx = drizzleNodePg(client, {
616
+ casing: "snake_case",
617
+ schema: schemaBuild.schema,
618
+ });
619
+ const result = await fn(client, tx);
620
+ await client.query("COMMIT");
621
+ return result;
622
+ } catch (error) {
623
+ await client.query("ROLLBACK");
624
+ throw error;
625
+ } finally {
626
+ client.release();
627
+ }
628
+ } else {
629
+ const client = (database.driver as { instance: PGlite }).instance;
630
+ try {
631
+ await client.query("BEGIN");
632
+ const tx = drizzlePglite(client, {
633
+ casing: "snake_case",
634
+ schema: schemaBuild.schema,
635
+ });
636
+ const result = await fn(client, tx);
637
+ await client.query("COMMIT");
638
+ return result;
639
+ } catch (error) {
640
+ await client?.query("ROLLBACK");
641
+ throw error;
642
+ }
643
+ }
644
+ },
645
+ async migrateSync() {
646
+ await this.wrap(
647
+ { method: "migrateSyncStore", includeTraceLogs: true },
648
+ async () => {
649
+ const kysely = new Kysely({
650
+ dialect:
651
+ dialect === "postgres"
652
+ ? new PostgresDialect({
653
+ pool: (driver as PostgresDriver).internal,
654
+ })
655
+ : createPgliteKyselyDialect((driver as PGliteDriver).instance),
656
+ log(event) {
657
+ if (event.level === "query") {
658
+ common.metrics.ponder_postgres_query_total.inc({
659
+ pool: "migrate",
660
+ });
661
+ }
662
+ },
663
+ plugins: [new WithSchemaPlugin("ponder_sync")],
664
+ });
665
+
666
+ const migrationProvider = buildMigrationProvider(common.logger);
667
+ const migrator = new Migrator({
668
+ db: kysely,
669
+ provider: migrationProvider,
670
+ migrationTableSchema: "ponder_sync",
671
+ });
672
+
673
+ const { error } = await migrator.migrateToLatest();
674
+ if (error) throw error;
675
+ },
676
+ );
464
677
  },
465
678
  async migrate({ buildId }) {
466
- await adminQB("create_meta_table").execute(
467
- sql.raw(`
679
+ await this.wrap(
680
+ { method: "createPonderSystemTables", includeTraceLogs: true },
681
+ async () => {
682
+ await qb.drizzle.execute(
683
+ sql.raw(`
468
684
  CREATE TABLE IF NOT EXISTS "${namespace.schema}"."_ponder_meta" (
469
685
  "key" TEXT PRIMARY KEY,
470
686
  "value" JSONB NOT NULL
471
687
  )`),
472
- );
688
+ );
473
689
 
474
- await adminQB("create_checkpoint_table").execute(
475
- sql.raw(`
690
+ await qb.drizzle.execute(
691
+ sql.raw(`
476
692
  CREATE TABLE IF NOT EXISTS "${namespace.schema}"."_ponder_checkpoint" (
477
693
  "chain_name" TEXT PRIMARY KEY,
478
694
  "chain_id" INTEGER NOT NULL,
479
695
  "safe_checkpoint" VARCHAR(75) NOT NULL,
480
696
  "latest_checkpoint" VARCHAR(75) NOT NULL
481
697
  )`),
482
- );
698
+ );
483
699
 
484
- const trigger = "status_trigger";
485
- const notification = "status_notify()";
486
- const channel = `${namespace.schema}_status_channel`;
700
+ const trigger = "status_trigger";
701
+ const notification = "status_notify()";
702
+ const channel = `${namespace.schema}_status_channel`;
487
703
 
488
- await adminQB("create_system_notification").execute(
489
- sql.raw(`
704
+ await qb.drizzle.execute(
705
+ sql.raw(`
490
706
  CREATE OR REPLACE FUNCTION "${namespace.schema}".${notification}
491
707
  RETURNS TRIGGER
492
708
  LANGUAGE plpgsql
@@ -496,20 +712,22 @@ NOTIFY "${channel}";
496
712
  RETURN NULL;
497
713
  END;
498
714
  $$;`),
499
- );
715
+ );
500
716
 
501
- await adminQB("create_system_trigger").execute(
502
- sql.raw(`
717
+ await qb.drizzle.execute(
718
+ sql.raw(`
503
719
  CREATE OR REPLACE TRIGGER "${trigger}"
504
720
  AFTER INSERT OR UPDATE OR DELETE
505
721
  ON "${namespace.schema}"._ponder_checkpoint
506
722
  FOR EACH STATEMENT
507
723
  EXECUTE PROCEDURE "${namespace.schema}".${notification};`),
724
+ );
725
+ },
508
726
  );
509
727
 
510
- const createTables = async (tx: QB) => {
728
+ const createTables = async (tx: Drizzle<Schema>) => {
511
729
  for (let i = 0; i < schemaBuild.statements.tables.sql.length; i++) {
512
- await tx()
730
+ await tx
513
731
  .execute(sql.raw(schemaBuild.statements.tables.sql[i]!))
514
732
  .catch((_error) => {
515
733
  const error = _error as Error;
@@ -523,9 +741,9 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`),
523
741
  }
524
742
  };
525
743
 
526
- const createEnums = async (tx: QB) => {
744
+ const createEnums = async (tx: Drizzle<Schema>) => {
527
745
  for (let i = 0; i < schemaBuild.statements.enums.sql.length; i++) {
528
- await tx()
746
+ await tx
529
747
  .execute(sql.raw(schemaBuild.statements.enums.sql[i]!))
530
748
  .catch((_error) => {
531
749
  const error = _error as Error;
@@ -540,179 +758,178 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`),
540
758
  };
541
759
 
542
760
  const tryAcquireLockAndMigrate = () =>
543
- adminQB("migrate").transaction(async (tx) => {
544
- // Note: All ponder versions are compatible with the next query (every version of the "_ponder_meta" table have the same columns)
545
-
546
- const previousApp = await tx()
547
- .select({ value: PONDER_META.value })
548
- .from(PONDER_META)
549
- .where(eq(PONDER_META.key, "app"))
550
- .then((result) => result[0]?.value);
551
-
552
- const metadata = {
553
- version: VERSION,
554
- build_id: buildId,
555
- table_names: tables.map(getTableName),
556
- is_dev: common.options.command === "dev" ? 1 : 0,
557
- is_locked: 1,
558
- is_ready: 0,
559
- heartbeat_at: Date.now(),
560
- } satisfies PonderApp;
561
-
562
- if (previousApp === undefined) {
563
- await createEnums(tx);
564
- await createTables(tx);
565
-
566
- common.logger.info({
567
- service: "database",
568
- msg: `Created tables [${tables.map(getTableName).join(", ")}]`,
569
- });
570
-
571
- await tx()
572
- .insert(PONDER_META)
573
- .values({ key: "app", value: metadata });
574
- return {
575
- status: "success",
576
- crashRecoveryCheckpoint: undefined,
577
- } as const;
578
- }
761
+ this.wrap({ method: "migrate", includeTraceLogs: true }, () =>
762
+ qb.drizzle.transaction(async (tx) => {
763
+ // Note: All ponder versions are compatible with the next query (every version of the "_ponder_meta" table have the same columns)
764
+
765
+ const previousApp = await tx
766
+ .select({ value: PONDER_META.value })
767
+ .from(PONDER_META)
768
+ .where(eq(PONDER_META.key, "app"))
769
+ .then((result) => result[0]?.value);
770
+
771
+ const metadata = {
772
+ version: VERSION,
773
+ build_id: buildId,
774
+ table_names: tables.map(getTableName),
775
+ is_dev: common.options.command === "dev" ? 1 : 0,
776
+ is_locked: 1,
777
+ is_ready: 0,
778
+ heartbeat_at: Date.now(),
779
+ } satisfies PonderApp;
780
+
781
+ if (previousApp === undefined) {
782
+ await createEnums(tx);
783
+ await createTables(tx);
784
+
785
+ common.logger.info({
786
+ service: "database",
787
+ msg: `Created tables [${tables.map(getTableName).join(", ")}]`,
788
+ });
789
+
790
+ await tx
791
+ .insert(PONDER_META)
792
+ .values({ key: "app", value: metadata });
793
+ return {
794
+ status: "success",
795
+ crashRecoveryCheckpoint: undefined,
796
+ } as const;
797
+ }
579
798
 
580
- if (
581
- previousApp.is_dev === 1 ||
582
- (process.env.PONDER_EXPERIMENTAL_DB === "platform" &&
583
- previousApp.build_id !== buildId)
584
- ) {
585
- for (const table of previousApp.table_names) {
586
- await tx().execute(
799
+ if (
800
+ previousApp.is_dev === 1 ||
801
+ (process.env.PONDER_EXPERIMENTAL_DB === "platform" &&
802
+ previousApp.build_id !== buildId)
803
+ ) {
804
+ for (const table of previousApp.table_names) {
805
+ await tx.execute(
806
+ sql.raw(
807
+ `DROP TABLE IF EXISTS "${namespace.schema}"."${table}" CASCADE`,
808
+ ),
809
+ );
810
+ await tx.execute(
811
+ sql.raw(
812
+ `DROP TABLE IF EXISTS "${namespace.schema}"."${sqlToReorgTableName(table)}" CASCADE`,
813
+ ),
814
+ );
815
+ }
816
+ for (const enumName of schemaBuild.statements.enums.json) {
817
+ await tx.execute(
818
+ sql.raw(
819
+ `DROP TYPE IF EXISTS "${namespace.schema}"."${enumName.name}"`,
820
+ ),
821
+ );
822
+ }
823
+
824
+ await tx.execute(
587
825
  sql.raw(
588
- `DROP TABLE IF EXISTS "${namespace.schema}"."${table}" CASCADE`,
826
+ `TRUNCATE TABLE "${namespace.schema}"."${getTableName(PONDER_CHECKPOINT)}" CASCADE`,
589
827
  ),
590
828
  );
591
- await tx().execute(
592
- sql.raw(
593
- `DROP TABLE IF EXISTS "${namespace.schema}"."${sqlToReorgTableName(table)}" CASCADE`,
594
- ),
829
+
830
+ await createEnums(tx);
831
+ await createTables(tx);
832
+
833
+ common.logger.info({
834
+ service: "database",
835
+ msg: `Created tables [${tables.map(getTableName).join(", ")}]`,
836
+ });
837
+
838
+ await tx.update(PONDER_META).set({ value: metadata });
839
+ return {
840
+ status: "success",
841
+ crashRecoveryCheckpoint: undefined,
842
+ } as const;
843
+ }
844
+
845
+ // Note: ponder <=0.8 will evaluate this as true because the version is undefined
846
+ if (previousApp.version !== VERSION) {
847
+ const error = new NonRetryableError(
848
+ `Schema '${namespace.schema}' was previously used by a Ponder app with a different minor version. Drop the schema first, or use a different schema. Read more: https://ponder.sh/docs/database#database-schema`,
595
849
  );
850
+ error.stack = undefined;
851
+ throw error;
596
852
  }
597
- for (const enumName of schemaBuild.statements.enums.json) {
598
- await tx().execute(
599
- sql.raw(
600
- `DROP TYPE IF EXISTS "${namespace.schema}"."${enumName.name}"`,
601
- ),
853
+
854
+ if (
855
+ common.options.command === "dev" ||
856
+ previousApp.build_id !== buildId
857
+ ) {
858
+ const error = new NonRetryableError(
859
+ `Schema '${namespace.schema}' was previously used by a different Ponder app. Drop the schema first, or use a different schema. Read more: https://ponder.sh/docs/database#database-schema`,
602
860
  );
861
+ error.stack = undefined;
862
+ throw error;
603
863
  }
604
864
 
605
- await tx().execute(
606
- sql.raw(
607
- `TRUNCATE TABLE "${namespace.schema}"."${getTableName(PONDER_CHECKPOINT)}" CASCADE`,
608
- ),
609
- );
865
+ const expiry =
866
+ previousApp.heartbeat_at +
867
+ common.options.databaseHeartbeatTimeout;
868
+
869
+ const isAppUnlocked =
870
+ previousApp.is_locked === 0 || expiry <= Date.now();
610
871
 
611
- await createEnums(tx);
612
- await createTables(tx);
872
+ if (isAppUnlocked === false) {
873
+ return { status: "locked", expiry } as const;
874
+ }
613
875
 
614
876
  common.logger.info({
615
877
  service: "database",
616
- msg: `Created tables [${tables.map(getTableName).join(", ")}]`,
878
+ msg: `Detected crash recovery for build '${buildId}' in schema '${namespace.schema}' last active ${formatEta(Date.now() - previousApp.heartbeat_at)} ago`,
617
879
  });
618
880
 
619
- await tx().update(PONDER_META).set({ value: metadata });
620
- return {
621
- status: "success",
622
- crashRecoveryCheckpoint: undefined,
623
- } as const;
624
- }
625
-
626
- // Note: ponder <=0.8 will evaluate this as true because the version is undefined
627
- if (previousApp.version !== VERSION) {
628
- const error = new NonRetryableError(
629
- `Schema '${namespace.schema}' was previously used by a Ponder app with a different minor version. Drop the schema first, or use a different schema. Read more: https://ponder.sh/docs/database#database-schema`,
630
- );
631
- error.stack = undefined;
632
- throw error;
633
- }
634
-
635
- if (
636
- common.options.command === "dev" ||
637
- previousApp.build_id !== buildId
638
- ) {
639
- const error = new NonRetryableError(
640
- `Schema '${namespace.schema}' was previously used by a different Ponder app. Drop the schema first, or use a different schema. Read more: https://ponder.sh/docs/database#database-schema`,
641
- );
642
- error.stack = undefined;
643
- throw error;
644
- }
645
-
646
- const expiry =
647
- previousApp.heartbeat_at + common.options.databaseHeartbeatTimeout;
648
-
649
- const isAppUnlocked =
650
- previousApp.is_locked === 0 || expiry <= Date.now();
881
+ const checkpoints = await tx.select().from(PONDER_CHECKPOINT);
882
+ const crashRecoveryCheckpoint =
883
+ checkpoints.length === 0
884
+ ? undefined
885
+ : checkpoints.map((c) => ({
886
+ chainId: c.chainId,
887
+ checkpoint: c.safeCheckpoint,
888
+ }));
889
+
890
+ if (previousApp.is_ready === 0) {
891
+ await tx.update(PONDER_META).set({ value: metadata });
892
+ return { status: "success", crashRecoveryCheckpoint } as const;
893
+ }
651
894
 
652
- if (isAppUnlocked === false) {
653
- return { status: "locked", expiry } as const;
654
- }
895
+ // Remove triggers
655
896
 
656
- common.logger.info({
657
- service: "database",
658
- msg: `Detected crash recovery for build '${buildId}' in schema '${namespace.schema}' last active ${formatEta(Date.now() - previousApp.heartbeat_at)} ago`,
659
- });
897
+ for (const table of tables) {
898
+ await tx.execute(
899
+ sql.raw(
900
+ `DROP TRIGGER IF EXISTS "${getTableNames(table).trigger}" ON "${namespace.schema}"."${getTableName(table)}"`,
901
+ ),
902
+ );
903
+ }
660
904
 
661
- const checkpoints = await tx().select().from(PONDER_CHECKPOINT);
662
- const crashRecoveryCheckpoint =
663
- checkpoints.length === 0
664
- ? undefined
665
- : checkpoints.map((c) => ({
666
- chainId: c.chainId,
667
- checkpoint: c.safeCheckpoint,
668
- }));
669
-
670
- if (previousApp.is_ready === 0) {
671
- await tx().update(PONDER_META).set({ value: metadata });
672
- return { status: "success", crashRecoveryCheckpoint } as const;
673
- }
905
+ // Remove indexes
674
906
 
675
- // Remove triggers
907
+ for (const indexStatement of schemaBuild.statements.indexes.json) {
908
+ await tx.execute(
909
+ sql.raw(
910
+ `DROP INDEX IF EXISTS "${namespace.schema}"."${indexStatement.data.name}"`,
911
+ ),
912
+ );
913
+ common.logger.debug({
914
+ service: "database",
915
+ msg: `Dropped index '${indexStatement.data.name}' in schema '${namespace.schema}'`,
916
+ });
917
+ }
676
918
 
677
- for (const table of tables) {
678
- await tx().execute(
679
- sql.raw(
680
- `DROP TRIGGER IF EXISTS "${getTableNames(table).trigger}" ON "${namespace.schema}"."${getTableName(table)}"`,
681
- ),
919
+ // Note: it is an invariant that checkpoints.length > 0;
920
+ const revertCheckpoint = min(
921
+ ...checkpoints.map((c) => c.safeCheckpoint),
682
922
  );
683
- }
684
923
 
685
- // Remove indexes
924
+ await this.revert({ checkpoint: revertCheckpoint, tx });
686
925
 
687
- for (const indexStatement of schemaBuild.statements.indexes.json) {
688
- await tx().execute(
689
- sql.raw(
690
- `DROP INDEX IF EXISTS "${namespace.schema}"."${indexStatement.data.name}"`,
691
- ),
692
- );
693
- common.logger.debug({
694
- service: "database",
695
- msg: `Dropped index '${indexStatement.data.name}' in schema '${namespace.schema}'`,
696
- });
697
- }
698
-
699
- // Note: it is an invariant that checkpoints.length > 0;
700
- const revertCheckpoint = min(
701
- ...checkpoints.map((c) => c.safeCheckpoint),
702
- );
926
+ // Note: We don't update the `_ponder_checkpoint` table here, instead we wait for it to be updated
927
+ // in the runtime script.
703
928
 
704
- await Promise.all(
705
- tables.map((table) =>
706
- revert(tx, { checkpoint: revertCheckpoint, table }),
707
- ),
708
- );
709
-
710
- // Note: We don't update the `_ponder_checkpoint` table here, instead we wait for it to be updated
711
- // in the runtime script.
712
-
713
- await tx().update(PONDER_META).set({ value: metadata });
714
- return { status: "success", crashRecoveryCheckpoint } as const;
715
- });
929
+ await tx.update(PONDER_META).set({ value: metadata });
930
+ return { status: "success", crashRecoveryCheckpoint } as const;
931
+ }),
932
+ );
716
933
 
717
934
  let result = await tryAcquireLockAndMigrate();
718
935
  if (result.status === "locked") {
@@ -742,11 +959,9 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`),
742
959
  try {
743
960
  const heartbeat = Date.now();
744
961
 
745
- await adminQB("heartbeat")
746
- .update(PONDER_META)
747
- .set({
748
- value: sql`jsonb_set(value, '{heartbeat_at}', ${heartbeat})`,
749
- });
962
+ await qb.drizzle.update(PONDER_META).set({
963
+ value: sql`jsonb_set(value, '{heartbeat_at}', ${heartbeat})`,
964
+ });
750
965
 
751
966
  common.logger.trace({
752
967
  service: "database",
@@ -766,5 +981,201 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`),
766
981
 
767
982
  return result.crashRecoveryCheckpoint;
768
983
  },
769
- };
984
+ async createIndexes() {
985
+ for (const statement of schemaBuild.statements.indexes.sql) {
986
+ await this.wrap({ method: "createIndexes" }, async () => {
987
+ await qb.drizzle.transaction(async (tx) => {
988
+ await tx.execute("SET statement_timeout = 3600000;"); // 60 minutes
989
+ await tx.execute(statement);
990
+ });
991
+ });
992
+ }
993
+ },
994
+ async createTriggers() {
995
+ await this.wrap(
996
+ { method: "createTriggers", includeTraceLogs: true },
997
+ async () => {
998
+ for (const table of tables) {
999
+ const columns = getTableColumns(table);
1000
+
1001
+ const columnNames = Object.values(columns).map(
1002
+ (column) => `"${getColumnCasing(column, "snake_case")}"`,
1003
+ );
1004
+
1005
+ await qb.drizzle.execute(
1006
+ sql.raw(`
1007
+ CREATE OR REPLACE FUNCTION "${namespace.schema}".${getTableNames(table).triggerFn}
1008
+ RETURNS TRIGGER AS $$
1009
+ BEGIN
1010
+ IF TG_OP = 'INSERT' THEN
1011
+ INSERT INTO "${namespace.schema}"."${getTableName(getReorgTable(table))}" (${columnNames.join(",")}, operation, checkpoint)
1012
+ VALUES (${columnNames.map((name) => `NEW.${name}`).join(",")}, 0, '${MAX_CHECKPOINT_STRING}');
1013
+ ELSIF TG_OP = 'UPDATE' THEN
1014
+ INSERT INTO "${namespace.schema}"."${getTableName(getReorgTable(table))}" (${columnNames.join(",")}, operation, checkpoint)
1015
+ VALUES (${columnNames.map((name) => `OLD.${name}`).join(",")}, 1, '${MAX_CHECKPOINT_STRING}');
1016
+ ELSIF TG_OP = 'DELETE' THEN
1017
+ INSERT INTO "${namespace.schema}"."${getTableName(getReorgTable(table))}" (${columnNames.join(",")}, operation, checkpoint)
1018
+ VALUES (${columnNames.map((name) => `OLD.${name}`).join(",")}, 2, '${MAX_CHECKPOINT_STRING}');
1019
+ END IF;
1020
+ RETURN NULL;
1021
+ END;
1022
+ $$ LANGUAGE plpgsql`),
1023
+ );
1024
+
1025
+ await qb.drizzle.execute(
1026
+ sql.raw(`
1027
+ CREATE OR REPLACE TRIGGER "${getTableNames(table).trigger}"
1028
+ AFTER INSERT OR UPDATE OR DELETE ON "${namespace.schema}"."${getTableName(table)}"
1029
+ FOR EACH ROW EXECUTE FUNCTION "${namespace.schema}".${getTableNames(table).triggerFn};
1030
+ `),
1031
+ );
1032
+ }
1033
+ },
1034
+ );
1035
+ },
1036
+ async removeTriggers() {
1037
+ await this.wrap(
1038
+ { method: "removeTriggers", includeTraceLogs: true },
1039
+ async () => {
1040
+ for (const table of tables) {
1041
+ await qb.drizzle.execute(
1042
+ sql.raw(
1043
+ `DROP TRIGGER IF EXISTS "${getTableNames(table).trigger}" ON "${namespace.schema}"."${getTableName(table)}"`,
1044
+ ),
1045
+ );
1046
+ }
1047
+ },
1048
+ );
1049
+ },
1050
+ async setCheckpoints({ checkpoints, db }) {
1051
+ if (checkpoints.length === 0) return;
1052
+
1053
+ return this.wrap({ method: "setCheckpoints" }, async () => {
1054
+ await db
1055
+ .insert(PONDER_CHECKPOINT)
1056
+ .values(checkpoints)
1057
+ .onConflictDoUpdate({
1058
+ target: PONDER_CHECKPOINT.chainName,
1059
+ set: {
1060
+ safeCheckpoint: sql`excluded.safe_checkpoint`,
1061
+ latestCheckpoint: sql`excluded.latest_checkpoint`,
1062
+ },
1063
+ });
1064
+ });
1065
+ },
1066
+ getCheckpoints() {
1067
+ return this.wrap({ method: "getCheckpoints" }, () =>
1068
+ qb.drizzle.select().from(PONDER_CHECKPOINT),
1069
+ );
1070
+ },
1071
+ setReady() {
1072
+ return this.wrap({ method: "setReady" }, async () => {
1073
+ await qb.drizzle
1074
+ .update(PONDER_META)
1075
+ .set({ value: sql`jsonb_set(value, '{is_ready}', to_jsonb(1))` });
1076
+ });
1077
+ },
1078
+ getReady() {
1079
+ return this.wrap({ method: "getReady" }, async () => {
1080
+ return qb.drizzle
1081
+ .select()
1082
+ .from(PONDER_META)
1083
+ .then((result) => result[0]?.value.is_ready === 1 ?? false);
1084
+ });
1085
+ },
1086
+ async revert({ checkpoint, tx }) {
1087
+ await this.record({ method: "revert", includeTraceLogs: true }, () =>
1088
+ Promise.all(
1089
+ tables.map(async (table) => {
1090
+ const primaryKeyColumns = getPrimaryKeyColumns(table);
1091
+
1092
+ const result = await tx.execute(
1093
+ sql.raw(`
1094
+ WITH reverted1 AS (
1095
+ DELETE FROM "${namespace.schema}"."${getTableName(getReorgTable(table))}"
1096
+ WHERE checkpoint > '${checkpoint}' RETURNING *
1097
+ ), reverted2 AS (
1098
+ SELECT ${primaryKeyColumns.map(({ sql }) => `"${sql}"`).join(", ")}, MIN(operation_id) AS operation_id FROM reverted1
1099
+ GROUP BY ${primaryKeyColumns.map(({ sql }) => `"${sql}"`).join(", ")}
1100
+ ), reverted3 AS (
1101
+ SELECT ${Object.values(getTableColumns(table))
1102
+ .map((column) => `reverted1."${getColumnCasing(column, "snake_case")}"`)
1103
+ .join(", ")}, reverted1.operation FROM reverted2
1104
+ INNER JOIN reverted1
1105
+ ON ${primaryKeyColumns.map(({ sql }) => `reverted2."${sql}" = reverted1."${sql}"`).join("AND ")}
1106
+ AND reverted2.operation_id = reverted1.operation_id
1107
+ ), inserted AS (
1108
+ DELETE FROM "${namespace.schema}"."${getTableName(table)}" as t
1109
+ WHERE EXISTS (
1110
+ SELECT * FROM reverted3
1111
+ WHERE ${primaryKeyColumns.map(({ sql }) => `t."${sql}" = reverted3."${sql}"`).join("AND ")}
1112
+ AND OPERATION = 0
1113
+ )
1114
+ RETURNING *
1115
+ ), updated_or_deleted AS (
1116
+ INSERT INTO "${namespace.schema}"."${getTableName(table)}"
1117
+ SELECT ${Object.values(getTableColumns(table))
1118
+ .map((column) => `"${getColumnCasing(column, "snake_case")}"`)
1119
+ .join(", ")} FROM reverted3
1120
+ WHERE operation = 1 OR operation = 2
1121
+ ON CONFLICT (${primaryKeyColumns.map(({ sql }) => `"${sql}"`).join(", ")})
1122
+ DO UPDATE SET
1123
+ ${Object.values(getTableColumns(table))
1124
+ .map(
1125
+ (column) =>
1126
+ `"${getColumnCasing(column, "snake_case")}" = EXCLUDED."${getColumnCasing(column, "snake_case")}"`,
1127
+ )
1128
+ .join(", ")}
1129
+ RETURNING *
1130
+ ) SELECT COUNT(*) FROM reverted1 as count;
1131
+ `),
1132
+ );
1133
+
1134
+ common.logger.info({
1135
+ service: "database",
1136
+ // @ts-ignore
1137
+ msg: `Reverted ${result.rows[0]!.count} unfinalized operations from '${getTableName(table)}'`,
1138
+ });
1139
+ }),
1140
+ ),
1141
+ );
1142
+ },
1143
+ async finalize({ checkpoint, db }) {
1144
+ await this.record(
1145
+ { method: "finalize", includeTraceLogs: true },
1146
+ async () => {
1147
+ await Promise.all(
1148
+ tables.map((table) =>
1149
+ db
1150
+ .delete(getReorgTable(table))
1151
+ .where(lte(getReorgTable(table).checkpoint, checkpoint)),
1152
+ ),
1153
+ );
1154
+ },
1155
+ );
1156
+
1157
+ const decoded = decodeCheckpoint(checkpoint);
1158
+
1159
+ common.logger.debug({
1160
+ service: "database",
1161
+ msg: `Updated finalized checkpoint to (timestamp=${decoded.blockTimestamp} chainId=${decoded.chainId} block=${decoded.blockNumber})`,
1162
+ });
1163
+ },
1164
+ async commitBlock({ checkpoint, db }) {
1165
+ await Promise.all(
1166
+ tables.map((table) =>
1167
+ this.wrap({ method: "complete" }, async () => {
1168
+ const reorgTable = getReorgTable(table);
1169
+ await db
1170
+ .update(reorgTable)
1171
+ .set({ checkpoint })
1172
+ .where(eq(reorgTable.checkpoint, MAX_CHECKPOINT_STRING));
1173
+ }),
1174
+ ),
1175
+ );
1176
+ },
1177
+ } satisfies Database;
1178
+
1179
+ // @ts-ignore
1180
+ return database;
770
1181
  };