Skip to content

Commit 0d4564d

Browse files
authored
Merge pull request #277 from hack-a-chain-software/blocks-from-depth-query-fix
minimum depth argument fixes; added missing index migrations; improved missing blocks speed
2 parents eb0ca70 + f5ece8b commit 0d4564d

32 files changed

+3859
-2485
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict';
2+
3+
/** @type {import('sequelize-cli').Migration} */
4+
module.exports = {
5+
async up(queryInterface) {
6+
await queryInterface.addIndex('TransactionDetails', {
7+
fields: ['transactionId'],
8+
name: 'transactiondetails_transactionid_idx',
9+
});
10+
11+
await queryInterface.addIndex('Transactions', {
12+
fields: ['creationtime'],
13+
name: 'transactions_creationtime_idx',
14+
});
15+
},
16+
17+
async down(queryInterface) {
18+
await queryInterface.removeIndex('TransactionDetails', 'transactiondetails_transactionid_idx');
19+
await queryInterface.removeIndex('Transactions', 'transactions_creationtime_idx');
20+
},
21+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
3+
/** @type {import('sequelize-cli').Migration} */
4+
module.exports = {
5+
async up(queryInterface) {
6+
// Remove the indexes
7+
await queryInterface.removeIndex('Transactions', 'transactions_canonical_idx');
8+
await queryInterface.removeIndex('Transactions', 'transactions_hash_idx');
9+
await queryInterface.removeIndex('Transactions', 'transactions_trgm_txid_idx');
10+
await queryInterface.removeIndex('Transactions', 'transactions_trgm_hash_idx');
11+
await queryInterface.removeIndex('Transactions', 'transactions_trgm_requestkey_idx');
12+
await queryInterface.removeIndex('Transactions', 'transactions_trgm_sender_idx');
13+
},
14+
15+
async down(queryInterface) {
16+
await queryInterface.addIndex('Transactions', {
17+
name: 'transactions_canonical_idx',
18+
fields: ['canonical'],
19+
});
20+
21+
await queryInterface.addIndex('Transactions', {
22+
name: 'transactions_hash_idx',
23+
fields: ['hash'],
24+
});
25+
26+
await queryInterface.addIndex('Transactions', {
27+
name: 'transactions_trgm_txid_idx',
28+
fields: [sequelize.fn('LOWER', sequelize.col('txid'))],
29+
using: 'gin',
30+
operator: 'gin_trgm_ops',
31+
});
32+
33+
await queryInterface.addIndex('Transactions', {
34+
name: 'transactions_trgm_hash_idx',
35+
fields: [sequelize.fn('LOWER', sequelize.col('hash'))],
36+
using: 'gin',
37+
operator: 'gin_trgm_ops',
38+
});
39+
40+
await queryInterface.addIndex('Transactions', {
41+
name: 'transactions_trgm_requestkey_idx',
42+
fields: [sequelize.fn('LOWER', sequelize.col('requestkey'))],
43+
using: 'gin',
44+
operator: 'gin_trgm_ops',
45+
});
46+
47+
await queryInterface.addIndex('Transactions', {
48+
name: 'transactions_trgm_sender_idx',
49+
fields: [sequelize.fn('LOWER', sequelize.col('sender'))],
50+
using: 'gin',
51+
operator: 'gin_trgm_ops',
52+
});
53+
},
54+
};

indexer/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@
7474
"graphql:generate-types": "npx graphql-codegen",
7575
"migrate:up": "dotenv -e .env npx sequelize-cli db:migrate",
7676
"migrate:down": "dotenv -e .env npx sequelize-cli db:migrate:undo",
77-
"test:unit": "jest tests/unit/*.test.ts",
77+
"test:unit": "jest tests/unit",
7878
"test:queries": "dotenv -e .env.testing jest tests/integration/queries/*.test.ts",
7979
"test:subscriptions": "dotenv -e .env.testing jest tests/integration/subscriptions/*.test.ts",
8080
"test:file": "dotenv -e .env.testing jest",
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/**
2+
* Specialized class for building SQL queries related to blockchain events
3+
*
4+
* This class encapsulates the complex logic for constructing SQL queries
5+
* to retrieve events from the database with various filtering criteria.
6+
*/
7+
export default class EventQueryBuilder {
8+
private readonly HEIGHT_BATCH_SIZE = 200;
9+
10+
/**
11+
* Calculates the height range for block filtering based on min/max height parameters
12+
*
13+
* @param minHeight - Minimum block height to include (optional)
14+
* @param maxHeight - Maximum block height to include (optional)
15+
* @returns Object containing fromHeight and toHeight values
16+
*/
17+
private calculateHeightRange(minHeight?: number | null, maxHeight?: number | null) {
18+
let fromHeight = 0;
19+
let toHeight = 0;
20+
21+
if (minHeight && maxHeight) {
22+
fromHeight = minHeight;
23+
toHeight = maxHeight - minHeight > 100 ? minHeight + this.HEIGHT_BATCH_SIZE : maxHeight;
24+
} else if (minHeight) {
25+
fromHeight = minHeight;
26+
toHeight = minHeight + this.HEIGHT_BATCH_SIZE;
27+
} else if (maxHeight) {
28+
fromHeight = maxHeight - this.HEIGHT_BATCH_SIZE;
29+
toHeight = maxHeight;
30+
}
31+
32+
const isHeightFiltered = Boolean(fromHeight || toHeight);
33+
return { fromHeight, toHeight, isHeightFiltered };
34+
}
35+
36+
/**
37+
* Builds the SQL query for fetching events with various filtering options
38+
*
39+
* @param params - Object containing parameters needed to build the query
40+
* @returns Object containing the query string and parameters array
41+
*/
42+
private buildEventQuery(params: {
43+
module: string;
44+
name: string;
45+
limit: number;
46+
order: string;
47+
after: string | null;
48+
before: string | null;
49+
blockHash?: string | null;
50+
chainId?: string | null;
51+
fromHeight: number;
52+
toHeight: number;
53+
requestKey?: string | null;
54+
isHeightChainOrBlockHash: boolean;
55+
}) {
56+
const {
57+
module,
58+
name,
59+
limit,
60+
order,
61+
after,
62+
before,
63+
blockHash,
64+
chainId,
65+
fromHeight,
66+
toHeight,
67+
requestKey,
68+
isHeightChainOrBlockHash,
69+
} = params;
70+
71+
const queryParams: (string | number)[] = [limit, module, name];
72+
const blockQueryParams: (string | number)[] = [];
73+
let conditions = '';
74+
let eventConditions = '';
75+
76+
// Process pagination parameters - keep their indices consistent for all query types
77+
if (after) {
78+
queryParams.push(after);
79+
}
80+
81+
if (before) {
82+
queryParams.push(before);
83+
}
84+
85+
// Add pagination conditions (indices need to be right)
86+
let idx = 3; // Starting after [limit, module, name]
87+
88+
if (after) {
89+
idx++; // Increment to account for the 'after' parameter
90+
eventConditions += `\nAND e.id < $${idx}`;
91+
}
92+
93+
if (before) {
94+
idx++; // Increment to account for the 'before' parameter
95+
eventConditions += `\nAND e.id > $${idx}`;
96+
}
97+
98+
// Initialize a flag to track if we've added any conditions
99+
let hasAddedBlockCondition = false;
100+
101+
if (blockHash) {
102+
blockQueryParams.push(blockHash);
103+
conditions += `WHERE b.hash = $${blockQueryParams.length + queryParams.length}`;
104+
hasAddedBlockCondition = true;
105+
}
106+
107+
if (chainId) {
108+
blockQueryParams.push(chainId);
109+
if (hasAddedBlockCondition) {
110+
conditions += `\nAND b."chainId" = $${blockQueryParams.length + queryParams.length}`;
111+
} else {
112+
conditions += `WHERE b."chainId" = $${blockQueryParams.length + queryParams.length}`;
113+
hasAddedBlockCondition = true;
114+
}
115+
}
116+
117+
if (fromHeight && toHeight) {
118+
blockQueryParams.push(fromHeight);
119+
if (hasAddedBlockCondition) {
120+
conditions += `\nAND b."height" >= $${blockQueryParams.length + queryParams.length}`;
121+
} else {
122+
conditions += `WHERE b."height" >= $${blockQueryParams.length + queryParams.length}`;
123+
hasAddedBlockCondition = true;
124+
}
125+
blockQueryParams.push(toHeight);
126+
conditions += `\nAND b."height" <= $${blockQueryParams.length + queryParams.length}`;
127+
}
128+
129+
let query = '';
130+
if (isHeightChainOrBlockHash) {
131+
query = `
132+
WITH block_filtered AS (
133+
select *
134+
from "Blocks" b
135+
${conditions}
136+
)
137+
SELECT
138+
e.id as id,
139+
e.requestkey as "requestKey",
140+
e."chainId" as "chainId",
141+
b.height as height,
142+
e."orderIndex" as "orderIndex",
143+
e.module as "moduleName",
144+
e.name as name,
145+
e.params as parameters,
146+
b.hash as "blockHash"
147+
FROM block_filtered b
148+
join "Transactions" t ON t."blockId" = b.id
149+
join "Events" e ON e."transactionId" = t.id
150+
WHERE e.module = $2
151+
AND e.name = $3
152+
${eventConditions}
153+
ORDER BY b.height ${order}
154+
LIMIT $1
155+
`;
156+
} else if (requestKey) {
157+
queryParams.push(requestKey);
158+
query = `
159+
WITH event_transaction_filtered AS (
160+
SELECT e.*, t."blockId"
161+
FROM "Transactions" t
162+
JOIN "Events" e ON t.id = e."transactionId"
163+
WHERE e.module = $2
164+
AND e.name = $3
165+
AND t.requestkey = $${blockQueryParams.length + queryParams.length}
166+
${eventConditions}
167+
ORDER BY e.id ${order}
168+
)
169+
SELECT
170+
et.id as id,
171+
et.requestkey as "requestKey",
172+
et."chainId" as "chainId",
173+
b.height as height,
174+
et."orderIndex" as "orderIndex",
175+
et.module as "moduleName",
176+
et.name as name,
177+
et.params as parameters,
178+
b.hash as "blockHash"
179+
FROM event_transaction_filtered et
180+
JOIN "Blocks" b ON b.id = et."blockId"
181+
${conditions}
182+
LIMIT $1
183+
`;
184+
} else {
185+
query = `
186+
WITH event_filtered AS (
187+
select *
188+
from "Events" e
189+
WHERE e.module = $2
190+
AND e.name = $3
191+
${eventConditions}
192+
ORDER BY e.id ${order}
193+
)
194+
SELECT
195+
e.id as id,
196+
e.requestkey as "requestKey",
197+
e."chainId" as "chainId",
198+
b.height as height,
199+
e."orderIndex" as "orderIndex",
200+
e.module as "moduleName",
201+
e.name as name,
202+
e.params as parameters,
203+
b.hash as "blockHash"
204+
FROM event_filtered e
205+
join "Transactions" t ON t.id = e."transactionId"
206+
join "Blocks" b ON b.id = t."blockId"
207+
${conditions}
208+
LIMIT $1
209+
`;
210+
}
211+
212+
return { query, queryParams: [...queryParams, ...blockQueryParams] };
213+
}
214+
215+
/**
216+
* Builds a complete query for events with qualified name, handling all filtering parameters
217+
*
218+
* @param params - Object containing all query parameters and filtering options
219+
* @returns Object containing the query string and parameters array
220+
*/
221+
buildEventsWithQualifiedNameQuery(params: {
222+
qualifiedEventName: string;
223+
limit: number;
224+
order: string;
225+
after: string | null;
226+
before: string | null;
227+
blockHash?: string | null;
228+
chainId?: string | null;
229+
minHeight?: number | null;
230+
maxHeight?: number | null;
231+
requestKey?: string | null;
232+
}) {
233+
const {
234+
qualifiedEventName,
235+
limit,
236+
order,
237+
after,
238+
before,
239+
blockHash,
240+
chainId,
241+
minHeight,
242+
maxHeight,
243+
requestKey,
244+
} = params;
245+
246+
const splitted = qualifiedEventName.split('.');
247+
const name = splitted.pop() ?? '';
248+
const module = splitted.join('.');
249+
250+
const { fromHeight, toHeight, isHeightFiltered } = this.calculateHeightRange(
251+
minHeight,
252+
maxHeight,
253+
);
254+
const isHeightChainOrBlockHash = isHeightFiltered || Boolean(blockHash || chainId);
255+
256+
return this.buildEventQuery({
257+
module,
258+
name,
259+
limit,
260+
order,
261+
after,
262+
before,
263+
blockHash,
264+
chainId,
265+
fromHeight,
266+
toHeight,
267+
requestKey,
268+
isHeightChainOrBlockHash,
269+
});
270+
}
271+
}

0 commit comments

Comments
 (0)