@apibara/plugin-drizzle 2.1.0-beta.3 → 2.1.0-beta.30
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 +250 -64
- 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 +239 -55
- 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 +21 -6
- package/src/constants.ts +3 -0
- package/src/helper.ts +219 -0
- package/src/index.ts +142 -17
- 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/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
|
+
}
|
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
|
+
};
|