ponder 0.11.21 → 0.11.23

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 (127) hide show
  1. package/CHANGELOG.md +12 -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/rpc.js +3 -3
  46. package/dist/esm/utils/rpc.js.map +1 -1
  47. package/dist/esm/utils/wait.js +0 -2
  48. package/dist/esm/utils/wait.js.map +1 -1
  49. package/dist/types/bin/commands/createViews.d.ts.map +1 -1
  50. package/dist/types/bin/commands/list.d.ts.map +1 -1
  51. package/dist/types/bin/commands/prune.d.ts.map +1 -1
  52. package/dist/types/bin/commands/start.d.ts +0 -2
  53. package/dist/types/bin/commands/start.d.ts.map +1 -1
  54. package/dist/types/bin/utils/run.d.ts +1 -1
  55. package/dist/types/bin/utils/run.d.ts.map +1 -1
  56. package/dist/types/build/index.d.ts +1 -1
  57. package/dist/types/build/index.d.ts.map +1 -1
  58. package/dist/types/client/index.d.ts.map +1 -1
  59. package/dist/types/database/index.d.ts +73 -25
  60. package/dist/types/database/index.d.ts.map +1 -1
  61. package/dist/types/drizzle/index.d.ts +3 -2
  62. package/dist/types/drizzle/index.d.ts.map +1 -1
  63. package/dist/types/drizzle/kit/index.d.ts +4 -3
  64. package/dist/types/drizzle/kit/index.d.ts.map +1 -1
  65. package/dist/types/drizzle/onchain.d.ts +5 -12
  66. package/dist/types/drizzle/onchain.d.ts.map +1 -1
  67. package/dist/types/graphql/index.d.ts +4 -2
  68. package/dist/types/graphql/index.d.ts.map +1 -1
  69. package/dist/types/graphql/middleware.d.ts +1 -1
  70. package/dist/types/graphql/middleware.d.ts.map +1 -1
  71. package/dist/types/indexing-store/cache.d.ts +12 -5
  72. package/dist/types/indexing-store/cache.d.ts.map +1 -1
  73. package/dist/types/indexing-store/historical.d.ts +7 -2
  74. package/dist/types/indexing-store/historical.d.ts.map +1 -1
  75. package/dist/types/indexing-store/index.d.ts +2 -4
  76. package/dist/types/indexing-store/index.d.ts.map +1 -1
  77. package/dist/types/indexing-store/realtime.d.ts +3 -1
  78. package/dist/types/indexing-store/realtime.d.ts.map +1 -1
  79. package/dist/types/internal/errors.d.ts +0 -4
  80. package/dist/types/internal/errors.d.ts.map +1 -1
  81. package/dist/types/server/index.d.ts +1 -1
  82. package/dist/types/server/index.d.ts.map +1 -1
  83. package/dist/types/sync/index.d.ts +1 -1
  84. package/dist/types/sync-store/index.d.ts.map +1 -1
  85. package/dist/types/utils/rpc.d.ts.map +1 -1
  86. package/dist/types/utils/wait.d.ts.map +1 -1
  87. package/package.json +2 -2
  88. package/src/bin/commands/createViews.ts +26 -37
  89. package/src/bin/commands/dev.ts +1 -1
  90. package/src/bin/commands/list.ts +4 -7
  91. package/src/bin/commands/prune.ts +17 -31
  92. package/src/bin/commands/serve.ts +1 -1
  93. package/src/bin/commands/start.ts +3 -4
  94. package/src/bin/utils/run.ts +210 -256
  95. package/src/build/index.ts +2 -53
  96. package/src/build/plugin.ts +1 -1
  97. package/src/client/index.ts +10 -21
  98. package/src/database/index.ts +742 -331
  99. package/src/drizzle/index.ts +3 -2
  100. package/src/drizzle/kit/index.ts +5 -2
  101. package/src/drizzle/onchain.ts +2 -26
  102. package/src/graphql/index.ts +26 -31
  103. package/src/graphql/middleware.ts +7 -5
  104. package/src/indexing-store/cache.ts +52 -35
  105. package/src/indexing-store/historical.ts +40 -28
  106. package/src/indexing-store/index.ts +27 -2
  107. package/src/indexing-store/realtime.ts +220 -176
  108. package/src/internal/errors.ts +0 -9
  109. package/src/server/index.ts +3 -14
  110. package/src/sync-store/index.ts +997 -870
  111. package/src/utils/rpc.ts +7 -6
  112. package/src/utils/wait.ts +0 -1
  113. package/dist/esm/database/queryBuilder.js +0 -206
  114. package/dist/esm/database/queryBuilder.js.map +0 -1
  115. package/dist/esm/database/utils.js +0 -100
  116. package/dist/esm/database/utils.js.map +0 -1
  117. package/dist/esm/drizzle/json.js +0 -119
  118. package/dist/esm/drizzle/json.js.map +0 -1
  119. package/dist/types/database/queryBuilder.d.ts +0 -37
  120. package/dist/types/database/queryBuilder.d.ts.map +0 -1
  121. package/dist/types/database/utils.d.ts +0 -25
  122. package/dist/types/database/utils.d.ts.map +0 -1
  123. package/dist/types/drizzle/json.d.ts +0 -51
  124. package/dist/types/drizzle/json.d.ts.map +0 -1
  125. package/src/database/queryBuilder.ts +0 -319
  126. package/src/database/utils.ts +0 -140
  127. package/src/drizzle/json.ts +0 -154
@@ -1,22 +1,21 @@
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 { getColumnCasing, getReorgTable, sqlToReorgTableName, } from '../drizzle/kit/index.js';
3
4
  import { NonRetryableError, ShutdownError } from '../internal/errors.js';
