@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/src/storage.ts CHANGED
@@ -11,11 +11,20 @@ import {
11
11
  char,
12
12
  integer,
13
13
  jsonb,
14
- pgTable,
14
+ pgSchema,
15
15
  serial,
16
16
  text,
17
17
  } from "drizzle-orm/pg-core";
18
- import { DrizzleStorageError } from "./utils";
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
- export const reorgRollbackTable = pgTable("__reorg_rollback", {
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 __reorg_rollback(
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 __reorg_rollback(indexer_id, cursor);
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 __reorg_rollback(op, table_name, cursor, row_id, row_value, indexer_id)
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 __reorg_rollback(op, table_name, cursor, row_id, row_value, indexer_id)
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 __reorg_rollback(op, table_name, cursor, row_id, row_value, indexer_id)
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
- idColumn: string,
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('${idColumn}', ${`${Number(endCursor.orderKey)}`}, '${indexerId}');
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
- idColumn: string,
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 __reorg_rollback
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 ${idColumn} = '${op.row_id}'
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((k) => k !== idColumn);
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}.${idColumn} = '${op.row_id}'
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 __reorg_rollback
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
+ };