@apibara/plugin-drizzle 2.1.0-beta.5 → 2.1.0-beta.50
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/dist/index.cjs +325 -67
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +172 -4
- package/dist/index.d.mts +172 -4
- package/dist/index.d.ts +172 -4
- package/dist/index.mjs +314 -59
- package/dist/index.mjs.map +1 -0
- package/dist/shared/plugin-drizzle.2d226351.mjs +6 -0
- package/dist/shared/plugin-drizzle.2d226351.mjs.map +1 -0
- package/dist/shared/plugin-drizzle.cae20704.cjs +10 -0
- package/dist/shared/plugin-drizzle.cae20704.cjs.map +1 -0
- package/dist/testing.cjs +14 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +6 -0
- package/dist/testing.d.mts +6 -0
- package/dist/testing.d.ts +6 -0
- package/dist/testing.mjs +12 -0
- package/dist/testing.mjs.map +1 -0
- package/package.json +21 -6
- package/src/constants.ts +3 -0
- package/src/helper.ts +219 -0
- package/src/index.ts +191 -21
- package/src/persistence.ts +139 -18
- package/src/storage.ts +88 -23
- package/src/testing.ts +13 -0
- package/src/utils.ts +19 -0
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useIndexerContext } from "@apibara/indexer";
|
|
2
|
-
import { defineIndexerPlugin } from "@apibara/indexer/plugins";
|
|
2
|
+
import { defineIndexerPlugin, useLogger } from "@apibara/indexer/plugins";
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
5
|
ExtractTablesWithRelations,
|
|
@@ -14,23 +14,37 @@ import type {
|
|
|
14
14
|
PgQueryResultHKT,
|
|
15
15
|
PgTransaction,
|
|
16
16
|
} from "drizzle-orm/pg-core";
|
|
17
|
+
import { DRIZZLE_PROPERTY, DRIZZLE_STORAGE_DB_PROPERTY } from "./constants";
|
|
18
|
+
import { type MigrateOptions, migrate } from "./helper";
|
|
17
19
|
import {
|
|
18
20
|
finalizeState,
|
|
19
21
|
getState,
|
|
20
22
|
initializePersistentState,
|
|
21
23
|
invalidateState,
|
|
22
24
|
persistState,
|
|
25
|
+
recordChainReorganization,
|
|
26
|
+
resetPersistence,
|
|
23
27
|
} from "./persistence";
|
|
24
28
|
import {
|
|
29
|
+
cleanupStorage,
|
|
25
30
|
finalize,
|
|
26
31
|
initializeReorgRollbackTable,
|
|
27
32
|
invalidate,
|
|
28
33
|
registerTriggers,
|
|
29
34
|
removeTriggers,
|
|
30
35
|
} from "./storage";
|
|
31
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
DrizzleStorageError,
|
|
38
|
+
type IdColumnMap,
|
|
39
|
+
getIdColumnForTable,
|
|
40
|
+
sleep,
|
|
41
|
+
withTransaction,
|
|
42
|
+
} from "./utils";
|
|
43
|
+
|
|
44
|
+
export * from "./helper";
|
|
45
|
+
|
|
46
|
+
export type { IdColumnMap };
|
|
32
47
|
|
|
33
|
-
const DRIZZLE_PROPERTY = "_drizzle";
|
|
34
48
|
const MAX_RETRIES = 5;
|
|
35
49
|
|
|
36
50
|
export type DrizzleStorage<
|
|
@@ -61,17 +75,82 @@ export function useDrizzleStorage<
|
|
|
61
75
|
return context[DRIZZLE_PROPERTY];
|
|
62
76
|
}
|
|
63
77
|
|
|
78
|
+
export function useTestDrizzleStorage<
|
|
79
|
+
TQueryResult extends PgQueryResultHKT,
|
|
80
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
81
|
+
TSchema extends
|
|
82
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
83
|
+
>(): PgDatabase<TQueryResult, TFullSchema, TSchema> {
|
|
84
|
+
const context = useIndexerContext();
|
|
85
|
+
|
|
86
|
+
if (!context[DRIZZLE_STORAGE_DB_PROPERTY]) {
|
|
87
|
+
throw new DrizzleStorageError(
|
|
88
|
+
"drizzle storage db is not available. Did you register the plugin?",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return context[DRIZZLE_STORAGE_DB_PROPERTY];
|
|
93
|
+
}
|
|
94
|
+
|
|
64
95
|
export interface DrizzleStorageOptions<
|
|
65
96
|
TQueryResult extends PgQueryResultHKT,
|
|
66
97
|
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
67
98
|
TSchema extends
|
|
68
99
|
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
69
100
|
> {
|
|
101
|
+
/**
|
|
102
|
+
* The Drizzle database instance.
|
|
103
|
+
*/
|
|
70
104
|
db: PgDatabase<TQueryResult, TFullSchema, TSchema>;
|
|
105
|
+
/**
|
|
106
|
+
* Whether to persist the indexer's state. Defaults to true.
|
|
107
|
+
*/
|
|
71
108
|
persistState?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* The name of the indexer. Default value is 'default'.
|
|
111
|
+
*/
|
|
72
112
|
indexerName?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The schema of the database.
|
|
115
|
+
*/
|
|
73
116
|
schema?: Record<string, unknown>;
|
|
74
|
-
|
|
117
|
+
/**
|
|
118
|
+
* The column to use as the primary identifier for each table.
|
|
119
|
+
*
|
|
120
|
+
* This identifier is used for tracking changes during reorgs and rollbacks.
|
|
121
|
+
*
|
|
122
|
+
* Can be specified in two ways:
|
|
123
|
+
*
|
|
124
|
+
* 1. As a single string that applies to all tables:
|
|
125
|
+
* ```ts
|
|
126
|
+
* idColumn: "_id" // Uses "_id" column for all tables
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* 2. As an object mapping table names to their ID columns:
|
|
130
|
+
* ```ts
|
|
131
|
+
* idColumn: {
|
|
132
|
+
* transfers: "transaction_hash", // Use "transaction_hash" for transfers table
|
|
133
|
+
* blocks: "block_number", // Use "block_number" for blocks table
|
|
134
|
+
* "*": "_id" // Use "_id" for all other tables | defaults to "id"
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* The special "*" key acts as a fallback for any tables not explicitly mapped.
|
|
139
|
+
*
|
|
140
|
+
* @default "id"
|
|
141
|
+
* @type {string | Partial<IdColumnMap>}
|
|
142
|
+
*/
|
|
143
|
+
idColumn?: string | Partial<IdColumnMap>;
|
|
144
|
+
/**
|
|
145
|
+
* The options for the database migration. When provided, the database will automatically run migrations before the indexer runs.
|
|
146
|
+
*/
|
|
147
|
+
migrate?: MigrateOptions;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Whether to record chain reorganizations in the database.
|
|
151
|
+
* @default false
|
|
152
|
+
*/
|
|
153
|
+
recordChainReorganizations?: boolean;
|
|
75
154
|
}
|
|
76
155
|
|
|
77
156
|
/**
|
|
@@ -83,6 +162,8 @@ export interface DrizzleStorageOptions<
|
|
|
83
162
|
* @param options.indexerName - The name of the indexer. Defaults value is 'default'.
|
|
84
163
|
* @param options.schema - The schema of the database.
|
|
85
164
|
* @param options.idColumn - The column to use as the id. Defaults to 'id'.
|
|
165
|
+
* @param options.migrate - The options for the database migration. when provided, the database will automatically run migrations before the indexer runs.
|
|
166
|
+
* @param options.recordChainReorganizations - Whether to record chain reorganizations in the database. Defaults to false.
|
|
86
167
|
*/
|
|
87
168
|
export function drizzleStorage<
|
|
88
169
|
TFilter,
|
|
@@ -95,42 +176,102 @@ export function drizzleStorage<
|
|
|
95
176
|
db,
|
|
96
177
|
persistState: enablePersistence = true,
|
|
97
178
|
indexerName: identifier = "default",
|
|
98
|
-
schema,
|
|
99
|
-
idColumn
|
|
179
|
+
schema: _schema,
|
|
180
|
+
idColumn,
|
|
181
|
+
migrate: migrateOptions,
|
|
182
|
+
recordChainReorganizations = false,
|
|
100
183
|
}: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>) {
|
|
101
184
|
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
102
185
|
let tableNames: string[] = [];
|
|
103
186
|
let indexerId = "";
|
|
187
|
+
const alwaysReindex = process.env["APIBARA_ALWAYS_REINDEX"] === "true";
|
|
188
|
+
let prevFinality: DataFinality | undefined;
|
|
189
|
+
const schema: TSchema = (_schema as TSchema) ?? db._.schema ?? {};
|
|
190
|
+
const idColumnMap: IdColumnMap = {
|
|
191
|
+
"*": typeof idColumn === "string" ? idColumn : "id",
|
|
192
|
+
...(typeof idColumn === "object" ? idColumn : {}),
|
|
193
|
+
};
|
|
104
194
|
|
|
105
195
|
try {
|
|
106
|
-
tableNames = Object.values(
|
|
107
|
-
(table) => table.dbName,
|
|
108
|
-
);
|
|
196
|
+
tableNames = Object.values(schema).map((table) => table.dbName);
|
|
109
197
|
} catch (error) {
|
|
110
198
|
throw new DrizzleStorageError("Failed to get table names from schema", {
|
|
111
199
|
cause: error,
|
|
112
200
|
});
|
|
113
201
|
}
|
|
114
202
|
|
|
115
|
-
|
|
203
|
+
// Check if specified idColumn exists in all the tables in schema
|
|
204
|
+
for (const table of Object.values(schema)) {
|
|
205
|
+
const columns = table.columns;
|
|
206
|
+
const tableIdColumn = getIdColumnForTable(table.dbName, idColumnMap);
|
|
207
|
+
|
|
208
|
+
const columnExists = Object.values(columns).some(
|
|
209
|
+
(column) => column.name === tableIdColumn,
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
if (!columnExists) {
|
|
213
|
+
throw new DrizzleStorageError(
|
|
214
|
+
`Column \`"${tableIdColumn}"\` does not exist in table \`"${table.dbName}"\`. ` +
|
|
215
|
+
"Make sure the table has the specified column or provide a valid `idColumn` mapping to `drizzleStorage`.",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
indexer.hooks.hook("plugins:init", async () => {
|
|
221
|
+
const internalContext = useInternalContext();
|
|
222
|
+
const context = useIndexerContext();
|
|
223
|
+
const logger = useLogger();
|
|
224
|
+
|
|
225
|
+
// For testing purposes using vcr.
|
|
226
|
+
context[DRIZZLE_STORAGE_DB_PROPERTY] = db;
|
|
227
|
+
|
|
116
228
|
const { indexerName: indexerFileName, availableIndexers } =
|
|
117
|
-
|
|
229
|
+
internalContext;
|
|
118
230
|
|
|
119
231
|
indexerId = generateIndexerId(indexerFileName, identifier);
|
|
120
232
|
|
|
121
233
|
let retries = 0;
|
|
122
234
|
|
|
235
|
+
// incase the migrations are already applied, we don't want to run them again
|
|
236
|
+
let migrationsApplied = false;
|
|
237
|
+
let cleanupApplied = false;
|
|
238
|
+
|
|
123
239
|
while (retries <= MAX_RETRIES) {
|
|
124
240
|
try {
|
|
241
|
+
if (migrateOptions && !migrationsApplied) {
|
|
242
|
+
// @ts-ignore type mismatch for db
|
|
243
|
+
await migrate(db, migrateOptions);
|
|
244
|
+
migrationsApplied = true;
|
|
245
|
+
logger.success("Migrations applied");
|
|
246
|
+
}
|
|
125
247
|
await withTransaction(db, async (tx) => {
|
|
126
248
|
await initializeReorgRollbackTable(tx, indexerId);
|
|
127
249
|
if (enablePersistence) {
|
|
128
250
|
await initializePersistentState(tx);
|
|
129
251
|
}
|
|
252
|
+
|
|
253
|
+
if (alwaysReindex && !cleanupApplied) {
|
|
254
|
+
logger.warn(
|
|
255
|
+
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
await cleanupStorage(tx, tableNames, indexerId);
|
|
259
|
+
|
|
260
|
+
if (enablePersistence) {
|
|
261
|
+
await resetPersistence({ tx, indexerId });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
cleanupApplied = true;
|
|
265
|
+
|
|
266
|
+
logger.success("Tables have been cleaned up for reindexing");
|
|
267
|
+
}
|
|
130
268
|
});
|
|
131
269
|
break;
|
|
132
270
|
} catch (error) {
|
|
133
271
|
if (retries === MAX_RETRIES) {
|
|
272
|
+
if (error instanceof DrizzleStorageError) {
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
134
275
|
throw new DrizzleStorageError(
|
|
135
276
|
"Initialization failed after 5 retries",
|
|
136
277
|
{
|
|
@@ -177,7 +318,8 @@ export function drizzleStorage<
|
|
|
177
318
|
}
|
|
178
319
|
|
|
179
320
|
await withTransaction(db, async (tx) => {
|
|
180
|
-
|
|
321
|
+
// Use the appropriate idColumn for each table when calling invalidate
|
|
322
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
181
323
|
|
|
182
324
|
if (enablePersistence) {
|
|
183
325
|
await invalidateState({ tx, cursor, indexerId });
|
|
@@ -204,7 +346,7 @@ export function drizzleStorage<
|
|
|
204
346
|
});
|
|
205
347
|
|
|
206
348
|
indexer.hooks.hook("message:finalize", async ({ message }) => {
|
|
207
|
-
const { cursor } = message
|
|
349
|
+
const { cursor } = message;
|
|
208
350
|
|
|
209
351
|
if (!cursor) {
|
|
210
352
|
throw new DrizzleStorageError("Finalized Cursor is undefined");
|
|
@@ -220,14 +362,36 @@ export function drizzleStorage<
|
|
|
220
362
|
});
|
|
221
363
|
|
|
222
364
|
indexer.hooks.hook("message:invalidate", async ({ message }) => {
|
|
223
|
-
const { cursor } = message
|
|
365
|
+
const { cursor } = message;
|
|
224
366
|
|
|
225
367
|
if (!cursor) {
|
|
226
368
|
throw new DrizzleStorageError("Invalidate Cursor is undefined");
|
|
227
369
|
}
|
|
228
370
|
|
|
229
371
|
await withTransaction(db, async (tx) => {
|
|
230
|
-
|
|
372
|
+
let oldHead: Cursor | undefined;
|
|
373
|
+
|
|
374
|
+
if (recordChainReorganizations) {
|
|
375
|
+
const { cursor: currentCursor } = await getState<
|
|
376
|
+
TFilter,
|
|
377
|
+
TQueryResult,
|
|
378
|
+
TFullSchema,
|
|
379
|
+
TSchema
|
|
380
|
+
>({
|
|
381
|
+
tx,
|
|
382
|
+
indexerId,
|
|
383
|
+
});
|
|
384
|
+
oldHead = currentCursor;
|
|
385
|
+
|
|
386
|
+
await recordChainReorganization({
|
|
387
|
+
tx,
|
|
388
|
+
indexerId,
|
|
389
|
+
oldHead,
|
|
390
|
+
newHead: cursor,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
231
395
|
|
|
232
396
|
if (enablePersistence) {
|
|
233
397
|
await invalidateState({ tx, cursor, indexerId });
|
|
@@ -238,7 +402,8 @@ export function drizzleStorage<
|
|
|
238
402
|
indexer.hooks.hook("handler:middleware", async ({ use }) => {
|
|
239
403
|
use(async (context, next) => {
|
|
240
404
|
try {
|
|
241
|
-
const { endCursor, finality } = context as {
|
|
405
|
+
const { endCursor, finality, cursor } = context as {
|
|
406
|
+
cursor: Cursor;
|
|
242
407
|
endCursor: Cursor;
|
|
243
408
|
finality: DataFinality;
|
|
244
409
|
};
|
|
@@ -254,12 +419,17 @@ export function drizzleStorage<
|
|
|
254
419
|
TSchema
|
|
255
420
|
>;
|
|
256
421
|
|
|
422
|
+
if (prevFinality === "pending") {
|
|
423
|
+
// invalidate if previous block's finality was "pending"
|
|
424
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
425
|
+
}
|
|
426
|
+
|
|
257
427
|
if (finality !== "finalized") {
|
|
258
428
|
await registerTriggers(
|
|
259
429
|
tx,
|
|
260
430
|
tableNames,
|
|
261
431
|
endCursor,
|
|
262
|
-
|
|
432
|
+
idColumnMap,
|
|
263
433
|
indexerId,
|
|
264
434
|
);
|
|
265
435
|
}
|
|
@@ -267,13 +437,15 @@ export function drizzleStorage<
|
|
|
267
437
|
await next();
|
|
268
438
|
delete context[DRIZZLE_PROPERTY];
|
|
269
439
|
|
|
270
|
-
if (enablePersistence) {
|
|
440
|
+
if (enablePersistence && finality !== "pending") {
|
|
271
441
|
await persistState({
|
|
272
442
|
tx,
|
|
273
443
|
endCursor,
|
|
274
444
|
indexerId,
|
|
275
445
|
});
|
|
276
446
|
}
|
|
447
|
+
|
|
448
|
+
prevFinality = finality;
|
|
277
449
|
});
|
|
278
450
|
|
|
279
451
|
if (finality !== "finalized") {
|
|
@@ -283,9 +455,7 @@ export function drizzleStorage<
|
|
|
283
455
|
} catch (error) {
|
|
284
456
|
await removeTriggers(db, tableNames, indexerId);
|
|
285
457
|
|
|
286
|
-
throw
|
|
287
|
-
cause: error,
|
|
288
|
-
});
|
|
458
|
+
throw error;
|
|
289
459
|
}
|
|
290
460
|
});
|
|
291
461
|
});
|
package/src/persistence.ts
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
import { type Cursor, normalizeCursor } from "@apibara/protocol";
|
|
2
|
-
import { and, eq, gt, isNull, lt } from "drizzle-orm";
|
|
2
|
+
import { and, eq, gt, isNull, lt, sql } from "drizzle-orm";
|
|
3
3
|
import type {
|
|
4
4
|
ExtractTablesWithRelations,
|
|
5
5
|
TablesRelationalConfig,
|
|
6
6
|
} from "drizzle-orm";
|
|
7
7
|
import type { PgQueryResultHKT, PgTransaction } from "drizzle-orm/pg-core";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
integer,
|
|
10
|
+
pgSchema,
|
|
11
|
+
primaryKey,
|
|
12
|
+
serial,
|
|
13
|
+
text,
|
|
14
|
+
timestamp,
|
|
15
|
+
} from "drizzle-orm/pg-core";
|
|
16
|
+
import { SCHEMA_NAME } from "./constants";
|
|
9
17
|
import { DrizzleStorageError, deserialize, serialize } from "./utils";
|
|
10
18
|
|
|
11
|
-
const CHECKPOINTS_TABLE_NAME = "
|
|
12
|
-
const FILTERS_TABLE_NAME = "
|
|
13
|
-
const SCHEMA_VERSION_TABLE_NAME = "
|
|
19
|
+
const CHECKPOINTS_TABLE_NAME = "checkpoints";
|
|
20
|
+
const FILTERS_TABLE_NAME = "filters";
|
|
21
|
+
const SCHEMA_VERSION_TABLE_NAME = "schema_version";
|
|
14
22
|
|
|
15
|
-
|
|
23
|
+
const schema = pgSchema(SCHEMA_NAME);
|
|
24
|
+
|
|
25
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
26
|
+
export const checkpoints = schema.table(CHECKPOINTS_TABLE_NAME, {
|
|
16
27
|
id: text("id").notNull().primaryKey(),
|
|
17
28
|
orderKey: integer("order_key").notNull(),
|
|
18
29
|
uniqueKey: text("unique_key"),
|
|
19
30
|
});
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
33
|
+
export const filters = schema.table(
|
|
22
34
|
FILTERS_TABLE_NAME,
|
|
23
35
|
{
|
|
24
36
|
id: text("id").notNull(),
|
|
@@ -33,7 +45,23 @@ export const filters = pgTable(
|
|
|
33
45
|
],
|
|
34
46
|
);
|
|
35
47
|
|
|
36
|
-
|
|
48
|
+
/** Table for recording chain reorganizations */
|
|
49
|
+
export const chainReorganizations = schema.table("chain_reorganizations", {
|
|
50
|
+
id: serial("id").primaryKey(),
|
|
51
|
+
indexerId: text("indexer_id").notNull(),
|
|
52
|
+
oldHeadOrderKey: integer("old_head_order_key"),
|
|
53
|
+
oldHeadUniqueKey: text("old_head_unique_key")
|
|
54
|
+
.$type<string | null>()
|
|
55
|
+
.default(null),
|
|
56
|
+
newHeadOrderKey: integer("new_head_order_key").notNull(),
|
|
57
|
+
newHeadUniqueKey: text("new_head_unique_key")
|
|
58
|
+
.$type<string | null>()
|
|
59
|
+
.default(null),
|
|
60
|
+
recordedAt: timestamp("recorded_at").defaultNow().notNull(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
64
|
+
export const schemaVersion = schema.table(SCHEMA_VERSION_TABLE_NAME, {
|
|
37
65
|
k: integer("k").notNull().primaryKey(),
|
|
38
66
|
version: integer("version").notNull(),
|
|
39
67
|
});
|
|
@@ -53,13 +81,43 @@ export async function initializePersistentState<
|
|
|
53
81
|
TSchema extends
|
|
54
82
|
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
55
83
|
>(tx: PgTransaction<TQueryResult, TFullSchema, TSchema>) {
|
|
84
|
+
// Create schema if it doesn't exist
|
|
85
|
+
await tx.execute(
|
|
86
|
+
sql.raw(`
|
|
87
|
+
CREATE SCHEMA IF NOT EXISTS ${SCHEMA_NAME};
|
|
88
|
+
`),
|
|
89
|
+
);
|
|
90
|
+
|
|
56
91
|
// Create schema version table
|
|
57
|
-
await tx.execute(
|
|
58
|
-
|
|
92
|
+
await tx.execute(
|
|
93
|
+
sql.raw(`
|
|
94
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${SCHEMA_VERSION_TABLE_NAME} (
|
|
59
95
|
k INTEGER PRIMARY KEY,
|
|
60
96
|
version INTEGER NOT NULL
|
|
61
97
|
);
|
|
62
|
-
`)
|
|
98
|
+
`),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await tx.execute(
|
|
102
|
+
sql.raw(`
|
|
103
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.chain_reorganizations (
|
|
104
|
+
id SERIAL PRIMARY KEY,
|
|
105
|
+
indexer_id TEXT NOT NULL,
|
|
106
|
+
old_head_order_key INTEGER,
|
|
107
|
+
old_head_unique_key TEXT DEFAULT NULL,
|
|
108
|
+
new_head_order_key INTEGER NOT NULL,
|
|
109
|
+
new_head_unique_key TEXT DEFAULT NULL,
|
|
110
|
+
recorded_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
111
|
+
);
|
|
112
|
+
`),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await tx.execute(
|
|
116
|
+
sql.raw(`
|
|
117
|
+
CREATE INDEX IF NOT EXISTS idx_chain_reorgs_indexer_id
|
|
118
|
+
ON ${SCHEMA_NAME}.chain_reorganizations(indexer_id);
|
|
119
|
+
`),
|
|
120
|
+
);
|
|
63
121
|
|
|
64
122
|
// Get current schema version
|
|
65
123
|
const versionRows = await tx
|
|
@@ -80,23 +138,27 @@ export async function initializePersistentState<
|
|
|
80
138
|
try {
|
|
81
139
|
if (storedVersion === -1) {
|
|
82
140
|
// First time initialization
|
|
83
|
-
await tx.execute(
|
|
84
|
-
|
|
141
|
+
await tx.execute(
|
|
142
|
+
sql.raw(`
|
|
143
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${CHECKPOINTS_TABLE_NAME} (
|
|
85
144
|
id TEXT PRIMARY KEY,
|
|
86
145
|
order_key INTEGER NOT NULL,
|
|
87
146
|
unique_key TEXT
|
|
88
147
|
);
|
|
89
|
-
`)
|
|
148
|
+
`),
|
|
149
|
+
);
|
|
90
150
|
|
|
91
|
-
await tx.execute(
|
|
92
|
-
|
|
151
|
+
await tx.execute(
|
|
152
|
+
sql.raw(`
|
|
153
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${FILTERS_TABLE_NAME} (
|
|
93
154
|
id TEXT NOT NULL,
|
|
94
155
|
filter TEXT NOT NULL,
|
|
95
156
|
from_block INTEGER NOT NULL,
|
|
96
157
|
to_block INTEGER DEFAULT NULL,
|
|
97
158
|
PRIMARY KEY (id, from_block)
|
|
98
159
|
);
|
|
99
|
-
`)
|
|
160
|
+
`),
|
|
161
|
+
);
|
|
100
162
|
|
|
101
163
|
// Set initial schema version
|
|
102
164
|
await tx.insert(schemaVersion).values({
|
|
@@ -128,6 +190,34 @@ export async function initializePersistentState<
|
|
|
128
190
|
}
|
|
129
191
|
}
|
|
130
192
|
|
|
193
|
+
export async function recordChainReorganization<
|
|
194
|
+
TQueryResult extends PgQueryResultHKT,
|
|
195
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
196
|
+
TSchema extends
|
|
197
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
198
|
+
>(props: {
|
|
199
|
+
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
200
|
+
indexerId: string;
|
|
201
|
+
oldHead: Cursor | undefined;
|
|
202
|
+
newHead: Cursor;
|
|
203
|
+
}) {
|
|
204
|
+
const { tx, indexerId, oldHead, newHead } = props;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
await tx.insert(chainReorganizations).values({
|
|
208
|
+
indexerId: indexerId,
|
|
209
|
+
oldHeadOrderKey: oldHead ? Number(oldHead.orderKey) : null,
|
|
210
|
+
oldHeadUniqueKey: oldHead?.uniqueKey ? oldHead.uniqueKey : null,
|
|
211
|
+
newHeadOrderKey: Number(newHead.orderKey),
|
|
212
|
+
newHeadUniqueKey: newHead.uniqueKey ? newHead.uniqueKey : null,
|
|
213
|
+
});
|
|
214
|
+
} catch (error) {
|
|
215
|
+
throw new DrizzleStorageError("Failed to record chain reorganization", {
|
|
216
|
+
cause: error,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
131
221
|
export async function persistState<
|
|
132
222
|
TFilter,
|
|
133
223
|
TQueryResult extends PgQueryResultHKT,
|
|
@@ -155,7 +245,9 @@ export async function persistState<
|
|
|
155
245
|
target: checkpoints.id,
|
|
156
246
|
set: {
|
|
157
247
|
orderKey: Number(endCursor.orderKey),
|
|
158
|
-
|
|
248
|
+
// Explicitly set the unique key to `null` to indicate that it has been deleted
|
|
249
|
+
// Otherwise drizzle will not update its value.
|
|
250
|
+
uniqueKey: endCursor.uniqueKey ? endCursor.uniqueKey : null,
|
|
159
251
|
},
|
|
160
252
|
});
|
|
161
253
|
|
|
@@ -245,6 +337,14 @@ export async function invalidateState<
|
|
|
245
337
|
const { tx, cursor, indexerId } = props;
|
|
246
338
|
|
|
247
339
|
try {
|
|
340
|
+
await tx
|
|
341
|
+
.update(checkpoints)
|
|
342
|
+
.set({
|
|
343
|
+
orderKey: Number(cursor.orderKey),
|
|
344
|
+
uniqueKey: cursor.uniqueKey ? cursor.uniqueKey : null,
|
|
345
|
+
})
|
|
346
|
+
.where(eq(checkpoints.id, indexerId));
|
|
347
|
+
|
|
248
348
|
await tx
|
|
249
349
|
.delete(filters)
|
|
250
350
|
.where(
|
|
@@ -297,3 +397,24 @@ export async function finalizeState<
|
|
|
297
397
|
});
|
|
298
398
|
}
|
|
299
399
|
}
|
|
400
|
+
|
|
401
|
+
export async function resetPersistence<
|
|
402
|
+
TQueryResult extends PgQueryResultHKT,
|
|
403
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
404
|
+
TSchema extends
|
|
405
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
406
|
+
>(props: {
|
|
407
|
+
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
408
|
+
indexerId: string;
|
|
409
|
+
}) {
|
|
410
|
+
const { tx, indexerId } = props;
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
await tx.delete(checkpoints).where(eq(checkpoints.id, indexerId));
|
|
414
|
+
await tx.delete(filters).where(eq(filters.id, indexerId));
|
|
415
|
+
} catch (error) {
|
|
416
|
+
throw new DrizzleStorageError("Failed to reset persistence state", {
|
|
417
|
+
cause: error,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|