4
5
  import { buildMigrationProvider } from '../sync-store/migrations.js';
5
- import * as PONDER_SYNC from '../sync-store/schema.js';
6
- import { min } from '../utils/checkpoint.js';
6
+ import * as ponderSyncSchema from '../sync-store/schema.js';
7
+ import { MAX_CHECKPOINT_STRING, decodeCheckpoint, min, } from '../utils/checkpoint.js';
7
8
  import { formatEta } from '../utils/format.js';
8
9
  import { createPool, createReadonlyPool } from '../utils/pg.js';
9
10
  import { createPglite, createPgliteKyselyDialect } from '../utils/pglite.js';
10
11
  import { startClock } from '../utils/timer.js';
11
12
  import { wait } from '../utils/wait.js';
12
- import { eq, getTableName, is, sql } from "drizzle-orm";
13
+ import { eq, getTableColumns, getTableName, is, lte, sql, } from "drizzle-orm";
13
14
  import { drizzle as drizzleNodePg } from "drizzle-orm/node-postgres";
14
- import { PgTable, pgSchema, pgTable } from "drizzle-orm/pg-core";
15
+ import { PgTable, pgSchema, pgTable, } from "drizzle-orm/pg-core";
15
16
  import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
16
17
  import { Kysely, Migrator, PostgresDialect, WithSchemaPlugin } from "kysely";
17
18
  import prometheus from "prom-client";
18
- import { createQB, parseSqlError } from "./queryBuilder.js";
19
- import { revert } from "./utils.js";
20
19
  export const SCHEMATA = pgSchema("information_schema").table("schemata", (t) => ({
21
20
  schemaName: t.text().primaryKey(),
22
21
  }));
@@ -31,7 +30,7 @@ export const VIEWS = pgSchema("information_schema").table("views", (t) => ({
31
30
  }));
32
31
  const VERSION = "2";
