@apibara/plugin-drizzle 2.1.0-beta.2 → 2.1.0-beta.21
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 +231 -55
- package/dist/index.d.cts +165 -4
- package/dist/index.d.mts +165 -4
- package/dist/index.d.ts +165 -4
- package/dist/index.mjs +217 -47
- package/dist/shared/plugin-drizzle.2d226351.mjs +5 -0
- package/dist/shared/plugin-drizzle.cae20704.cjs +9 -0
- package/dist/testing.cjs +13 -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 +11 -0
- package/package.json +12 -6
- package/src/constants.ts +3 -0
- package/src/helper.ts +202 -0
- package/src/index.ts +140 -15
- package/src/persistence.ts +60 -18
- package/src/storage.ts +81 -17
- package/src/testing.ts +13 -0
- package/src/utils.ts +19 -0
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,7 +86,7 @@ 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
92
|
id_col TEXT := TG_ARGV[0]::TEXT;
|
|
@@ -82,13 +96,13 @@ export async function initializeReorgRollbackTable<
|
|
|
82
96
|
old_id_value TEXT := row_to_json(OLD.*)->>id_col;
|
|
83
97
|
BEGIN
|
|
84
98
|
IF (TG_OP = 'DELETE') THEN
|
|
85
|
-
INSERT INTO
|
|
99
|
+
INSERT INTO ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(op, table_name, cursor, row_id, row_value, indexer_id)
|
|
86
100
|
SELECT 'D', TG_TABLE_NAME, order_key, old_id_value, row_to_json(OLD.*), indexer_id;
|
|
87
101
|
ELSIF (TG_OP = 'UPDATE') THEN
|
|
88
|
-
INSERT INTO
|
|
102
|
+
INSERT INTO ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(op, table_name, cursor, row_id, row_value, indexer_id)
|
|
89
103
|
SELECT 'U', TG_TABLE_NAME, order_key, new_id_value, row_to_json(OLD.*), indexer_id;
|
|
90
104
|
ELSIF (TG_OP = 'INSERT') THEN
|
|
91
|
-
INSERT INTO
|
|
105
|
+
INSERT INTO ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}(op, table_name, cursor, row_id, row_value, indexer_id)
|
|
92
106
|
SELECT 'I', TG_TABLE_NAME, order_key, new_id_value, null, indexer_id;
|
|
93
107
|
END IF;
|
|
94
108
|
RETURN NULL;
|
|
@@ -115,11 +129,14 @@ export async function registerTriggers<
|
|
|
115
129
|
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
116
130
|
tables: string[],
|
|
117
131
|
endCursor: Cursor,
|
|
118
|
-
|
|
132
|
+
idColumnMap: IdColumnMap,
|
|
119
133
|
indexerId: string,
|
|
120
134
|
) {
|
|
121
135
|
try {
|
|
122
136
|
for (const table of tables) {
|
|
137
|
+
// Determine the column ID for this specific table
|
|
138
|
+
const tableIdColumn = getIdColumnForTable(table, idColumnMap);
|
|
139
|
+
|
|
123
140
|
await tx.execute(
|
|
124
141
|
sql.raw(
|
|
125
142
|
`DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`,
|
|
@@ -130,7 +147,7 @@ export async function registerTriggers<
|
|
|
130
147
|
CREATE CONSTRAINT TRIGGER ${getReorgTriggerName(table, indexerId)}
|
|
131
148
|
AFTER INSERT OR UPDATE OR DELETE ON ${table}
|
|
132
149
|
DEFERRABLE INITIALLY DEFERRED
|
|
133
|
-
FOR EACH ROW EXECUTE FUNCTION reorg_checkpoint('${
|
|
150
|
+
FOR EACH ROW EXECUTE FUNCTION ${SCHEMA_NAME}.reorg_checkpoint('${tableIdColumn}', ${Number(endCursor.orderKey)}, '${indexerId}');
|
|
134
151
|
`),
|
|
135
152
|
);
|
|
136
153
|
}
|
|
@@ -174,14 +191,14 @@ export async function invalidate<
|
|
|
174
191
|
>(
|
|
175
192
|
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
176
193
|
cursor: Cursor,
|
|
177
|
-
|
|
194
|
+
idColumnMap: IdColumnMap,
|
|
178
195
|
indexerId: string,
|
|
179
196
|
) {
|
|
180
197
|
// Get and delete operations after cursor in one query, ordered by newest first
|
|
181
198
|
const { rows: result } = (await tx.execute(
|
|
182
199
|
sql.raw(`
|
|
183
200
|
WITH deleted AS (
|
|
184
|
-
DELETE FROM
|
|
201
|
+
DELETE FROM ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}
|
|
185
202
|
WHERE cursor > ${Number(cursor.orderKey)}
|
|
186
203
|
AND indexer_id = '${indexerId}'
|
|
187
204
|
RETURNING *
|
|
@@ -198,6 +215,9 @@ export async function invalidate<
|
|
|
198
215
|
|
|
199
216
|
// Process each operation in reverse order
|
|
200
217
|
for (const op of result) {
|
|
218
|
+
// Determine the column ID for this specific table
|
|
219
|
+
const tableIdColumn = getIdColumnForTable(op.table_name, idColumnMap);
|
|
220
|
+
|
|
201
221
|
switch (op.op) {
|
|
202
222
|
case "I":
|
|
203
223
|
try {
|
|
@@ -208,7 +228,7 @@ export async function invalidate<
|
|
|
208
228
|
await tx.execute(
|
|
209
229
|
sql.raw(`
|
|
210
230
|
DELETE FROM ${op.table_name}
|
|
211
|
-
WHERE ${
|
|
231
|
+
WHERE ${tableIdColumn} = '${op.row_id}'
|
|
212
232
|
`),
|
|
213
233
|
);
|
|
214
234
|
} catch (error) {
|
|
@@ -261,7 +281,9 @@ export async function invalidate<
|
|
|
261
281
|
? JSON.parse(op.row_value)
|
|
262
282
|
: op.row_value;
|
|
263
283
|
|
|
264
|
-
const nonIdKeys = Object.keys(rowValue).filter(
|
|
284
|
+
const nonIdKeys = Object.keys(rowValue).filter(
|
|
285
|
+
(k) => k !== tableIdColumn,
|
|
286
|
+
);
|
|
265
287
|
|
|
266
288
|
const fields = nonIdKeys.map((c) => `${c} = prev.${c}`).join(", ");
|
|
267
289
|
|
|
@@ -271,7 +293,7 @@ export async function invalidate<
|
|
|
271
293
|
FROM (
|
|
272
294
|
SELECT * FROM json_populate_record(null::${op.table_name}, '${JSON.stringify(op.row_value)}'::json)
|
|
273
295
|
) as prev
|
|
274
|
-
WHERE ${op.table_name}.${
|
|
296
|
+
WHERE ${op.table_name}.${tableIdColumn} = '${op.row_id}'
|
|
275
297
|
`);
|
|
276
298
|
|
|
277
299
|
await tx.execute(query);
|
|
@@ -305,7 +327,7 @@ export async function finalize<
|
|
|
305
327
|
try {
|
|
306
328
|
await tx.execute(
|
|
307
329
|
sql.raw(`
|
|
308
|
-
DELETE FROM
|
|
330
|
+
DELETE FROM ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}
|
|
309
331
|
WHERE cursor <= ${Number(cursor.orderKey)}
|
|
310
332
|
AND indexer_id = '${indexerId}'
|
|
311
333
|
`),
|
|
@@ -316,3 +338,45 @@ export async function finalize<
|
|
|
316
338
|
});
|
|
317
339
|
}
|
|
318
340
|
}
|
|
341
|
+
|
|
342
|
+
export async function cleanupStorage<
|
|
343
|
+
TQueryResult extends PgQueryResultHKT,
|
|
344
|
+
TFullSchema extends Record<string, unknown> = Record<string, never>,
|
|
345
|
+
TSchema extends
|
|
346
|
+
TablesRelationalConfig = ExtractTablesWithRelations<TFullSchema>,
|
|
347
|
+
>(
|
|
348
|
+
tx: PgTransaction<TQueryResult, TFullSchema, TSchema>,
|
|
349
|
+
tables: string[],
|
|
350
|
+
indexerId: string,
|
|
351
|
+
) {
|
|
352
|
+
try {
|
|
353
|
+
for (const table of tables) {
|
|
354
|
+
await tx.execute(
|
|
355
|
+
sql.raw(
|
|
356
|
+
`DROP TRIGGER IF EXISTS ${getReorgTriggerName(table, indexerId)} ON ${table};`,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
await tx.execute(
|
|
362
|
+
sql.raw(`
|
|
363
|
+
DELETE FROM ${SCHEMA_NAME}.${ROLLBACK_TABLE_NAME}
|
|
364
|
+
WHERE indexer_id = '${indexerId}'
|
|
365
|
+
`),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
for (const table of tables) {
|
|
369
|
+
try {
|
|
370
|
+
await tx.execute(sql.raw(`TRUNCATE TABLE ${table} CASCADE;`));
|
|
371
|
+
} catch (error) {
|
|
372
|
+
throw new DrizzleStorageError(`Failed to truncate table ${table}`, {
|
|
373
|
+
cause: error,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
} catch (error) {
|
|
378
|
+
throw new DrizzleStorageError("Failed to clean up storage", {
|
|
379
|
+
cause: error,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { VcrResult } from "@apibara/indexer/testing";
|
|
2
|
+
import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
|
|
3
|
+
import { DRIZZLE_STORAGE_DB_PROPERTY } from "./constants";
|
|
4
|
+
|
|
5
|
+
export function getTestDatabase(context: VcrResult) {
|
|
6
|
+
const db = context[DRIZZLE_STORAGE_DB_PROPERTY];
|
|
7
|
+
|
|
8
|
+
if (!db) {
|
|
9
|
+
throw new Error("Drizzle database not found in context");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return db as PgDatabase<PgQueryResultHKT>;
|
|
13
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -48,3 +48,22 @@ export function serialize<T>(obj: T): string {
|
|
|
48
48
|
export function sleep(ms: number) {
|
|
49
49
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
50
50
|
}
|
|
51
|
+
|
|
52
|
+
export interface IdColumnMap extends Record<string, string> {
|
|
53
|
+
/**
|
|
54
|
+
* Wildcard mapping for all tables.
|
|
55
|
+
*/
|
|
56
|
+
"*": string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const getIdColumnForTable = (
|
|
60
|
+
tableName: string,
|
|
61
|
+
idColumn: IdColumnMap,
|
|
62
|
+
): string => {
|
|
63
|
+
// If there's a specific mapping for this table, use it
|
|
64
|
+
if (idColumn[tableName]) {
|
|
65
|
+
return idColumn[tableName];
|
|
66
|
+
}
|
|
67
|
+
// Default fallback
|
|
68
|
+
return idColumn["*"];
|
|
69
|
+
};
|