@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/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,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
- id_col TEXT := TG_ARGV[0]::TEXT;
79
- order_key INTEGER := TG_ARGV[1]::INTEGER;
80
- indexer_id TEXT := TG_ARGV[2]::TEXT;
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 __reorg_rollback(op, table_name, cursor, row_id, row_value, indexer_id)
86
- SELECT 'D', TG_TABLE_NAME, order_key, old_id_value, row_to_json(OLD.*), indexer_id;
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 __reorg_rollback(op, table_name, cursor, row_id, row_value, indexer_id)
89
- SELECT 'U', TG_TABLE_NAME, order_key, new_id_value, row_to_json(OLD.*), indexer_id;
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 __reorg_rollback(op, table_name, cursor, row_id, row_value, indexer_id)
92
- SELECT 'I', TG_TABLE_NAME, order_key, new_id_value, null, indexer_id;
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
- idColumn: string,
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('${idColumn}', ${`${Number(endCursor.orderKey)}`}, '${indexerId}');
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
- idColumn: string,
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 __reorg_rollback
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 ${idColumn} = '${op.row_id}'
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((k) => k !== idColumn);
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}.${idColumn} = '${op.row_id}'
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 __reorg_rollback
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
+ };