@apibara/plugin-drizzle 2.1.0-beta.4 → 2.1.0-beta.40
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 +261 -64
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +166 -4
- package/dist/index.d.mts +166 -4
- package/dist/index.d.ts +166 -4
- package/dist/index.mjs +250 -56
- 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 +160 -18
- package/src/persistence.ts +60 -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,36 @@ 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
|
+
resetPersistence,
|
|
23
26
|
} from "./persistence";
|
|
24
27
|
import {
|
|
28
|
+
cleanupStorage,
|
|
25
29
|
finalize,
|
|
26
30
|
initializeReorgRollbackTable,
|
|
27
31
|
invalidate,
|
|
28
32
|
registerTriggers,
|
|
29
33
|
removeTriggers,
|
|
30
34
|
} from "./storage";
|
|
31
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
DrizzleStorageError,
|
|
37
|
+
type IdColumnMap,
|
|
38
|
+
getIdColumnForTable,
|
|
39
|
+
sleep,
|
|
40
|
+
withTransaction,
|
|
41
|
+
} from "./utils";
|
|
42
|
+
|
|
43
|
+
export * from "./helper";
|
|
44
|
+
|
|
45
|
+
export type { IdColumnMap };
|
|
32
46
|
|
|
33
|
-
const DRIZZLE_PROPERTY = "_drizzle";
|
|
34
47
|
const MAX_RETRIES = 5;
|
|
35
48
|
|
|
36
49
|
export type DrizzleStorage<
|
|
@@ -61,17 +74,76 @@ export function useDrizzleStorage<
|
|
|
61
74
|
return context[DRIZZLE_PROPERTY];
|
|
62
75
|
}
|
|
63
76
|
|
|
77
|
+
export function useTestDrizzleStorage<
|
|
78
|
+
TQueryResult extends PgQueryResultHKT,
|
|
79
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
80
|
+
TSchema extends
|
|
81
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
82
|
+
>(): PgDatabase<TQueryResult, TFullSchema, TSchema> {
|
|
83
|
+
const context = useIndexerContext();
|
|
84
|
+
|
|
85
|
+
if (!context[DRIZZLE_STORAGE_DB_PROPERTY]) {
|
|
86
|
+
throw new DrizzleStorageError(
|
|
87
|
+
"drizzle storage db is not available. Did you register the plugin?",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return context[DRIZZLE_STORAGE_DB_PROPERTY];
|
|
92
|
+
}
|
|
93
|
+
|
|
64
94
|
export interface DrizzleStorageOptions<
|
|
65
95
|
TQueryResult extends PgQueryResultHKT,
|
|
66
96
|
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
67
97
|
TSchema extends
|
|
68
98
|
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
69
99
|
> {
|
|
100
|
+
/**
|
|
101
|
+
* The Drizzle database instance.
|
|
102
|
+
*/
|
|
70
103
|
db: PgDatabase<TQueryResult, TFullSchema, TSchema>;
|
|
104
|
+
/**
|
|
105
|
+
* Whether to persist the indexer's state. Defaults to true.
|
|
106
|
+
*/
|
|
71
107
|
persistState?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* The name of the indexer. Default value is 'default'.
|
|
110
|
+
*/
|
|
72
111
|
indexerName?: string;
|
|
112
|
+
/**
|
|
113
|
+
* The schema of the database.
|
|
114
|
+
*/
|
|
73
115
|
schema?: Record<string, unknown>;
|
|
74
|
-
|
|
116
|
+
/**
|
|
117
|
+
* The column to use as the primary identifier for each table.
|
|
118
|
+
*
|
|
119
|
+
* This identifier is used for tracking changes during reorgs and rollbacks.
|
|
120
|
+
*
|
|
121
|
+
* Can be specified in two ways:
|
|
122
|
+
*
|
|
123
|
+
* 1. As a single string that applies to all tables:
|
|
124
|
+
* ```ts
|
|
125
|
+
* idColumn: "_id" // Uses "_id" column for all tables
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* 2. As an object mapping table names to their ID columns:
|
|
129
|
+
* ```ts
|
|
130
|
+
* idColumn: {
|
|
131
|
+
* transfers: "transaction_hash", // Use "transaction_hash" for transfers table
|
|
132
|
+
* blocks: "block_number", // Use "block_number" for blocks table
|
|
133
|
+
* "*": "_id" // Use "_id" for all other tables | defaults to "id"
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* The special "*" key acts as a fallback for any tables not explicitly mapped.
|
|
138
|
+
*
|
|
139
|
+
* @default "id"
|
|
140
|
+
* @type {string | Partial<IdColumnMap>}
|
|
141
|
+
*/
|
|
142
|
+
idColumn?: string | Partial<IdColumnMap>;
|
|
143
|
+
/**
|
|
144
|
+
* The options for the database migration. When provided, the database will automatically run migrations before the indexer runs.
|
|
145
|
+
*/
|
|
146
|
+
migrate?: MigrateOptions;
|
|
75
147
|
}
|
|
76
148
|
|
|
77
149
|
/**
|
|
@@ -83,6 +155,7 @@ export interface DrizzleStorageOptions<
|
|
|
83
155
|
* @param options.indexerName - The name of the indexer. Defaults value is 'default'.
|
|
84
156
|
* @param options.schema - The schema of the database.
|
|
85
157
|
* @param options.idColumn - The column to use as the id. Defaults to 'id'.
|
|
158
|
+
* @param options.migrate - The options for the database migration. when provided, the database will automatically run migrations before the indexer runs.
|
|
86
159
|
*/
|
|
87
160
|
export function drizzleStorage<
|
|
88
161
|
TFilter,
|
|
@@ -95,42 +168,101 @@ export function drizzleStorage<
|
|
|
95
168
|
db,
|
|
96
169
|
persistState: enablePersistence = true,
|
|
97
170
|
indexerName: identifier = "default",
|
|
98
|
-
schema,
|
|
99
|
-
idColumn
|
|
171
|
+
schema: _schema,
|
|
172
|
+
idColumn,
|
|
173
|
+
migrate: migrateOptions,
|
|
100
174
|
}: DrizzleStorageOptions<TQueryResult, TFullSchema, TSchema>) {
|
|
101
175
|
return defineIndexerPlugin<TFilter, TBlock>((indexer) => {
|
|
102
176
|
let tableNames: string[] = [];
|
|
103
177
|
let indexerId = "";
|
|
178
|
+
const alwaysReindex = process.env["APIBARA_ALWAYS_REINDEX"] === "true";
|
|
179
|
+
let prevFinality: DataFinality | undefined;
|
|
180
|
+
const schema: TSchema = (_schema as TSchema) ?? db._.schema ?? {};
|
|
181
|
+
const idColumnMap: IdColumnMap = {
|
|
182
|
+
"*": typeof idColumn === "string" ? idColumn : "id",
|
|
183
|
+
...(typeof idColumn === "object" ? idColumn : {}),
|
|
184
|
+
};
|
|
104
185
|
|
|
105
186
|
try {
|
|
106
|
-
tableNames = Object.values(
|
|
107
|
-
(table) => table.dbName,
|
|
108
|
-
);
|
|
187
|
+
tableNames = Object.values(schema).map((table) => table.dbName);
|
|
109
188
|
} catch (error) {
|
|
110
189
|
throw new DrizzleStorageError("Failed to get table names from schema", {
|
|
111
190
|
cause: error,
|
|
112
191
|
});
|
|
113
192
|
}
|
|
114
193
|
|
|
115
|
-
|
|
194
|
+
// Check if specified idColumn exists in all the tables in schema
|
|
195
|
+
for (const table of Object.values(schema)) {
|
|
196
|
+
const columns = table.columns;
|
|
197
|
+
const tableIdColumn = getIdColumnForTable(table.dbName, idColumnMap);
|
|
198
|
+
|
|
199
|
+
const columnExists = Object.values(columns).some(
|
|
200
|
+
(column) => column.name === tableIdColumn,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
if (!columnExists) {
|
|
204
|
+
throw new DrizzleStorageError(
|
|
205
|
+
`Column \`"${tableIdColumn}"\` does not exist in table \`"${table.dbName}"\`. ` +
|
|
206
|
+
"Make sure the table has the specified column or provide a valid `idColumn` mapping to `drizzleStorage`.",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
indexer.hooks.hook("plugins:init", async () => {
|
|
212
|
+
const internalContext = useInternalContext();
|
|
213
|
+
const context = useIndexerContext();
|
|
214
|
+
const logger = useLogger();
|
|
215
|
+
|
|
216
|
+
// For testing purposes using vcr.
|
|
217
|
+
context[DRIZZLE_STORAGE_DB_PROPERTY] = db;
|
|
218
|
+
|
|
116
219
|
const { indexerName: indexerFileName, availableIndexers } =
|
|
117
|
-
|
|
220
|
+
internalContext;
|
|
118
221
|
|
|
119
222
|
indexerId = generateIndexerId(indexerFileName, identifier);
|
|
120
223
|
|
|
121
224
|
let retries = 0;
|
|
122
225
|
|
|
226
|
+
// incase the migrations are already applied, we don't want to run them again
|
|
227
|
+
let migrationsApplied = false;
|
|
228
|
+
let cleanupApplied = false;
|
|
229
|
+
|
|
123
230
|
while (retries <= MAX_RETRIES) {
|
|
124
231
|
try {
|
|
232
|
+
if (migrateOptions && !migrationsApplied) {
|
|
233
|
+
// @ts-ignore type mismatch for db
|
|
234
|
+
await migrate(db, migrateOptions);
|
|
235
|
+
migrationsApplied = true;
|
|
236
|
+
logger.success("Migrations applied");
|
|
237
|
+
}
|
|
125
238
|
await withTransaction(db, async (tx) => {
|
|
126
239
|
await initializeReorgRollbackTable(tx, indexerId);
|
|
127
240
|
if (enablePersistence) {
|
|
128
241
|
await initializePersistentState(tx);
|
|
129
242
|
}
|
|
243
|
+
|
|
244
|
+
if (alwaysReindex && !cleanupApplied) {
|
|
245
|
+
logger.warn(
|
|
246
|
+
`Reindexing: Deleting all data from tables - ${tableNames.join(", ")}`,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
await cleanupStorage(tx, tableNames, indexerId);
|
|
250
|
+
|
|
251
|
+
if (enablePersistence) {
|
|
252
|
+
await resetPersistence({ tx, indexerId });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
cleanupApplied = true;
|
|
256
|
+
|
|
257
|
+
logger.success("Tables have been cleaned up for reindexing");
|
|
258
|
+
}
|
|
130
259
|
});
|
|
131
260
|
break;
|
|
132
261
|
} catch (error) {
|
|
133
262
|
if (retries === MAX_RETRIES) {
|
|
263
|
+
if (error instanceof DrizzleStorageError) {
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
134
266
|
throw new DrizzleStorageError(
|
|
135
267
|
"Initialization failed after 5 retries",
|
|
136
268
|
{
|
|
@@ -177,7 +309,8 @@ export function drizzleStorage<
|
|
|
177
309
|
}
|
|
178
310
|
|
|
179
311
|
await withTransaction(db, async (tx) => {
|
|
180
|
-
|
|
312
|
+
// Use the appropriate idColumn for each table when calling invalidate
|
|
313
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
181
314
|
|
|
182
315
|
if (enablePersistence) {
|
|
183
316
|
await invalidateState({ tx, cursor, indexerId });
|
|
@@ -204,7 +337,7 @@ export function drizzleStorage<
|
|
|
204
337
|
});
|
|
205
338
|
|
|
206
339
|
indexer.hooks.hook("message:finalize", async ({ message }) => {
|
|
207
|
-
const { cursor } = message
|
|
340
|
+
const { cursor } = message;
|
|
208
341
|
|
|
209
342
|
if (!cursor) {
|
|
210
343
|
throw new DrizzleStorageError("Finalized Cursor is undefined");
|
|
@@ -220,14 +353,15 @@ export function drizzleStorage<
|
|
|
220
353
|
});
|
|
221
354
|
|
|
222
355
|
indexer.hooks.hook("message:invalidate", async ({ message }) => {
|
|
223
|
-
const { cursor } = message
|
|
356
|
+
const { cursor } = message;
|
|
224
357
|
|
|
225
358
|
if (!cursor) {
|
|
226
359
|
throw new DrizzleStorageError("Invalidate Cursor is undefined");
|
|
227
360
|
}
|
|
228
361
|
|
|
229
362
|
await withTransaction(db, async (tx) => {
|
|
230
|
-
|
|
363
|
+
// Use the appropriate idColumn for each table when calling invalidate
|
|
364
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
231
365
|
|
|
232
366
|
if (enablePersistence) {
|
|
233
367
|
await invalidateState({ tx, cursor, indexerId });
|
|
@@ -238,7 +372,8 @@ export function drizzleStorage<
|
|
|
238
372
|
indexer.hooks.hook("handler:middleware", async ({ use }) => {
|
|
239
373
|
use(async (context, next) => {
|
|
240
374
|
try {
|
|
241
|
-
const { endCursor, finality } = context as {
|
|
375
|
+
const { endCursor, finality, cursor } = context as {
|
|
376
|
+
cursor: Cursor;
|
|
242
377
|
endCursor: Cursor;
|
|
243
378
|
finality: DataFinality;
|
|
244
379
|
};
|
|
@@ -254,12 +389,17 @@ export function drizzleStorage<
|
|
|
254
389
|
TSchema
|
|
255
390
|
>;
|
|
256
391
|
|
|
392
|
+
if (prevFinality === "pending") {
|
|
393
|
+
// invalidate if previous block's finality was "pending"
|
|
394
|
+
await invalidate(tx, cursor, idColumnMap, indexerId);
|
|
395
|
+
}
|
|
396
|
+
|
|
257
397
|
if (finality !== "finalized") {
|
|
258
398
|
await registerTriggers(
|
|
259
399
|
tx,
|
|
260
400
|
tableNames,
|
|
261
401
|
endCursor,
|
|
262
|
-
|
|
402
|
+
idColumnMap,
|
|
263
403
|
indexerId,
|
|
264
404
|
);
|
|
265
405
|
}
|
|
@@ -267,13 +407,15 @@ export function drizzleStorage<
|
|
|
267
407
|
await next();
|
|
268
408
|
delete context[DRIZZLE_PROPERTY];
|
|
269
409
|
|
|
270
|
-
if (enablePersistence) {
|
|
410
|
+
if (enablePersistence && finality !== "pending") {
|
|
271
411
|
await persistState({
|
|
272
412
|
tx,
|
|
273
413
|
endCursor,
|
|
274
414
|
indexerId,
|
|
275
415
|
});
|
|
276
416
|
}
|
|
417
|
+
|
|
418
|
+
prevFinality = finality;
|
|
277
419
|
});
|
|
278
420
|
|
|
279
421
|
if (finality !== "finalized") {
|
package/src/persistence.ts
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
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 { integer,
|
|
8
|
+
import { integer, pgSchema, primaryKey, text } from "drizzle-orm/pg-core";
|
|
9
|
+
import { SCHEMA_NAME } from "./constants";
|
|
9
10
|
import { DrizzleStorageError, deserialize, serialize } from "./utils";
|
|
10
11
|
|
|
11
|
-
const CHECKPOINTS_TABLE_NAME = "
|
|
12
|
-
const FILTERS_TABLE_NAME = "
|
|
13
|
-
const SCHEMA_VERSION_TABLE_NAME = "
|
|
12
|
+
const CHECKPOINTS_TABLE_NAME = "checkpoints";
|
|
13
|
+
const FILTERS_TABLE_NAME = "filters";
|
|
14
|
+
const SCHEMA_VERSION_TABLE_NAME = "schema_version";
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
const schema = pgSchema(SCHEMA_NAME);
|
|
17
|
+
|
|
18
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
19
|
+
export const checkpoints = schema.table(CHECKPOINTS_TABLE_NAME, {
|
|
16
20
|
id: text("id").notNull().primaryKey(),
|
|
17
21
|
orderKey: integer("order_key").notNull(),
|
|
18
22
|
uniqueKey: text("unique_key"),
|
|
19
23
|
});
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
26
|
+
export const filters = schema.table(
|
|
22
27
|
FILTERS_TABLE_NAME,
|
|
23
28
|
{
|
|
24
29
|
id: text("id").notNull(),
|
|
@@ -33,7 +38,8 @@ export const filters = pgTable(
|
|
|
33
38
|
],
|
|
34
39
|
);
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
42
|
+
export const schemaVersion = schema.table(SCHEMA_VERSION_TABLE_NAME, {
|
|
37
43
|
k: integer("k").notNull().primaryKey(),
|
|
38
44
|
version: integer("version").notNull(),
|
|
39
45
|
});
|
|
@@ -53,13 +59,22 @@ export async function initializePersistentState<
|
|
|
53
59
|
TSchema extends
|
|
54
60
|
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
55
61
|
>(tx: PgTransaction<TQueryResult, TFullSchema, TSchema>) {
|
|
62
|
+
// Create schema if it doesn't exist
|
|
63
|
+
await tx.execute(
|
|
64
|
+
sql.raw(`
|
|
65
|
+
CREATE SCHEMA IF NOT EXISTS ${SCHEMA_NAME};
|
|
66
|
+
`),
|
|
67
|
+
);
|
|
68
|
+
|
|
56
69
|
// Create schema version table
|
|
57
|
-
await tx.execute(
|
|
58
|
-
|
|
70
|
+
await tx.execute(
|
|
71
|
+
sql.raw(`
|
|
72
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${SCHEMA_VERSION_TABLE_NAME} (
|
|
59
73
|
k INTEGER PRIMARY KEY,
|
|
60
74
|
version INTEGER NOT NULL
|
|
61
75
|
);
|
|
62
|
-
`)
|
|
76
|
+
`),
|
|
77
|
+
);
|
|
63
78
|
|
|
64
79
|
// Get current schema version
|
|
65
80
|
const versionRows = await tx
|
|
@@ -80,23 +95,27 @@ export async function initializePersistentState<
|
|
|
80
95
|
try {
|
|
81
96
|
if (storedVersion === -1) {
|
|
82
97
|
// First time initialization
|
|
83
|
-
await tx.execute(
|
|
84
|
-
|
|
98
|
+
await tx.execute(
|
|
99
|
+
sql.raw(`
|
|
100
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${CHECKPOINTS_TABLE_NAME} (
|
|
85
101
|
id TEXT PRIMARY KEY,
|
|
86
102
|
order_key INTEGER NOT NULL,
|
|
87
103
|
unique_key TEXT
|
|
88
104
|
);
|
|
89
|
-
`)
|
|
105
|
+
`),
|
|
106
|
+
);
|
|
90
107
|
|
|
91
|
-
await tx.execute(
|
|
92
|
-
|
|
108
|
+
await tx.execute(
|
|
109
|
+
sql.raw(`
|
|
110
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${FILTERS_TABLE_NAME} (
|
|
93
111
|
id TEXT NOT NULL,
|
|
94
112
|
filter TEXT NOT NULL,
|
|
95
113
|
from_block INTEGER NOT NULL,
|
|
96
114
|
to_block INTEGER DEFAULT NULL,
|
|
97
115
|
PRIMARY KEY (id, from_block)
|
|
98
116
|
);
|
|
99
|
-
`)
|
|
117
|
+
`),
|
|
118
|
+
);
|
|
100
119
|
|
|
101
120
|
// Set initial schema version
|
|
102
121
|
await tx.insert(schemaVersion).values({
|
|
@@ -155,7 +174,9 @@ export async function persistState<
|
|
|
155
174
|
target: checkpoints.id,
|
|
156
175
|
set: {
|
|
157
176
|
orderKey: Number(endCursor.orderKey),
|
|
158
|
-
|
|
177
|
+
// Explicitly set the unique key to `null` to indicate that it has been deleted
|
|
178
|
+
// Otherwise drizzle will not update its value.
|
|
179
|
+
uniqueKey: endCursor.uniqueKey ? endCursor.uniqueKey : null,
|
|
159
180
|
},
|
|
160
181
|
});
|
|
161
182
|
|
|
@@ -297,3 +318,24 @@ export async function finalizeState<
|
|
|
297
318
|
});
|
|
298
319
|
}
|
|
299
320
|
}
|
|
321
|
+
|
|
322
|
+
export async function resetPersistence<
|
|
323
|
+
TQueryResult extends PgQueryResultHKT,
|
|
324
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
325
|
+
TSchema extends
|
|
326
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
327
|
+
>(props: {
|
|
328
|
+
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>;
|
|
329
|
+
indexerId: string;
|
|
330
|
+
}) {
|
|
331
|
+
const { tx, indexerId } = props;
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
await tx.delete(checkpoints).where(eq(checkpoints.id, indexerId));
|
|
335
|
+
await tx.delete(filters).where(eq(filters.id, indexerId));
|
|
336
|
+
} catch (error) {
|
|
337
|
+
throw new DrizzleStorageError("Failed to reset persistence state", {
|
|
338
|
+
cause: error,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
package/src/storage.ts
CHANGED
|
@@ -11,11 +11,20 @@ import {
|
|
|
11
11
|
char,
|
|
12
12
|
integer,
|
|
13
13
|
jsonb,
|
|
14
|
-
|
|
14
|
+
pgSchema,
|
|
15
15
|
serial,
|
|
16
16
|
text,
|
|
17
17
|
} from "drizzle-orm/pg-core";
|
|
18
|
-
import {
|
|
18
|
+
import { SCHEMA_NAME } from "./constants";
|
|
19
|
+
import {
|
|
20
|
+
DrizzleStorageError,
|
|
21
|
+
type IdColumnMap,
|
|
22
|
+
getIdColumnForTable,
|
|
23
|
+
} from "./utils";
|
|
24
|
+
|
|
25
|
+
const ROLLBACK_TABLE_NAME = "reorg_rollback";
|
|
26
|
+
|
|
27
|
+
const schema = pgSchema(SCHEMA_NAME);
|
|
19
28
|
|
|
20
29
|
function getReorgTriggerName(table: string, indexerId: string) {
|
|
21
30
|
return `${table}_reorg_${indexerId}`;
|
|
@@ -23,7 +32,8 @@ function getReorgTriggerName(table: string, indexerId: string) {
|
|
|
23
32
|
|
|
24
33
|
export type ReorgOperation = "I" | "U" | "D";
|
|
25
34
|
|
|
26
|
-
|
|
35
|
+
/** This table is not used for migrations, its only used for ease of internal operations with drizzle. */
|
|
36
|
+
export const reorgRollbackTable = schema.table(ROLLBACK_TABLE_NAME, {
|
|
27
37
|
n: serial("n").primaryKey(),
|
|
28
38
|
op: char("op", { length: 1 }).$type<ReorgOperation>().notNull(),
|
|
29
39
|
table_name: text("table_name").notNull(),
|
|
@@ -42,10 +52,14 @@ export async function initializeReorgRollbackTable<
|
|
|
42
52
|
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
43
53
|
>(tx: PgTransaction<TQueryResult, TFullSchema, TSchema>, indexerId: string) {
|
|
44
54
|
try {
|
|
55
|
+
// Create schema if it doesn't exist
|
|
56
|
+
await tx.execute(`
|
|
57
|
+
CREATE SCHEMA IF NOT EXISTS ${SCHEMA_NAME};
|
|
58
|
+
`);
|
|
45
59
|
// Create the audit log table
|
|
46
60
|
await tx.execute(
|
|
47
61
|
sql.raw(`
|
|
48
|
-
CREATE TABLE IF NOT EXISTS
|
|
62
|
+
CREATE TABLE IF NOT EXISTS ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(
|
|
49
63
|
n SERIAL PRIMARY KEY,
|
|
50
64
|
op CHAR(1) NOT NULL,
|
|
51
65
|
table_name TEXT NOT NULL,
|
|
@@ -59,7 +73,7 @@ export async function initializeReorgRollbackTable<
|
|
|
59
73
|
|
|
60
74
|
await tx.execute(
|
|
61
75
|
sql.raw(`
|
|
62
|
-
CREATE INDEX IF NOT EXISTS idx_reorg_rollback_indexer_id_cursor ON
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_reorg_rollback_indexer_id_cursor ON ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(indexer_id, cursor);
|
|
63
77
|
`),
|
|
64
78
|
);
|
|
65
79
|
} catch (error) {
|
|
@@ -72,24 +86,25 @@ export async function initializeReorgRollbackTable<
|
|
|
72
86
|
// Create the trigger function
|
|
73
87
|
await tx.execute(
|
|
74
88
|
sql.raw(`
|
|
75
|
-
CREATE OR REPLACE FUNCTION reorg_checkpoint()
|
|
89
|
+
CREATE OR REPLACE FUNCTION ${SCHEMA_NAME}.reorg_checkpoint()
|
|
76
90
|
RETURNS TRIGGER AS $$
|
|
77
91
|
DECLARE
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
92
|
+
table_name TEXT := TG_ARGV[0]::TEXT;
|
|
93
|
+
id_col TEXT := TG_ARGV[1]::TEXT;
|
|
94
|
+
order_key INTEGER := TG_ARGV[2]::INTEGER;
|
|
95
|
+
indexer_id TEXT := TG_ARGV[3]::TEXT;
|
|
81
96
|
new_id_value TEXT := row_to_json(NEW.*)->>id_col;
|
|
82
97
|
old_id_value TEXT := row_to_json(OLD.*)->>id_col;
|
|
83
98
|
BEGIN
|
|
84
99
|
IF (TG_OP = 'DELETE') THEN
|
|
85
|
-
INSERT INTO
|
|
86
|
-
SELECT 'D',
|
|
100
|
+
INSERT INTO ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(op, table_name, cursor, row_id, row_value, indexer_id)
|
|
101
|
+
SELECT 'D', table_name, order_key, old_id_value, row_to_json(OLD.*), indexer_id;
|
|
87
102
|
ELSIF (TG_OP = 'UPDATE') THEN
|
|
88
|
-
INSERT INTO
|
|
89
|
-
SELECT 'U',
|
|
103
|
+
INSERT INTO ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(op, table_name, cursor, row_id, row_value, indexer_id)
|
|
104
|
+
SELECT 'U', table_name, order_key, new_id_value, row_to_json(OLD.*), indexer_id;
|
|
90
105
|
ELSIF (TG_OP = 'INSERT') THEN
|
|
91
|
-
INSERT INTO
|
|
92
|
-
SELECT 'I',
|
|
106
|
+
INSERT INTO ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(op, table_name, cursor, row_id, row_value, indexer_id)
|
|
107
|
+
SELECT 'I', table_name, order_key, new_id_value, null, indexer_id;
|
|
93
108
|
END IF;
|
|
94
109
|
RETURN NULL;
|
|
95
110
|
END;
|
|
@@ -115,11 +130,14 @@ export async function registerTriggers<
|
|
|
115
130
|
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
116
131
|
tables: string[],
|
|
117
132
|
endCursor: Cursor,
|
|
118
|
-
|
|
133
|
+
idColumnMap: IdColumnMap,
|
|
119
134
|
indexerId: string,
|
|
120
135
|
) {
|
|
121
136
|
try {
|
|
122
137
|
for (const table of tables) {
|
|
138
|
+
// Determine the column ID for this specific table
|
|
139
|
+
const tableIdColumn = getIdColumnForTable(table, idColumnMap);
|
|
140
|
+
|
|
123
141
|
await tx.execute(
|
|
124
142
|
sql.raw(
|
|
125
143
|
`DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`,
|
|
@@ -130,7 +148,7 @@ export async function registerTriggers<
|
|
|
130
148
|
CREATE CONSTRAINT TRIGGER ${getReorgTriggerName(table, indexerId)}
|
|
131
149
|
AFTER INSERT OR UPDATE OR DELETE ON ${table}
|
|
132
150
|
DEFERRABLE INITIALLY DEFERRED
|
|
133
|
-
FOR EACH ROW EXECUTE FUNCTION reorg_checkpoint('${
|
|
151
|
+
FOR EACH ROW EXECUTE FUNCTION ${SCHEMA_NAME}.reorg_checkpoint('${table}', '${tableIdColumn}', ${Number(endCursor.orderKey)}, '${indexerId}');
|
|
134
152
|
`),
|
|
135
153
|
);
|
|
136
154
|
}
|
|
@@ -174,14 +192,14 @@ export async function invalidate<
|
|
|
174
192
|
>(
|
|
175
193
|
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
176
194
|
cursor: Cursor,
|
|
177
|
-
|
|
195
|
+
idColumnMap: IdColumnMap,
|
|
178
196
|
indexerId: string,
|
|
179
197
|
) {
|
|
180
198
|
// Get and delete operations after cursor in one query, ordered by newest first
|
|
181
199
|
const { rows: result } = (await tx.execute(
|
|
182
200
|
sql.raw(`
|
|
183
201
|
WITH deleted AS (
|
|
184
|
-
DELETE FROM
|
|
202
|
+
DELETE FROM ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}
|
|
185
203
|
WHERE cursor > ${Number(cursor.orderKey)}
|
|
186
204
|
AND indexer_id = '${indexerId}'
|
|
187
205
|
RETURNING *
|
|
@@ -198,6 +216,9 @@ export async function invalidate<
|
|
|
198
216
|
|
|
199
217
|
// Process each operation in reverse order
|
|
200
218
|
for (const op of result) {
|
|
219
|
+
// Determine the column ID for this specific table
|
|
220
|
+
const tableIdColumn = getIdColumnForTable(op.table_name, idColumnMap);
|
|
221
|
+
|
|
201
222
|
switch (op.op) {
|
|
202
223
|
case "I":
|
|
203
224
|
try {
|
|
@@ -208,7 +229,7 @@ export async function invalidate<
|
|
|
208
229
|
await tx.execute(
|
|
209
230
|
sql.raw(`
|
|
210
231
|
DELETE FROM ${op.table_name}
|
|
211
|
-
WHERE ${
|
|
232
|
+
WHERE ${tableIdColumn} = '${op.row_id}'
|
|
212
233
|
`),
|
|
213
234
|
);
|
|
214
235
|
} catch (error) {
|
|
@@ -261,7 +282,9 @@ export async function invalidate<
|
|
|
261
282
|
? JSON.parse(op.row_value)
|
|
262
283
|
: op.row_value;
|
|
263
284
|
|
|
264
|
-
const nonIdKeys = Object.keys(rowValue).filter(
|
|
285
|
+
const nonIdKeys = Object.keys(rowValue).filter(
|
|
286
|
+
(k) => k !== tableIdColumn,
|
|
287
|
+
);
|
|
265
288
|
|
|
266
289
|
const fields = nonIdKeys.map((c) => `${c} = prev.${c}`).join(", ");
|
|
267
290
|
|
|
@@ -271,7 +294,7 @@ export async function invalidate<
|
|
|
271
294
|
FROM (
|
|
272
295
|
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
273
296
|
) as prev
|
|
274
|
-
WHERE ${op.table_name}.${
|
|
297
|
+
WHERE ${op.table_name}.${tableIdColumn} = '${op.row_id}'
|
|
275
298
|
`);
|
|
276
299
|
|
|
277
300
|
await tx.execute(query);
|
|
@@ -305,7 +328,7 @@ export async function finalize<
|
|
|
305
328
|
try {
|
|
306
329
|
await tx.execute(
|
|
307
330
|
sql.raw(`
|
|
308
|
-
DELETE FROM
|
|
331
|
+
DELETE FROM ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}
|
|
309
332
|
WHERE cursor <= ${Number(cursor.orderKey)}
|
|
310
333
|
AND indexer_id = '${indexerId}'
|
|
311
334
|
`),
|
|
@@ -316,3 +339,45 @@ export async function finalize<
|
|
|
316
339
|
});
|
|
317
340
|
}
|
|
318
341
|
}
|
|
342
|
+
|
|
343
|
+
export async function cleanupStorage<
|
|
344
|
+
TQueryResult extends PgQueryResultHKT,
|
|
345
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
346
|
+
TSchema extends
|
|
347
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
348
|
+
>(
|
|
349
|
+
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
350
|
+
tables: string[],
|
|
351
|
+
indexerId: string,
|
|
352
|
+
) {
|
|
353
|
+
try {
|
|
354
|
+
for (const table of tables) {
|
|
355
|
+
await tx.execute(
|
|
356
|
+
sql.raw(
|
|
357
|
+
`DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`,
|
|
358
|
+
),
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
await tx.execute(
|
|
363
|
+
sql.raw(`
|
|
364
|
+
DELETE FROM ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}
|
|
365
|
+
WHERE indexer_id = '${indexerId}'
|
|
366
|
+
`),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
for (const table of tables) {
|
|
370
|
+
try {
|
|
371
|
+
await tx.execute(sql.raw(`TRUNCATE TABLE ${table} CASCADE;`));
|
|
372
|
+
} catch (error) {
|
|
373
|
+
throw new DrizzleStorageError(`Failed to truncate table ${table}`, {
|
|
374
|
+
cause: error,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} catch (error) {
|
|
379
|
+
throw new DrizzleStorageError("Failed to clean up storage", {
|
|
380
|
+
cause: error,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|