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.
- package/CHANGELOG.md +6 -0
- package/dist/esm/bin/commands/createViews.js +9 -20
- package/dist/esm/bin/commands/createViews.js.map +1 -1
- package/dist/esm/bin/commands/dev.js +1 -1
- package/dist/esm/bin/commands/dev.js.map +1 -1
- package/dist/esm/bin/commands/list.js +4 -7
- package/dist/esm/bin/commands/list.js.map +1 -1
- package/dist/esm/bin/commands/prune.js +9 -21
- package/dist/esm/bin/commands/prune.js.map +1 -1
- package/dist/esm/bin/commands/serve.js +1 -1
- package/dist/esm/bin/commands/serve.js.map +1 -1
- package/dist/esm/bin/commands/start.js +3 -3
- package/dist/esm/bin/commands/start.js.map +1 -1
- package/dist/esm/bin/utils/run.js +159 -180
- package/dist/esm/bin/utils/run.js.map +1 -1
- package/dist/esm/build/index.js +1 -48
- package/dist/esm/build/index.js.map +1 -1
- package/dist/esm/build/plugin.js +1 -1
- package/dist/esm/client/index.js +9 -13
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/database/index.js +429 -141
- package/dist/esm/database/index.js.map +1 -1
- package/dist/esm/drizzle/index.js.map +1 -1
- package/dist/esm/drizzle/kit/index.js.map +1 -1
- package/dist/esm/drizzle/onchain.js +1 -8
- package/dist/esm/drizzle/onchain.js.map +1 -1
- package/dist/esm/graphql/index.js +16 -19
- package/dist/esm/graphql/index.js.map +1 -1
- package/dist/esm/graphql/middleware.js +7 -3
- package/dist/esm/graphql/middleware.js.map +1 -1
- package/dist/esm/indexing-store/cache.js +32 -26
- package/dist/esm/indexing-store/cache.js.map +1 -1
- package/dist/esm/indexing-store/historical.js +32 -23
- package/dist/esm/indexing-store/historical.js.map +1 -1
- package/dist/esm/indexing-store/index.js +18 -1
- package/dist/esm/indexing-store/index.js.map +1 -1
- package/dist/esm/indexing-store/realtime.js +140 -89
- package/dist/esm/indexing-store/realtime.js.map +1 -1
- package/dist/esm/internal/errors.js +0 -12
- package/dist/esm/internal/errors.js.map +1 -1
- package/dist/esm/server/index.js +2 -10
- package/dist/esm/server/index.js.map +1 -1
- package/dist/esm/sync-store/index.js +432 -403
- package/dist/esm/sync-store/index.js.map +1 -1
- package/dist/esm/utils/wait.js +0 -2
- package/dist/esm/utils/wait.js.map +1 -1
- package/dist/types/bin/commands/createViews.d.ts.map +1 -1
- package/dist/types/bin/commands/list.d.ts.map +1 -1
- package/dist/types/bin/commands/prune.d.ts.map +1 -1
- package/dist/types/bin/commands/start.d.ts +0 -2
- package/dist/types/bin/commands/start.d.ts.map +1 -1
- package/dist/types/bin/utils/run.d.ts +1 -1
- package/dist/types/bin/utils/run.d.ts.map +1 -1
- package/dist/types/build/index.d.ts +1 -1
- package/dist/types/build/index.d.ts.map +1 -1
- package/dist/types/client/index.d.ts.map +1 -1
- package/dist/types/database/index.d.ts +73 -25
- package/dist/types/database/index.d.ts.map +1 -1
- package/dist/types/drizzle/index.d.ts +3 -2
- package/dist/types/drizzle/index.d.ts.map +1 -1
- package/dist/types/drizzle/kit/index.d.ts +4 -3
- package/dist/types/drizzle/kit/index.d.ts.map +1 -1
- package/dist/types/drizzle/onchain.d.ts +5 -12
- package/dist/types/drizzle/onchain.d.ts.map +1 -1
- package/dist/types/graphql/index.d.ts +4 -2
- package/dist/types/graphql/index.d.ts.map +1 -1
- package/dist/types/graphql/middleware.d.ts +1 -1
- package/dist/types/graphql/middleware.d.ts.map +1 -1
- package/dist/types/indexing-store/cache.d.ts +12 -5
- package/dist/types/indexing-store/cache.d.ts.map +1 -1
- package/dist/types/indexing-store/historical.d.ts +7 -2
- package/dist/types/indexing-store/historical.d.ts.map +1 -1
- package/dist/types/indexing-store/index.d.ts +2 -4
- package/dist/types/indexing-store/index.d.ts.map +1 -1
- package/dist/types/indexing-store/realtime.d.ts +3 -1
- package/dist/types/indexing-store/realtime.d.ts.map +1 -1
- package/dist/types/internal/errors.d.ts +0 -4
- package/dist/types/internal/errors.d.ts.map +1 -1
- package/dist/types/server/index.d.ts +1 -1
- package/dist/types/server/index.d.ts.map +1 -1
- package/dist/types/sync/index.d.ts +1 -1
- package/dist/types/sync-store/index.d.ts.map +1 -1
- package/dist/types/utils/wait.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/bin/commands/createViews.ts +26 -37
- package/src/bin/commands/dev.ts +1 -1
- package/src/bin/commands/list.ts +4 -7
- package/src/bin/commands/prune.ts +17 -31
- package/src/bin/commands/serve.ts +1 -1
- package/src/bin/commands/start.ts +3 -4
- package/src/bin/utils/run.ts +210 -256
- package/src/build/index.ts +2 -53
- package/src/build/plugin.ts +1 -1
- package/src/client/index.ts +10 -21
- package/src/database/index.ts +742 -331
- package/src/drizzle/index.ts +3 -2
- package/src/drizzle/kit/index.ts +5 -2
- package/src/drizzle/onchain.ts +2 -26
- package/src/graphql/index.ts +26 -31
- package/src/graphql/middleware.ts +7 -5
- package/src/indexing-store/cache.ts +52 -35
- package/src/indexing-store/historical.ts +40 -28
- package/src/indexing-store/index.ts +27 -2
- package/src/indexing-store/realtime.ts +220 -176
- package/src/internal/errors.ts +0 -9
- package/src/server/index.ts +3 -14
- package/src/sync-store/index.ts +997 -870
- package/src/utils/wait.ts +0 -1
- package/dist/esm/database/queryBuilder.js +0 -206
- package/dist/esm/database/queryBuilder.js.map +0 -1
- package/dist/esm/database/utils.js +0 -100
- package/dist/esm/database/utils.js.map +0 -1
- package/dist/esm/drizzle/json.js +0 -119
- package/dist/esm/drizzle/json.js.map +0 -1
- package/dist/types/database/queryBuilder.d.ts +0 -37
- package/dist/types/database/queryBuilder.d.ts.map +0 -1
- package/dist/types/database/utils.d.ts +0 -25
- package/dist/types/database/utils.d.ts.map +0 -1
- package/dist/types/drizzle/json.d.ts +0 -51
- package/dist/types/drizzle/json.d.ts.map +0 -1
- package/src/database/queryBuilder.ts +0 -319
- package/src/database/utils.ts +0 -140
- package/src/drizzle/json.ts +0 -154
package/src/database/index.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
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
|
|
14
|
-
import {
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
201
|
-
()
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
()
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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.
|
|
328
|
+
await driver.internal.query(
|
|
281
329
|
`CREATE SCHEMA IF NOT EXISTS "${namespace.schema}"`,
|
|
282
330
|
);
|
|
283
331
|
|
|
284
|
-
|
|
285
|
-
(
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
|
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.
|
|
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: "
|
|
352
|
-
this.set({ pool: "
|
|
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: "
|
|
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
|
|
406
|
+
(table): table is PgTableWithColumns<TableConfig> => is(table, PgTable),
|
|
381
407
|
);
|
|
382
408
|
|
|
383
|
-
|
|
409
|
+
const database = {
|
|
384
410
|
driver,
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
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
|
-
|
|
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
|
-
|
|
414
|
-
|
|
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:
|
|
545
|
+
{ method: options.method },
|
|
418
546
|
endClock(),
|
|
419
547
|
);
|
|
420
548
|
|
|
421
|
-
|
|
549
|
+
if (common.shutdown.isKilled) {
|
|
550
|
+
throw new ShutdownError();
|
|
551
|
+
}
|
|
552
|
+
return result;
|
|
422
553
|
} catch (_error) {
|
|
423
|
-
const 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:
|
|
561
|
+
{ method: options.method },
|
|
431
562
|
endClock(),
|
|
432
563
|
);
|
|
433
564
|
common.metrics.ponder_database_method_error_total.inc({
|
|
434
|
-
method:
|
|
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 '
|
|
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 ===
|
|
582
|
+
if (i === RETRY_COUNT) {
|
|
447
583
|
common.logger.warn({
|
|
448
584
|
service: "database",
|
|
449
|
-
msg: `Failed '
|
|
585
|
+
msg: `Failed '${options.method}' database method after '${i + 1}' attempts (id=${id})`,
|
|
450
586
|
error,
|
|
451
587
|
});
|
|
452
|
-
throw
|
|
588
|
+
throw firstError;
|
|
453
589
|
}
|
|
454
590
|
|
|
455
|
-
const duration =
|
|
591
|
+
const duration = BASE_DURATION * 2 ** i;
|
|
456
592
|
common.logger.debug({
|
|
457
593
|
service: "database",
|
|
458
|
-
msg: `Failed '
|
|
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
|
|
467
|
-
|
|
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
|
-
|
|
475
|
-
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
700
|
+
const trigger = "status_trigger";
|
|
701
|
+
const notification = "status_notify()";
|
|
702
|
+
const channel = `${namespace.schema}_status_channel`;
|
|
487
703
|
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
502
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
`
|
|
826
|
+
`TRUNCATE TABLE "${namespace.schema}"."${getTableName(PONDER_CHECKPOINT)}" CASCADE`,
|
|
589
827
|
),
|
|
590
828
|
);
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
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
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
612
|
-
|
|
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: `
|
|
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
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
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
|
-
|
|
653
|
-
return { status: "locked", expiry } as const;
|
|
654
|
-
}
|
|
895
|
+
// Remove triggers
|
|
655
896
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
-
|
|
924
|
+
await this.revert({ checkpoint: revertCheckpoint, tx });
|
|
686
925
|
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
|
746
|
-
|
|
747
|
-
|
|
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
|
};
|