33
32
  export const getPonderMetaTable = (schema) => {
34
- if (schema === undefined || schema === "public") {
33
+ if (schema === "public") {
35
34
  return pgTable("_ponder_meta", (t) => ({
36
35
  key: t.text().primaryKey().$type(),
37
36
  value: t.jsonb().$type().notNull(),
@@ -42,15 +41,8 @@ export const getPonderMetaTable = (schema) => {
42
41
  value: t.jsonb().$type().notNull(),
43
42
  }));
44
43
  };
45
- /**
46
- * - "safe" checkpoint: The closest-to-tip finalized and completed checkpoint.
47
- * - "latest" checkpoint: The closest-to-tip completed checkpoint.
48
- *
49
- * @dev It is an invariant that every "latest" checkpoint is specific to that chain.
50
- * In other words, `chainId === latestCheckpoint.chainId`.
51
- */
52
44
  export const getPonderCheckpointTable = (schema) => {
53
- if (schema === undefined || schema === "public") {
45
+ if (schema === "public") {
54
46
  return pgTable("_ponder_checkpoint", (t) => ({
55
47
  chainName: t.text().primaryKey(),
56
48
  chainId: t.bigint({ mode: "number" }).notNull(),
@@ -73,10 +65,7 @@ export const createDatabase = async ({ common, namespace, preBuild, schemaBuild,
73
65
  // Create schema, drivers, roles, and query builders
74
66
  ////////
75
67
  let driver;
76
- let syncQB;
77
- let adminQB;
78
- let userQB;
79
- let readonlyQB;
68
+ let qb;
80
69
  const dialect = preBuild.databaseConfig.kind;
81
70
  if (namespace.viewsSchema) {
82
71
  common.logger.info({
@@ -92,7 +81,6 @@ export const createDatabase = async ({ common, namespace, preBuild, schemaBuild,
92
81
  }
93
82
  if (dialect === "pglite" || dialect === "pglite_test") {
94
83
  driver = {
95
- dialect: "pglite",
96
84
  instance: dialect === "pglite"
97
85
  ? createPglite(preBuild.databaseConfig.options)
98
86
  : preBuild.databaseConfig.instance,
@@ -100,7 +88,7 @@ export const createDatabase = async ({ common, namespace, preBuild, schemaBuild,
100
88
  common.shutdown.add(async () => {
101
89
  clearInterval(heartbeatInterval);
102
90
  if (["start", "dev"].includes(common.options.command)) {
103
- await adminQB("unlock")
91
+ await qb.drizzle
104
92
  .update(PONDER_META)
105
93
  .set({ value: sql `jsonb_set(value, '{is_locked}', to_jsonb(0))` });
106
94
  }
@@ -110,22 +98,20 @@ export const createDatabase = async ({ common, namespace, preBuild, schemaBuild,
110
98
  });
111
99
  await driver.instance.query(`CREATE SCHEMA IF NOT EXISTS "${namespace.schema}"`);
112
100
  await driver.instance.query(`SET search_path TO "${namespace.schema}"`);
113
- syncQB = createQB(() => drizzlePglite(driver.instance, {
114
- casing: "snake_case",
115
- schema: PONDER_SYNC,
116
- }), { common });
117
- adminQB = createQB(() => drizzlePglite(driver.instance, {
118
- casing: "snake_case",
119
- schema: schemaBuild.schema,
120
- }), { common, isAdmin: true });
121
- userQB = createQB(() => drizzlePglite(driver.instance, {
122
- casing: "snake_case",
123
- schema: schemaBuild.schema,
124
- }), { common });
125
- readonlyQB = createQB(() => drizzlePglite(driver.instance, {
126
- casing: "snake_case",
127
- schema: schemaBuild.schema,
128
- }), { common });
101
+ qb = {
102
+ sync: drizzlePglite(driver.instance, {
103
+ casing: "snake_case",
104
+ schema: ponderSyncSchema,
105
+ }),
106
+ drizzle: drizzlePglite(driver.instance, {
107
+ casing: "snake_case",
108
+ schema: schemaBuild.schema,
109
+ }),
110
+ drizzleReadonly: drizzlePglite(driver.instance, {
111
+ casing: "snake_case",
112
+ schema: schemaBuild.schema,
113
+ }),
114
+ };
129
115
  }
130
116
  else {
131
117
  const internalMax = 2;
@@ -134,12 +120,7 @@ export const createDatabase = async ({ common, namespace, preBuild, schemaBuild,
134
120
  ? [preBuild.databaseConfig.poolConfig.max - internalMax, 0, 0]
135
121
  : [equalMax, equalMax, equalMax];
136
122
  driver = {
137
- sync: createPool({
138
- ...preBuild.databaseConfig.poolConfig,
139
- application_name: "ponder_sync",
140
- max: syncMax,
141
- }, common.logger),
142
- admin: createPool({
123
+ internal: createPool({
143
124
  ...preBuild.databaseConfig.poolConfig,
144
125
  application_name: `${namespace.schema}_internal`,
145
126
  max: internalMax,
@@ -155,47 +136,42 @@ export const createDatabase = async ({ common, namespace, preBuild, schemaBuild,
155
136
  application_name: `${namespace.schema}_readonly`,
156
137
  max: readonlyMax,
157
138
  }, common.logger, namespace.schema),
139
+ sync: createPool({
140
+ ...preBuild.databaseConfig.poolConfig,
141
+ application_name: "ponder_sync",
142
+ max: syncMax,
143
+ }, common.logger),
158
144
  listen: undefined,
159
145
  };
160
- await driver.admin.query(`CREATE SCHEMA IF NOT EXISTS "${namespace.schema}"`);
161
- syncQB = createQB(() =>
162
- // @ts-expect-error
163
- drizzleNodePg(driver.sync, {
164
- casing: "snake_case",
165
- schema: PONDER_SYNC,
166
- }), { common });
167
- adminQB = createQB(() =>
168
- // @ts-expect-error
169
- drizzleNodePg(driver.admin, {
170
- casing: "snake_case",
171
- schema: schemaBuild.schema,
172
- }), { common, isAdmin: true });
173
- userQB = createQB(() =>
174
- // @ts-expect-error
175
- drizzleNodePg(driver.user, {
176
- casing: "snake_case",
177
- schema: schemaBuild.schema,
178
- }), { common });
179
- readonlyQB = createQB(() =>
180
- // @ts-expect-error
181
- drizzleNodePg(driver.readonly, {
182
- casing: "snake_case",
183
- schema: schemaBuild.schema,
184
- }), { common });
146
+ await driver.internal.query(`CREATE SCHEMA IF NOT EXISTS "${namespace.schema}"`);
147
+ qb = {
148
+ sync: drizzleNodePg(driver.sync, {
149
+ casing: "snake_case",
150
+ schema: ponderSyncSchema,
151
+ }),
152
+ drizzle: drizzleNodePg(driver.user, {
153
+ casing: "snake_case",
154
+ schema: schemaBuild.schema,
155
+ }),
156
+ drizzleReadonly: drizzleNodePg(driver.readonly, {
157
+ casing: "snake_case",
158
+ schema: schemaBuild.schema,
159
+ }),
160
+ };
185
161
  common.shutdown.add(async () => {
186
162
  clearInterval(heartbeatInterval);
187
163
  if (["start", "dev"].includes(common.options.command)) {
188
- await adminQB("unlock")
164
+ await qb.drizzle
189
165
  .update(PONDER_META)
190
166
  .set({ value: sql `jsonb_set(value, '{is_locked}', to_jsonb(0))` });
191
167
  }
192
168
  const d = driver;
193
169
  d.listen?.release();
194
170
  await Promise.all([
195
- d.sync.end(),
196
- d.admin.end(),
171
+ d.internal.end(),
197
172
  d.user.end(),
198
173
  d.readonly.end(),
174
+ d.sync.end(),
199
175
  ]);
200
176
  });
201
177
  // Register Postgres-only metrics
@@ -207,8 +183,8 @@ export const createDatabase = async ({ common, namespace, preBuild, schemaBuild,
207
183
  labelNames: ["pool", "kind"],
208
184
  registers: [common.metrics.registry],
209
185
  collect() {
210
- this.set({ pool: "admin", kind: "idle" }, d.admin.idleCount);
211
- this.set({ pool: "admin", kind: "total" }, d.admin.totalCount);
186
+ this.set({ pool: "internal", kind: "idle" }, d.internal.idleCount);
187
+ this.set({ pool: "internal", kind: "total" }, d.internal.totalCount);
212
188
  this.set({ pool: "sync", kind: "idle" }, d.sync.idleCount);
213
189
  this.set({ pool: "sync", kind: "total" }, d.sync.totalCount);
214
190
  this.set({ pool: "user", kind: "idle" }, d.user.idleCount);
@@ -224,7 +200,7 @@ export const createDatabase = async ({ common, namespace, preBuild, schemaBuild,
224
200
  labelNames: ["pool"],
225
201
  registers: [common.metrics.registry],
226
202
  collect() {
227
- this.set({ pool: "admin" }, d.admin.waitingCount);
203
+ this.set({ pool: "internal" }, d.internal.waitingCount);
228
204
  this.set({ pool: "sync" }, d.sync.waitingCount);
229
205
  this.set({ pool: "user" }, d.user.waitingCount);
230
206
  this.set({ pool: "readonly" }, d.readonly.waitingCount);
@@ -232,91 +208,253 @@ export const createDatabase = async ({ common, namespace, preBuild, schemaBuild,
232
208
  });
233
209
  }
234
210
  const tables = Object.values(schemaBuild.schema).filter((table) => is(table, PgTable));
235
- return {
211
+ const database = {
236
212
  driver,
237
- syncQB,
238
- adminQB,
239
- userQB,
240
- readonlyQB,
241
- async migrateSync() {
242
- const kysely = new Kysely({
243
- dialect: dialect === "postgres"
244
- ? new PostgresDialect({ pool: driver.admin })
245
- : createPgliteKyselyDialect(driver.instance),
246
- log(event) {
247
- if (event.level === "query") {
248
- common.metrics.ponder_postgres_query_total.inc({ pool: "migrate" });
213
+ qb,
214
+ PONDER_META,
215
+ PONDER_CHECKPOINT,
216
+ async retry(fn) {
217
+ const RETRY_COUNT = 9;
218
+ const BASE_DURATION = 125;
219
+ // First error thrown is often the most useful
220
+ let firstError;
221
+ let hasError = false;
222
+ for (let i = 0; i <= RETRY_COUNT; i++) {
223
+ try {
224
+ if (common.shutdown.isKilled) {
225
+ throw new ShutdownError();
249
226
  }
250
- },
251
- plugins: [new WithSchemaPlugin("ponder_sync")],
252
- });
253
- const migrationProvider = buildMigrationProvider(common.logger);
254
- const migrator = new Migrator({
255
- db: kysely,
256
- provider: migrationProvider,
257
- migrationTableSchema: "ponder_sync",
258
- });
259
- for (let i = 0; i <= 9; i++) {
227
+ const result = await fn();
228
+ if (common.shutdown.isKilled) {
229
+ throw new ShutdownError();
230
+ }
231
+ return result;
232
+ }
233
+ catch (_error) {
234
+ const error = _error;
235
+ if (common.shutdown.isKilled) {
236
+ throw new ShutdownError();
237
+ }
238
+ if (!hasError) {
239
+ hasError = true;
240
+ firstError = error;
241
+ }
242
+ if (error instanceof NonRetryableError) {
243
+ throw error;
244
+ }
245
+ if (i === RETRY_COUNT) {
246
+ throw firstError;
247
+ }
248
+ const duration = BASE_DURATION * 2 ** i;
249
+ await wait(duration);
250
+ }
251
+ }
252
+ throw "unreachable";
253
+ },
254
+ async record(options, fn) {
255
+ const endClock = startClock();
256
+ const id = crypto.randomUUID().slice(0, 8);
257
+ if (options.includeTraceLogs) {
258
+ common.logger.trace({
259
+ service: "database",
260
+ msg: `Started '${options.method}' database method (id=${id})`,
261
+ });
262
+ }
263
+ try {
264
+ if (common.shutdown.isKilled) {
265
+ throw new ShutdownError();
266
+ }
267
+ const result = await fn();
268
+ common.metrics.ponder_database_method_duration.observe({ method: options.method }, endClock());
269
+ if (common.shutdown.isKilled) {
270
+ throw new ShutdownError();
271
+ }
272
+ return result;
273
+ }
274
+ catch (_error) {
275
+ const error = _error;
276
+ if (common.shutdown.isKilled) {
277
+ throw new ShutdownError();
278
+ }
279
+ common.metrics.ponder_database_method_duration.observe({ method: options.method }, endClock());
280
+ common.metrics.ponder_database_method_error_total.inc({
281
+ method: options.method,
282
+ });
283
+ common.logger.warn({
284
+ service: "database",
285
+ msg: `Failed '${options.method}' database method (id=${id})`,
286
+ error,
287
+ });
288
+ throw error;
289
+ }
290
+ finally {
291
+ if (options.includeTraceLogs) {
292
+ common.logger.trace({
293
+ service: "database",
294
+ msg: `Completed '${options.method}' database method in ${Math.round(endClock())}ms (id=${id})`,
295
+ });
296
+ }
297
+ }
298
+ },
299
+ async wrap(options, fn) {
300
+ const RETRY_COUNT = 9;
301
+ const BASE_DURATION = 125;
302
+ // First error thrown is often the most useful
303
+ let firstError;
304
+ let hasError = false;
305
+ for (let i = 0; i <= RETRY_COUNT; i++) {
260
306
  const endClock = startClock();
307
+ const id = crypto.randomUUID().slice(0, 8);
308
+ if (options.includeTraceLogs) {
309
+ common.logger.trace({
310
+ service: "database",
311
+ msg: `Started '${options.method}' database method (id=${id})`,
312
+ });
313
+ }
261
314
  try {
262
- const { error } = await migrator.migrateToLatest();
263
- if (error)
264
- throw error;
265
- common.metrics.ponder_database_method_duration.observe({ method: "migrate_sync" }, endClock());
266
- return;
315
+ if (common.shutdown.isKilled) {
316
+ throw new ShutdownError();
317
+ }
318
+ const result = await fn();
319
+ common.metrics.ponder_database_method_duration.observe({ method: options.method }, endClock());
320
+ if (common.shutdown.isKilled) {
321
+ throw new ShutdownError();
322
+ }
323
+ return result;
267
324
  }
268
325
  catch (_error) {
269
- const error = parseSqlError(_error);
326
+ const error = _error;
270
327
  if (common.shutdown.isKilled) {
271
328
  throw new ShutdownError();
272
329
  }
273
- common.metrics.ponder_database_method_duration.observe({ method: "migrate_sync" }, endClock());
330
+ common.metrics.ponder_database_method_duration.observe({ method: options.method }, endClock());
274
331
  common.metrics.ponder_database_method_error_total.inc({
275
- method: "migrate_sync",
332
+ method: options.method,
276
333
  });
334
+ if (!hasError) {
335
+ hasError = true;
336
+ firstError = error;
337
+ }
277
338
  if (error instanceof NonRetryableError) {
278
339
  common.logger.warn({
279
340
  service: "database",
280
- msg: `Failed 'migrate_sync' database query`,
341
+ msg: `Failed '${options.method}' database method (id=${id})`,
281
342
  error,
282
343
  });
283
344
  throw error;
284
345
  }
285
- if (i === 9) {
346
+ if (i === RETRY_COUNT) {
286
347
  common.logger.warn({
287
348
  service: "database",
288
- msg: `Failed 'migrate_sync' database query after '${i + 1}' attempts`,
349
+ msg: `Failed '${options.method}' database method after '${i + 1}' attempts (id=${id})`,
289
350
  error,
290
351
  });
291
- throw error;
352
+ throw firstError;
292
353
  }
293
- const duration = 125 * 2 ** i;
354
+ const duration = BASE_DURATION * 2 ** i;
294
355
  common.logger.debug({
295
356
  service: "database",
296
- msg: `Failed 'migrate_sync' database query, retrying after ${duration} milliseconds`,
357
+ msg: `Failed '${options.method}' database method, retrying after ${duration} milliseconds (id=${id})`,
297
358
  error,
298
359
  });
299
360
  await wait(duration);
300
361
  }
362
+ finally {
363
+ if (options.includeTraceLogs) {
364
+ common.logger.trace({
365
+ service: "database",
366
+ msg: `Completed '${options.method}' database method in ${Math.round(endClock())}ms (id=${id})`,
367
+ });
368
+ }
369
+ }
370
+ }
371
+ throw "unreachable";
372
+ },
373
+ async transaction(fn) {
374
+ if (dialect === "postgres") {
375
+ const client = await database.driver.user.connect();
376
+ try {
377
+ await client.query("BEGIN");
378
+ const tx = drizzleNodePg(client, {
379
+ casing: "snake_case",
380
+ schema: schemaBuild.schema,
381
+ });
382
+ const result = await fn(client, tx);
383
+ await client.query("COMMIT");
384
+ return result;
385
+ }
386
+ catch (error) {
387
+ await client.query("ROLLBACK");
388
+ throw error;
389
+ }
390
+ finally {
391
+ client.release();
392
+ }
393
+ }
394
+ else {
395
+ const client = database.driver.instance;
396
+ try {
397
+ await client.query("BEGIN");
398
+ const tx = drizzlePglite(client, {
399
+ casing: "snake_case",
400
+ schema: schemaBuild.schema,
401
+ });
402
+ const result = await fn(client, tx);
403
+ await client.query("COMMIT");
404
+ return result;
405
+ }
406
+ catch (error) {
407
+ await client?.query("ROLLBACK");
408
+ throw error;
409
+ }
301
410
  }
302
411
  },
412
+ async migrateSync() {
413
+ await this.wrap({ method: "migrateSyncStore", includeTraceLogs: true }, async () => {
414
+ const kysely = new Kysely({
415
+ dialect: dialect === "postgres"
416
+ ? new PostgresDialect({
417
+ pool: driver.internal,
418
+ })
419
+ : createPgliteKyselyDialect(driver.instance),
420
+ log(event) {
421
+ if (event.level === "query") {
422
+ common.metrics.ponder_postgres_query_total.inc({
423
+ pool: "migrate",
424
+ });
425
+ }
426
+ },
427
+ plugins: [new WithSchemaPlugin("ponder_sync")],
428
+ });
429
+ const migrationProvider = buildMigrationProvider(common.logger);
430
+ const migrator = new Migrator({
431
+ db: kysely,
432
+ provider: migrationProvider,
433
+ migrationTableSchema: "ponder_sync",
434
+ });
435
+ const { error } = await migrator.migrateToLatest();
436
+ if (error)
437
+ throw error;
438
+ });
439
+ },
303
440
  async migrate({ buildId }) {
304
- await adminQB("create_meta_table").execute(sql.raw(`
441
+ await this.wrap({ method: "createPonderSystemTables", includeTraceLogs: true }, async () => {
442
+ await qb.drizzle.execute(sql.raw(`
305
443
  CREATE TABLE IF NOT EXISTS "${namespace.schema}"."_ponder_meta" (
306
444
  "key" TEXT PRIMARY KEY,
307
445
  "value" JSONB NOT NULL
308
446
  )`));
309
- await adminQB("create_checkpoint_table").execute(sql.raw(`
447
+ await qb.drizzle.execute(sql.raw(`
310
448
  CREATE TABLE IF NOT EXISTS "${namespace.schema}"."_ponder_checkpoint" (
311
449
  "chain_name" TEXT PRIMARY KEY,
312
450
  "chain_id" INTEGER NOT NULL,
313
451
  "safe_checkpoint" VARCHAR(75) NOT NULL,
314
452
  "latest_checkpoint" VARCHAR(75) NOT NULL
315
453
  )`));
316
- const trigger = "status_trigger";
317
- const notification = "status_notify()";
318
- const channel = `${namespace.schema}_status_channel`;
319
- await adminQB("create_system_notification").execute(sql.raw(`
454
+ const trigger = "status_trigger";
455
+ const notification = "status_notify()";
456
+ const channel = `${namespace.schema}_status_channel`;
457
+ await qb.drizzle.execute(sql.raw(`
320
458
  CREATE OR REPLACE FUNCTION "${namespace.schema}".${notification}
321
459
  RETURNS TRIGGER
322
460
  LANGUAGE plpgsql
@@ -326,15 +464,16 @@ NOTIFY "${channel}";
326
464
  RETURN NULL;
327
465
  END;
328
466
  $$;`));
329
- await adminQB("create_system_trigger").execute(sql.raw(`
467
+ await qb.drizzle.execute(sql.raw(`
330
468
  CREATE OR REPLACE TRIGGER "${trigger}"
331
469
  AFTER INSERT OR UPDATE OR DELETE
332
470
  ON "${namespace.schema}"._ponder_checkpoint
333
471
  FOR EACH STATEMENT
334
472
  EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
473
+ });
335
474
  const createTables = async (tx) => {
336
475
  for (let i = 0; i < schemaBuild.statements.tables.sql.length; i++) {
337
- await tx()
476
+ await tx
338
477
  .execute(sql.raw(schemaBuild.statements.tables.sql[i]))
339
478
  .catch((_error) => {
340
479
  const error = _error;
@@ -348,7 +487,7 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
348
487
  };
349
488
  const createEnums = async (tx) => {
350
489
  for (let i = 0; i < schemaBuild.statements.enums.sql.length; i++) {
351
- await tx()
490
+ await tx
352
491
  .execute(sql.raw(schemaBuild.statements.enums.sql[i]))
353
492
  .catch((_error) => {
354
493
  const error = _error;
@@ -360,9 +499,9 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
360
499
  });
361
500
  }
362
501
  };
363
- const tryAcquireLockAndMigrate = () => adminQB("migrate").transaction(async (tx) => {
502
+ const tryAcquireLockAndMigrate = () => this.wrap({ method: "migrate", includeTraceLogs: true }, () => qb.drizzle.transaction(async (tx) => {
364
503
  // Note: All ponder versions are compatible with the next query (every version of the "_ponder_meta" table have the same columns)
365
- const previousApp = await tx()
504
+ const previousApp = await tx
366
505
  .select({ value: PONDER_META.value })
367
506
  .from(PONDER_META)
368
507
  .where(eq(PONDER_META.key, "app"))
@@ -383,7 +522,7 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
383
522
  service: "database",
384
523
  msg: `Created tables [${tables.map(getTableName).join(", ")}]`,
385
524
  });
386
- await tx()
525
+ await tx
387
526
  .insert(PONDER_META)
388
527
  .values({ key: "app", value: metadata });
389
528
  return {
@@ -395,20 +534,20 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
395
534
  (process.env.PONDER_EXPERIMENTAL_DB === "platform" &&
396
535
  previousApp.build_id !== buildId)) {
397
536
  for (const table of previousApp.table_names) {
398
- await tx().execute(sql.raw(`DROP TABLE IF EXISTS "${namespace.schema}"."${table}" CASCADE`));
399
- await tx().execute(sql.raw(`DROP TABLE IF EXISTS "${namespace.schema}"."${sqlToReorgTableName(table)}" CASCADE`));
537
+ await tx.execute(sql.raw(`DROP TABLE IF EXISTS "${namespace.schema}"."${table}" CASCADE`));
538
+ await tx.execute(sql.raw(`DROP TABLE IF EXISTS "${namespace.schema}"."${sqlToReorgTableName(table)}" CASCADE`));
400
539
  }
401
540
  for (const enumName of schemaBuild.statements.enums.json) {
402
- await tx().execute(sql.raw(`DROP TYPE IF EXISTS "${namespace.schema}"."${enumName.name}"`));
541
+ await tx.execute(sql.raw(`DROP TYPE IF EXISTS "${namespace.schema}"."${enumName.name}"`));
403
542
  }
404
- await tx().execute(sql.raw(`TRUNCATE TABLE "${namespace.schema}"."${getTableName(PONDER_CHECKPOINT)}" CASCADE`));
543
+ await tx.execute(sql.raw(`TRUNCATE TABLE "${namespace.schema}"."${getTableName(PONDER_CHECKPOINT)}" CASCADE`));
405
544
  await createEnums(tx);
406
545
  await createTables(tx);
407
546
  common.logger.info({
408
547
  service: "database",
409
548
  msg: `Created tables [${tables.map(getTableName).join(", ")}]`,
410
549
  });
411
- await tx().update(PONDER_META).set({ value: metadata });
550
+ await tx.update(PONDER_META).set({ value: metadata });
412
551
  return {
413
552
  status: "success",
414
553
  crashRecoveryCheckpoint: undefined,
@@ -426,7 +565,8 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
426
565
  error.stack = undefined;
427
566
  throw error;
428
567
  }
429
- const expiry = previousApp.heartbeat_at + common.options.databaseHeartbeatTimeout;
568
+ const expiry = previousApp.heartbeat_at +
569
+ common.options.databaseHeartbeatTimeout;
430
570
  const isAppUnlocked = previousApp.is_locked === 0 || expiry <= Date.now();
431
571
  if (isAppUnlocked === false) {
432
572
  return { status: "locked", expiry };
@@ -435,7 +575,7 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
435
575
  service: "database",
436
576
  msg: `Detected crash recovery for build '${buildId}' in schema '${namespace.schema}' last active ${formatEta(Date.now() - previousApp.heartbeat_at)} ago`,
437
577
  });
438
- const checkpoints = await tx().select().from(PONDER_CHECKPOINT);
578
+ const checkpoints = await tx.select().from(PONDER_CHECKPOINT);
439
579
  const crashRecoveryCheckpoint = checkpoints.length === 0
440
580
  ? undefined
441
581
  : checkpoints.map((c) => ({
@@ -443,16 +583,16 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
443
583
  checkpoint: c.safeCheckpoint,
444
584
  }));
445
585
  if (previousApp.is_ready === 0) {
446
- await tx().update(PONDER_META).set({ value: metadata });
586
+ await tx.update(PONDER_META).set({ value: metadata });
447
587
  return { status: "success", crashRecoveryCheckpoint };
448
588
  }
449
589
  // Remove triggers
450
590
  for (const table of tables) {
451
- await tx().execute(sql.raw(`DROP TRIGGER IF EXISTS "${getTableNames(table).trigger}" ON "${namespace.schema}"."${getTableName(table)}"`));
591
+ await tx.execute(sql.raw(`DROP TRIGGER IF EXISTS "${getTableNames(table).trigger}" ON "${namespace.schema}"."${getTableName(table)}"`));
452
592
  }
453
593
  // Remove indexes
454
594
  for (const indexStatement of schemaBuild.statements.indexes.json) {
455
- await tx().execute(sql.raw(`DROP INDEX IF EXISTS "${namespace.schema}"."${indexStatement.data.name}"`));
595
+ await tx.execute(sql.raw(`DROP INDEX IF EXISTS "${namespace.schema}"."${indexStatement.data.name}"`));
456
596
  common.logger.debug({
457
597
  service: "database",
458
598
  msg: `Dropped index '${indexStatement.data.name}' in schema '${namespace.schema}'`,
@@ -460,12 +600,12 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
460
600
  }
461
601
  // Note: it is an invariant that checkpoints.length > 0;
462
602
  const revertCheckpoint = min(...checkpoints.map((c) => c.safeCheckpoint));
463
- await Promise.all(tables.map((table) => revert(tx, { checkpoint: revertCheckpoint, table })));
603
+ await this.revert({ checkpoint: revertCheckpoint, tx });
464
604
  // Note: We don't update the `_ponder_checkpoint` table here, instead we wait for it to be updated
465
605
  // in the runtime script.
466
- await tx().update(PONDER_META).set({ value: metadata });
606
+ await tx.update(PONDER_META).set({ value: metadata });
467
607
  return { status: "success", crashRecoveryCheckpoint };
468
- });
608
+ }));
469
609
  let result = await tryAcquireLockAndMigrate();
470
610
  if (result.status === "locked") {
471
611
  const duration = result.expiry - Date.now();
@@ -488,9 +628,7 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
488
628
  heartbeatInterval = setInterval(async () => {
489
629
  try {
490
630
  const heartbeat = Date.now();
491
- await adminQB("heartbeat")
492
- .update(PONDER_META)
493
- .set({
631
+ await qb.drizzle.update(PONDER_META).set({
494
632
  value: sql `jsonb_set(value, '{heartbeat_at}', ${heartbeat})`,
495
633
  });
496
634
  common.logger.trace({
@@ -509,6 +647,156 @@ EXECUTE PROCEDURE "${namespace.schema}".${notification};`));
509
647
  }, common.options.databaseHeartbeatInterval);
510
648
  return result.crashRecoveryCheckpoint;
511
649
  },
650
+ async createIndexes() {
651
+ for (const statement of schemaBuild.statements.indexes.sql) {
652
+ await this.wrap({ method: "createIndexes" }, async () => {
653
+ await qb.drizzle.transaction(async (tx) => {
654
+ await tx.execute("SET statement_timeout = 3600000;"); // 60 minutes
655
+ await tx.execute(statement);
656
+ });
657
+ });
658
+ }
659
+ },
660
+ async createTriggers() {
661
+ await this.wrap({ method: "createTriggers", includeTraceLogs: true }, async () => {
662
+ for (const table of tables) {
663
+ const columns = getTableColumns(table);
664
+ const columnNames = Object.values(columns).map((column) => `"${getColumnCasing(column, "snake_case")}"`);
665
+ await qb.drizzle.execute(sql.raw(`
666
+ CREATE OR REPLACE FUNCTION "${namespace.schema}".${getTableNames(table).triggerFn}
667
+ RETURNS TRIGGER AS $$
668
+ BEGIN
669
+ IF TG_OP = 'INSERT' THEN
670
+ INSERT INTO "${namespace.schema}"."${getTableName(getReorgTable(table))}" (${columnNames.join(",")}, operation, checkpoint)
671
+ VALUES (${columnNames.map((name) => `NEW.${name}`).join(",")}, 0, '${MAX_CHECKPOINT_STRING}');
672
+ ELSIF TG_OP = 'UPDATE' THEN
673
+ INSERT INTO "${namespace.schema}"."${getTableName(getReorgTable(table))}" (${columnNames.join(",")}, operation, checkpoint)
674
+ VALUES (${columnNames.map((name) => `OLD.${name}`).join(",")}, 1, '${MAX_CHECKPOINT_STRING}');
675
+ ELSIF TG_OP = 'DELETE' THEN
676
+ INSERT INTO "${namespace.schema}"."${getTableName(getReorgTable(table))}" (${columnNames.join(",")}, operation, checkpoint)
677
+ VALUES (${columnNames.map((name) => `OLD.${name}`).join(",")}, 2, '${MAX_CHECKPOINT_STRING}');
678
+ END IF;
679
+ RETURN NULL;
680
+ END;
681
+ $$ LANGUAGE plpgsql`));
682
+ await qb.drizzle.execute(sql.raw(`
683
+ CREATE OR REPLACE TRIGGER "${getTableNames(table).trigger}"
684
+ AFTER INSERT OR UPDATE OR DELETE ON "${namespace.schema}"."${getTableName(table)}"
685
+ FOR EACH ROW EXECUTE FUNCTION "${namespace.schema}".${getTableNames(table).triggerFn};
686
+ `));
687
+ }
688
+ });
689
+ },
690
+ async removeTriggers() {
691
+ await this.wrap({ method: "removeTriggers", includeTraceLogs: true }, async () => {
692
+ for (const table of tables) {
693
+ await qb.drizzle.execute(sql.raw(`DROP TRIGGER IF EXISTS "${getTableNames(table).trigger}" ON "${namespace.schema}"."${getTableName(table)}"`));
694
+ }
695
+ });
696
+ },
697
+ async setCheckpoints({ checkpoints, db }) {
698
+ if (checkpoints.length === 0)
699
+ return;
700
+ return this.wrap({ method: "setCheckpoints" }, async () => {
701
+ await db
702
+ .insert(PONDER_CHECKPOINT)
703
+ .values(checkpoints)
704
+ .onConflictDoUpdate({
705
+ target: PONDER_CHECKPOINT.chainName,
706
+ set: {
707
+ safeCheckpoint: sql `excluded.safe_checkpoint`,
708
+ latestCheckpoint: sql `excluded.latest_checkpoint`,
709
+ },
710
+ });
711
+ });
712
+ },
713
+ getCheckpoints() {
714
+ return this.wrap({ method: "getCheckpoints" }, () => qb.drizzle.select().from(PONDER_CHECKPOINT));
715
+ },
716
+ setReady() {
717
+ return this.wrap({ method: "setReady" }, async () => {
718
+ await qb.drizzle
719
+ .update(PONDER_META)
720
+ .set({ value: sql `jsonb_set(value, '{is_ready}', to_jsonb(1))` });
721
+ });
722
+ },
723
+ getReady() {
724
+ return this.wrap({ method: "getReady" }, async () => {
725
+ return qb.drizzle
726
+ .select()
727
+ .from(PONDER_META)
728
+ .then((result) => result[0]?.value.is_ready === 1 ?? false);
729
+ });
730
+ },
731
+ async revert({ checkpoint, tx }) {
732
+ await this.record({ method: "revert", includeTraceLogs: true }, () => Promise.all(tables.map(async (table) => {
733
+ const primaryKeyColumns = getPrimaryKeyColumns(table);
734
+ const result = await tx.execute(sql.raw(`
735
+ WITH reverted1 AS (
736
+ DELETE FROM "${namespace.schema}"."${getTableName(getReorgTable(table))}"
737
+ WHERE checkpoint > '${checkpoint}' RETURNING *
738
+ ), reverted2 AS (
739
+ SELECT ${primaryKeyColumns.map(({ sql }) => `"${sql}"`).join(", ")}, MIN(operation_id) AS operation_id FROM reverted1
740
+ GROUP BY ${primaryKeyColumns.map(({ sql }) => `"${sql}"`).join(", ")}
741
+ ), reverted3 AS (
742
+ SELECT ${Object.values(getTableColumns(table))
743
+ .map((column) => `reverted1."${getColumnCasing(column, "snake_case")}"`)
744
+ .join(", ")}, reverted1.operation FROM reverted2
745
+ INNER JOIN reverted1
746
+ ON ${primaryKeyColumns.map(({ sql }) => `reverted2."${sql}" = reverted1."${sql}"`).join("AND ")}
747
+ AND reverted2.operation_id = reverted1.operation_id
748
+ ), inserted AS (
749
+ DELETE FROM "${namespace.schema}"."${getTableName(table)}" as t
750
+ WHERE EXISTS (
751
+ SELECT * FROM reverted3
752
+ WHERE ${primaryKeyColumns.map(({ sql }) => `t."${sql}" = reverted3."${sql}"`).join("AND ")}
753
+ AND OPERATION = 0
754
+ )
755
+ RETURNING *
756
+ ), updated_or_deleted AS (
757
+ INSERT INTO "${namespace.schema}"."${getTableName(table)}"
758
+ SELECT ${Object.values(getTableColumns(table))
759
+ .map((column) => `"${getColumnCasing(column, "snake_case")}"`)
760
+ .join(", ")} FROM reverted3
761
+ WHERE operation = 1 OR operation = 2
762
+ ON CONFLICT (${primaryKeyColumns.map(({ sql }) => `"${sql}"`).join(", ")})
763
+ DO UPDATE SET
764
+ ${Object.values(getTableColumns(table))
765
+ .map((column) => `"${getColumnCasing(column, "snake_case")}" = EXCLUDED."${getColumnCasing(column, "snake_case")}"`)
766
+ .join(", ")}
767
+ RETURNING *
768
+ ) SELECT COUNT(*) FROM reverted1 as count;
769
+ `));
770
+ common.logger.info({
771
+ service: "database",
772
+ // @ts-ignore
773
+ msg: `Reverted ${result.rows[0].count} unfinalized operations from '${getTableName(table)}'`,
774
+ });
775
+ })));
776
+ },
777
+ async finalize({ checkpoint, db }) {
778
+ await this.record({ method: "finalize", includeTraceLogs: true }, async () => {
779
+ await Promise.all(tables.map((table) => db
780
+ .delete(getReorgTable(table))
781
+ .where(lte(getReorgTable(table).checkpoint, checkpoint))));
782
+ });
783
+ const decoded = decodeCheckpoint(checkpoint);
784
+ common.logger.debug({
785
+ service: "database",
786
+ msg: `Updated finalized checkpoint to (timestamp=${decoded.blockTimestamp} chainId=${decoded.chainId} block=${decoded.blockNumber})`,
787
+ });
788
+ },
789
+ async commitBlock({ checkpoint, db }) {
790
+ await Promise.all(tables.map((table) => this.wrap({ method: "complete" }, async () => {
791
+ const reorgTable = getReorgTable(table);
792
+ await db
793
+ .update(reorgTable)
794
+ .set({ checkpoint })
795
+ .where(eq(reorgTable.checkpoint, MAX_CHECKPOINT_STRING));
796
+ })));
797
+ },
512
798
  };
799
+ // @ts-ignore
800
+ return database;
513
801
  };
514
802
  //# sourceMappingURL=index.js.map