@ocap/indexdb-sqlite 1.29.9 → 1.29.11
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/esm/db/base.d.mts +3 -1
- package/esm/db/base.mjs +125 -47
- package/esm/db/index.mjs +2 -1
- package/esm/interfaces.d.mts +12 -1
- package/esm/migrations/002-composite-indexes.d.mts +12 -0
- package/esm/migrations/002-composite-indexes.mjs +73 -0
- package/esm/migrations/index.mjs +5 -1
- package/esm/table/account.d.mts +31 -0
- package/esm/table/account.mjs +58 -0
- package/esm/table/transaction.d.mts +7 -1
- package/esm/table/transaction.mjs +31 -2
- package/lib/db/base.cjs +125 -47
- package/lib/db/base.d.cts +3 -1
- package/lib/db/index.cjs +2 -1
- package/lib/interfaces.d.cts +12 -1
- package/lib/migrations/002-composite-indexes.cjs +80 -0
- package/lib/migrations/002-composite-indexes.d.cts +12 -0
- package/lib/migrations/index.cjs +5 -1
- package/lib/table/account.cjs +60 -0
- package/lib/table/account.d.cts +31 -0
- package/lib/table/transaction.cjs +31 -2
- package/lib/table/transaction.d.cts +7 -1
- package/package.json +6 -6
package/esm/db/base.d.mts
CHANGED
|
@@ -36,7 +36,9 @@ declare class SqliteBaseIndexDB extends BaseIndexDB implements IIndexDB {
|
|
|
36
36
|
*/
|
|
37
37
|
listAssets(params?: Partial<TRequestListAssets>): Promise<IListAssetsResult>;
|
|
38
38
|
/**
|
|
39
|
-
* List top accounts by token balance
|
|
39
|
+
* List top accounts by token balance.
|
|
40
|
+
* For tokenAddress filter: uses balance table JOIN for indexed lookup.
|
|
41
|
+
* Without tokenAddress: uses SQL ORDER BY + LIMIT directly.
|
|
40
42
|
*/
|
|
41
43
|
listTopAccounts(params?: Partial<TRequestListTopAccounts>): Promise<IListAccountsResult>;
|
|
42
44
|
/**
|
package/esm/db/base.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { sql } from "kysely";
|
|
1
2
|
import { BaseIndexDB, formatDelegationAfterRead, formatNextPagination, formatPagination, parseDateTime } from "@ocap/indexdb";
|
|
2
3
|
import { toChecksumAddress } from "@arcblock/did/lib/type";
|
|
3
|
-
import { BN } from "@ocap/util";
|
|
4
4
|
import { DEFAULT_TOKEN_DECIMAL } from "@ocap/util/lib/constant";
|
|
5
5
|
import debugFactory from "debug";
|
|
6
6
|
import omit from "lodash/omit.js";
|
|
@@ -55,8 +55,9 @@ var SqliteBaseIndexDB = class extends BaseIndexDB {
|
|
|
55
55
|
includeItxData
|
|
56
56
|
});
|
|
57
57
|
let qb = this.db.selectFrom("tx").selectAll();
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
const isMutual = !!(sender && receiver && direction === "MUTUAL");
|
|
59
|
+
const isUnion = !!(sender && receiver && direction !== "MUTUAL" && direction !== "ONE_WAY");
|
|
60
|
+
if (sender && receiver) if (direction === "MUTUAL") {} else if (direction === "ONE_WAY") qb = qb.where("sender", "=", sender).where("receiver", "=", receiver);
|
|
60
61
|
else qb = qb.where((eb) => eb.or([eb("sender", "=", sender), eb("receiver", "=", receiver)]));
|
|
61
62
|
else if (sender) qb = qb.where("sender", "=", sender);
|
|
62
63
|
else if (receiver) qb = qb.where("receiver", "=", receiver);
|
|
@@ -71,18 +72,98 @@ var SqliteBaseIndexDB = class extends BaseIndexDB {
|
|
|
71
72
|
const { validity } = validityFilter;
|
|
72
73
|
if (validity === "VALID") qb = qb.where("valid", "=", 1);
|
|
73
74
|
else if (validity === "INVALID") qb = qb.where("valid", "=", 0);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
75
|
+
const txRefTypeMap = {
|
|
76
|
+
factories: "factory",
|
|
77
|
+
tokens: "token",
|
|
78
|
+
assets: "asset",
|
|
79
|
+
accounts: "account",
|
|
80
|
+
rollups: "rollup",
|
|
81
|
+
stakes: "stake",
|
|
82
|
+
delegations: "delegation",
|
|
83
|
+
tokenFactories: "tokenFactory"
|
|
84
|
+
};
|
|
85
|
+
const applyJsonArrayFilter = (column, values) => {
|
|
86
|
+
const refType = txRefTypeMap[column];
|
|
87
|
+
qb = qb.where("hash", "in", this.db.selectFrom("tx_ref").select("hash").where("type", "=", refType).where("value", "in", values));
|
|
88
|
+
};
|
|
89
|
+
if (factories.length) applyJsonArrayFilter("factories", factories);
|
|
90
|
+
if (assets.length) applyJsonArrayFilter("assets", assets);
|
|
91
|
+
if (tokens.length) applyJsonArrayFilter("tokens", tokens);
|
|
92
|
+
if (accounts.length) applyJsonArrayFilter("accounts", accounts);
|
|
93
|
+
if (rollups.length) applyJsonArrayFilter("rollups", rollups);
|
|
94
|
+
if (stakes.length) applyJsonArrayFilter("stakes", stakes);
|
|
95
|
+
if (delegations.length) applyJsonArrayFilter("delegations", delegations);
|
|
96
|
+
if (tokenFactories.length) applyJsonArrayFilter("tokenFactories", tokenFactories);
|
|
97
|
+
let total;
|
|
98
|
+
const activeJsonFilters = [];
|
|
99
|
+
if (factories.length) activeJsonFilters.push(["factory", factories]);
|
|
100
|
+
if (assets.length) activeJsonFilters.push(["asset", assets]);
|
|
101
|
+
if (tokens.length) activeJsonFilters.push(["token", tokens]);
|
|
102
|
+
if (accounts.length) activeJsonFilters.push(["account", accounts]);
|
|
103
|
+
if (rollups.length) activeJsonFilters.push(["rollup", rollups]);
|
|
104
|
+
if (stakes.length) activeJsonFilters.push(["stake", stakes]);
|
|
105
|
+
if (delegations.length) activeJsonFilters.push(["delegation", delegations]);
|
|
106
|
+
if (tokenFactories.length) activeJsonFilters.push(["tokenFactory", tokenFactories]);
|
|
107
|
+
const hasNonJsonFilters = !!(sender || receiver || types.length || txs.length || parsedStart || parsedEnd || validity);
|
|
108
|
+
const isUnionOnly = isUnion && !types.length && !txs.length && !parsedStart && !parsedEnd && !validity && activeJsonFilters.length === 0;
|
|
109
|
+
const isMultiTypeOnly = types.length > 1 && !sender && !receiver && !txs.length && !parsedStart && !parsedEnd && !validity && activeJsonFilters.length === 0;
|
|
110
|
+
if (isMutual) {
|
|
111
|
+
const [c1, c2] = await Promise.all([this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("sender", "=", sender).where("receiver", "=", receiver).executeTakeFirst(), this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("sender", "=", receiver).where("receiver", "=", sender).executeTakeFirst()]);
|
|
112
|
+
total = (c1?.count ?? 0) + (c2?.count ?? 0);
|
|
113
|
+
} else if (!hasNonJsonFilters && activeJsonFilters.length === 1) {
|
|
114
|
+
const [refType, values] = activeJsonFilters[0];
|
|
115
|
+
total = (await this.db.selectFrom("tx_ref").select(this.db.fn.countAll().as("count")).where("type", "=", refType).where("value", "in", values).executeTakeFirst())?.count ?? 0;
|
|
116
|
+
} else if (isUnionOnly) {
|
|
117
|
+
const [senderCount, receiverCount, overlapCount] = await Promise.all([
|
|
118
|
+
this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("sender", "=", sender).executeTakeFirst(),
|
|
119
|
+
this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("receiver", "=", receiver).executeTakeFirst(),
|
|
120
|
+
this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("sender", "=", sender).where("receiver", "=", receiver).executeTakeFirst()
|
|
121
|
+
]);
|
|
122
|
+
total = (senderCount?.count ?? 0) + (receiverCount?.count ?? 0) - (overlapCount?.count ?? 0);
|
|
123
|
+
} else if (isMultiTypeOnly) total = (await Promise.all(types.map((t) => this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("type", "=", t).executeTakeFirst()))).reduce((sum, c) => sum + (c?.count ?? 0), 0);
|
|
124
|
+
else if (!hasNonJsonFilters && activeJsonFilters.length === 0) total = (await sql`SELECT max(rowid) as c FROM tx`.execute(this.db)).rows[0]?.c ?? 0;
|
|
125
|
+
else total = (await qb.clearSelect().select(this.db.fn.countAll().as("count")).executeTakeFirst())?.count ?? 0;
|
|
83
126
|
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
|
84
|
-
|
|
85
|
-
|
|
127
|
+
let rawResults;
|
|
128
|
+
if (isMutual) {
|
|
129
|
+
const q1 = this.db.selectFrom("tx").selectAll().where("sender", "=", sender).where("receiver", "=", receiver);
|
|
130
|
+
const q2 = this.db.selectFrom("tx").selectAll().where("sender", "=", receiver).where("receiver", "=", sender);
|
|
131
|
+
rawResults = await q1.unionAll(q2).orderBy(pagination.order.field, orderDir).limit(pagination.size).offset(pagination.cursor).execute();
|
|
132
|
+
} else if (isUnionOnly) {
|
|
133
|
+
const topN = pagination.cursor + pagination.size;
|
|
134
|
+
const dirRaw = sql.raw(orderDir === "desc" ? "DESC" : "ASC");
|
|
135
|
+
const colRef = sql.ref(pagination.order.field);
|
|
136
|
+
rawResults = (await sql`
|
|
137
|
+
SELECT * FROM (
|
|
138
|
+
SELECT * FROM (SELECT * FROM tx WHERE sender = ${sender} ORDER BY ${colRef} ${dirRaw} LIMIT ${topN})
|
|
139
|
+
UNION
|
|
140
|
+
SELECT * FROM (SELECT * FROM tx WHERE receiver = ${receiver} ORDER BY ${colRef} ${dirRaw} LIMIT ${topN})
|
|
141
|
+
) ORDER BY ${colRef} ${dirRaw}
|
|
142
|
+
LIMIT ${pagination.size} OFFSET ${pagination.cursor}
|
|
143
|
+
`.execute(this.db)).rows;
|
|
144
|
+
} else if (isMultiTypeOnly) {
|
|
145
|
+
const topN = pagination.cursor + pagination.size;
|
|
146
|
+
const dirRaw = sql.raw(orderDir === "desc" ? "DESC" : "ASC");
|
|
147
|
+
const colRef = sql.ref(pagination.order.field);
|
|
148
|
+
let unionQuery = sql`SELECT * FROM (SELECT * FROM tx WHERE type = ${types[0]} ORDER BY ${colRef} ${dirRaw} LIMIT ${topN})`;
|
|
149
|
+
for (let i = 1; i < types.length; i++) unionQuery = sql`${unionQuery} UNION ALL SELECT * FROM (SELECT * FROM tx WHERE type = ${types[i]} ORDER BY ${colRef} ${dirRaw} LIMIT ${topN})`;
|
|
150
|
+
rawResults = (await sql`
|
|
151
|
+
SELECT * FROM (${unionQuery}) ORDER BY ${colRef} ${dirRaw}
|
|
152
|
+
LIMIT ${pagination.size} OFFSET ${pagination.cursor}
|
|
153
|
+
`.execute(this.db)).rows;
|
|
154
|
+
} else if (!hasNonJsonFilters && activeJsonFilters.length === 1 && pagination.order.field === "time") {
|
|
155
|
+
const [refType, values] = activeJsonFilters[0];
|
|
156
|
+
const hashes = (await this.db.selectFrom("tx_ref").select("hash").where("type", "=", refType).where("value", "in", values).orderBy("time", orderDir).limit(pagination.size).offset(pagination.cursor).execute()).map((r) => r.hash);
|
|
157
|
+
if (hashes.length > 0) {
|
|
158
|
+
const dataRows = await this.db.selectFrom("tx").selectAll().where("hash", "in", hashes).execute();
|
|
159
|
+
const hashOrder = new Map(hashes.map((h, i) => [h, i]));
|
|
160
|
+
rawResults = [...dataRows].sort((a, b) => (hashOrder.get(a.hash) ?? 0) - (hashOrder.get(b.hash) ?? 0));
|
|
161
|
+
} else rawResults = [];
|
|
162
|
+
} else {
|
|
163
|
+
qb = qb.orderBy(pagination.order.field, orderDir).offset(pagination.cursor).limit(pagination.size);
|
|
164
|
+
rawResults = await qb.execute();
|
|
165
|
+
}
|
|
166
|
+
let transactions = rawResults.map((row) => {
|
|
86
167
|
const tx = { ...row };
|
|
87
168
|
[
|
|
88
169
|
"receipts",
|
|
@@ -212,7 +293,9 @@ var SqliteBaseIndexDB = class extends BaseIndexDB {
|
|
|
212
293
|
};
|
|
213
294
|
}
|
|
214
295
|
/**
|
|
215
|
-
* List top accounts by token balance
|
|
296
|
+
* List top accounts by token balance.
|
|
297
|
+
* For tokenAddress filter: uses balance table JOIN for indexed lookup.
|
|
298
|
+
* Without tokenAddress: uses SQL ORDER BY + LIMIT directly.
|
|
216
299
|
*/
|
|
217
300
|
async listTopAccounts(params = {}) {
|
|
218
301
|
const { paging, tokenAddress } = params;
|
|
@@ -230,12 +313,8 @@ var SqliteBaseIndexDB = class extends BaseIndexDB {
|
|
|
230
313
|
paging,
|
|
231
314
|
pagination
|
|
232
315
|
});
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
|
236
|
-
qb = qb.orderBy(pagination.order.field, orderDir);
|
|
237
|
-
}
|
|
238
|
-
let accounts = (await qb.execute()).map((row) => {
|
|
316
|
+
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
|
317
|
+
const parseAccount = (row) => {
|
|
239
318
|
const account = { ...row };
|
|
240
319
|
[
|
|
241
320
|
"stake",
|
|
@@ -251,31 +330,21 @@ var SqliteBaseIndexDB = class extends BaseIndexDB {
|
|
|
251
330
|
return token;
|
|
252
331
|
});
|
|
253
332
|
return account;
|
|
254
|
-
}
|
|
255
|
-
if (tokenAddress)
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
const descending = pagination.order.type === "desc";
|
|
263
|
-
accounts.sort((a, b) => {
|
|
264
|
-
const tokensA = a.tokens;
|
|
265
|
-
const tokensB = b.tokens;
|
|
266
|
-
const tokenA = tokensA?.find((t) => t.address === tokenAddress);
|
|
267
|
-
const tokenB = tokensB?.find((t) => t.address === tokenAddress);
|
|
268
|
-
const balanceA = new BN(tokenA ? tokenA.balance : "0");
|
|
269
|
-
const balanceB = new BN(tokenB ? tokenB.balance : "0");
|
|
270
|
-
if (balanceB.gt(balanceA)) return descending ? 1 : -1;
|
|
271
|
-
if (balanceB.eq(balanceA)) return 0;
|
|
272
|
-
return descending ? -1 : 1;
|
|
273
|
-
});
|
|
333
|
+
};
|
|
334
|
+
if (tokenAddress) {
|
|
335
|
+
const total$1 = (await this.db.selectFrom("balance").select(this.db.fn.countAll().as("count")).where("tokenAddress", "=", tokenAddress).where("balance", "!=", "0").executeTakeFirst())?.count ?? 0;
|
|
336
|
+
const sortField$1 = pagination.order.field === "balance" ? "b.balanceNum" : `a.${pagination.order.field}`;
|
|
337
|
+
return {
|
|
338
|
+
accounts: (await this.db.selectFrom("balance as b").innerJoin("account as a", "a.address", "b.address").selectAll("a").where("b.tokenAddress", "=", tokenAddress).where("b.balance", "!=", "0").orderBy(sortField$1, orderDir).limit(pagination.size).offset(pagination.cursor).execute()).map(parseAccount),
|
|
339
|
+
paging: formatNextPagination(total$1, pagination)
|
|
340
|
+
};
|
|
274
341
|
}
|
|
275
|
-
|
|
276
|
-
|
|
342
|
+
let qb = this.db.selectFrom("account").selectAll();
|
|
343
|
+
const sortField = pagination.order.field === "balance" ? "balanceNum" : pagination.order.field;
|
|
344
|
+
qb = qb.orderBy(sortField, orderDir);
|
|
345
|
+
const total = (await this.db.selectFrom("account").select(this.db.fn.countAll().as("count")).executeTakeFirst())?.count ?? 0;
|
|
277
346
|
return {
|
|
278
|
-
accounts,
|
|
347
|
+
accounts: (await qb.limit(pagination.size).offset(pagination.cursor).execute()).map(parseAccount),
|
|
279
348
|
paging: formatNextPagination(total, pagination)
|
|
280
349
|
};
|
|
281
350
|
}
|
|
@@ -457,7 +526,10 @@ var SqliteBaseIndexDB = class extends BaseIndexDB {
|
|
|
457
526
|
if (parsedStart && parsedEnd) qb = qb.where("renaissanceTime", ">", parsedStart).where("renaissanceTime", "<=", parsedEnd);
|
|
458
527
|
else if (parsedStart) qb = qb.where("renaissanceTime", ">", parsedStart);
|
|
459
528
|
else if (parsedEnd) qb = qb.where("renaissanceTime", "<=", parsedEnd);
|
|
460
|
-
if (assets.length) qb = qb.where((eb) =>
|
|
529
|
+
if (assets.length) qb = qb.where((eb) => {
|
|
530
|
+
const conditions = assets.map((v) => eb(sql`INSTR(${sql.ref("assets")}, ${`"${v}"`})`, ">", 0));
|
|
531
|
+
return conditions.length === 1 ? conditions[0] : eb.or(conditions);
|
|
532
|
+
});
|
|
461
533
|
const total = (await qb.clearSelect().select(this.db.fn.countAll().as("count")).executeTakeFirst())?.count ?? 0;
|
|
462
534
|
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
|
463
535
|
qb = qb.orderBy(pagination.order.field, orderDir).offset(pagination.cursor).limit(pagination.size);
|
|
@@ -550,8 +622,14 @@ var SqliteBaseIndexDB = class extends BaseIndexDB {
|
|
|
550
622
|
if (parsedStart && parsedEnd) qb = qb.where("genesisTime", ">", parsedStart).where("genesisTime", "<=", parsedEnd);
|
|
551
623
|
else if (parsedStart) qb = qb.where("genesisTime", ">", parsedStart);
|
|
552
624
|
else if (parsedEnd) qb = qb.where("genesisTime", "<=", parsedEnd);
|
|
553
|
-
if (txs.length) qb = qb.where((eb) =>
|
|
554
|
-
|
|
625
|
+
if (txs.length) qb = qb.where((eb) => {
|
|
626
|
+
const conditions = txs.map((v) => eb(sql`INSTR(${sql.ref("txs")}, ${`"${v}"`})`, ">", 0));
|
|
627
|
+
return conditions.length === 1 ? conditions[0] : eb.or(conditions);
|
|
628
|
+
});
|
|
629
|
+
if (validators.length) qb = qb.where((eb) => {
|
|
630
|
+
const conditions = validators.map((v) => eb(sql`INSTR(${sql.ref("validators")}, ${`"${v}"`})`, ">", 0));
|
|
631
|
+
return conditions.length === 1 ? conditions[0] : eb.or(conditions);
|
|
632
|
+
});
|
|
555
633
|
if (tokenAddress) qb = qb.where((eb) => eb(eb.fn("json_extract", [eb.ref("tokenInfo"), eb.val("$.address")]), "=", tokenAddress));
|
|
556
634
|
const total = (await qb.clearSelect().select(this.db.fn.countAll().as("count")).executeTakeFirst())?.count ?? 0;
|
|
557
635
|
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
package/esm/db/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createKysely } from "../kysely.mjs";
|
|
2
2
|
import { runMigrations } from "../migrations/index.mjs";
|
|
3
3
|
import base_default from "../table/base.mjs";
|
|
4
|
+
import account_default from "../table/account.mjs";
|
|
4
5
|
import transaction_default from "../table/transaction.mjs";
|
|
5
6
|
import base_default$1 from "./base.mjs";
|
|
6
7
|
import package_default from "../package.mjs";
|
|
@@ -96,7 +97,7 @@ var SqliteIndexDB = class extends base_default$1 {
|
|
|
96
97
|
]
|
|
97
98
|
}
|
|
98
99
|
});
|
|
99
|
-
this.account = new
|
|
100
|
+
this.account = new account_default({
|
|
100
101
|
name: "account",
|
|
101
102
|
uniqIndex: "address",
|
|
102
103
|
db: this.db,
|
package/esm/interfaces.d.mts
CHANGED
|
@@ -20,6 +20,7 @@ interface Database {
|
|
|
20
20
|
tokenDistribution: TokenDistributionTable;
|
|
21
21
|
tokenFactory: TokenFactoryTable;
|
|
22
22
|
balance: BalanceTable;
|
|
23
|
+
tx_ref: TxRefTable;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* Transaction table for indexed transactions
|
|
@@ -318,6 +319,16 @@ interface BalanceTable {
|
|
|
318
319
|
balanceNum: string | null;
|
|
319
320
|
data: string | null;
|
|
320
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Junction table for fast tx JSON array lookups.
|
|
324
|
+
* Normalizes the JSON array columns (factories, tokens, etc.) into indexed rows.
|
|
325
|
+
*/
|
|
326
|
+
interface TxRefTable {
|
|
327
|
+
hash: string;
|
|
328
|
+
type: string;
|
|
329
|
+
value: string;
|
|
330
|
+
time: string | null;
|
|
331
|
+
}
|
|
321
332
|
/**
|
|
322
333
|
* SQLite-specific operation context with optional transaction
|
|
323
334
|
*/
|
|
@@ -325,4 +336,4 @@ interface SqliteOperationContext {
|
|
|
325
336
|
txn?: Transaction<Database>;
|
|
326
337
|
}
|
|
327
338
|
//#endregion
|
|
328
|
-
export { AccountTable, AssetTable, BalanceTable, Database, DelegationTable, FactoryTable, RollupBlockTable, RollupTable, RollupValidatorTable, SqliteOperationContext, StakeTable, TokenDistributionTable, TokenFactoryTable, TokenTable, TxTable };
|
|
339
|
+
export { AccountTable, AssetTable, BalanceTable, Database, DelegationTable, FactoryTable, RollupBlockTable, RollupTable, RollupValidatorTable, SqliteOperationContext, StakeTable, TokenDistributionTable, TokenFactoryTable, TokenTable, TxRefTable, TxTable };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
|
|
3
|
+
//#region src/migrations/002-composite-indexes.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Add composite indexes for covering WHERE + ORDER BY query patterns.
|
|
7
|
+
* These eliminate filesort when filtering by one column and sorting by another.
|
|
8
|
+
*/
|
|
9
|
+
declare function up(db: Kysely<unknown>): Promise<void>;
|
|
10
|
+
declare function down(db: Kysely<unknown>): Promise<void>;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { down, up };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { __exportAll } from "../_virtual/rolldown_runtime.mjs";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
|
|
4
|
+
//#region src/migrations/002-composite-indexes.ts
|
|
5
|
+
var _002_composite_indexes_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
down: () => down,
|
|
7
|
+
up: () => up
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Add composite indexes for covering WHERE + ORDER BY query patterns.
|
|
11
|
+
* These eliminate filesort when filtering by one column and sorting by another.
|
|
12
|
+
*/
|
|
13
|
+
async function up(db) {
|
|
14
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_tx_sender_time ON tx(sender, time DESC)`.execute(db);
|
|
15
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_tx_receiver_time ON tx(receiver, time DESC)`.execute(db);
|
|
16
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_tx_type_time ON tx(type, time DESC)`.execute(db);
|
|
17
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_tx_valid_time ON tx(valid, time DESC)`.execute(db);
|
|
18
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_asset_owner_renaissance ON asset(owner, renaissanceTime DESC)`.execute(db);
|
|
19
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_asset_parent_renaissance ON asset(parent, renaissanceTime DESC)`.execute(db);
|
|
20
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_stake_sender_renaissance ON stake(sender, renaissanceTime DESC)`.execute(db);
|
|
21
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_stake_receiver_renaissance ON stake(receiver, renaissanceTime DESC)`.execute(db);
|
|
22
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_delegation_from_renaissance ON delegation(from_, renaissanceTime DESC)`.execute(db);
|
|
23
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_delegation_to_renaissance ON delegation(to_, renaissanceTime DESC)`.execute(db);
|
|
24
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_rollupblock_rollup_genesis ON rollupBlock(rollup, genesisTime DESC)`.execute(db);
|
|
25
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_rollupvalidator_rollup_genesis ON rollupValidator(rollup, genesisTime DESC)`.execute(db);
|
|
26
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_factory_owner_renaissance ON factory(owner, renaissanceTime DESC)`.execute(db);
|
|
27
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_token_issuer_renaissance ON token(issuer, renaissanceTime DESC)`.execute(db);
|
|
28
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_tokenfactory_owner_renaissance ON tokenFactory(owner, renaissanceTime DESC)`.execute(db);
|
|
29
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_tx_sender_receiver_time ON tx(sender, receiver, time DESC)`.execute(db);
|
|
30
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_balance_token_amount ON balance(tokenAddress, balanceNum DESC)`.execute(db);
|
|
31
|
+
await sql`CREATE TABLE IF NOT EXISTS tx_ref (hash TEXT NOT NULL, type TEXT NOT NULL, value TEXT NOT NULL, time TEXT)`.execute(db);
|
|
32
|
+
await sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_tx_ref_pk ON tx_ref(hash, type, value)`.execute(db);
|
|
33
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_tx_ref_lookup ON tx_ref(type, value)`.execute(db);
|
|
34
|
+
await sql`CREATE INDEX IF NOT EXISTS idx_tx_ref_time_lookup ON tx_ref(type, value, time DESC)`.execute(db);
|
|
35
|
+
for (const [type, column] of [
|
|
36
|
+
["factory", "factories"],
|
|
37
|
+
["token", "tokens"],
|
|
38
|
+
["asset", "assets"],
|
|
39
|
+
["account", "accounts"],
|
|
40
|
+
["rollup", "rollups"],
|
|
41
|
+
["stake", "stakes"],
|
|
42
|
+
["delegation", "delegations"],
|
|
43
|
+
["tokenFactory", "tokenFactories"]
|
|
44
|
+
]) await sql.raw(`INSERT OR IGNORE INTO tx_ref (hash, type, value, time) SELECT tx.hash, '${type}', je.value, tx.time FROM tx, json_each(tx.${column}) AS je WHERE tx.${column} IS NOT NULL AND tx.${column} != '[]'`).execute(db);
|
|
45
|
+
}
|
|
46
|
+
async function down(db) {
|
|
47
|
+
for (const idx of [
|
|
48
|
+
"idx_tx_sender_time",
|
|
49
|
+
"idx_tx_receiver_time",
|
|
50
|
+
"idx_tx_type_time",
|
|
51
|
+
"idx_tx_valid_time",
|
|
52
|
+
"idx_asset_owner_renaissance",
|
|
53
|
+
"idx_asset_parent_renaissance",
|
|
54
|
+
"idx_stake_sender_renaissance",
|
|
55
|
+
"idx_stake_receiver_renaissance",
|
|
56
|
+
"idx_delegation_from_renaissance",
|
|
57
|
+
"idx_delegation_to_renaissance",
|
|
58
|
+
"idx_rollupblock_rollup_genesis",
|
|
59
|
+
"idx_rollupvalidator_rollup_genesis",
|
|
60
|
+
"idx_factory_owner_renaissance",
|
|
61
|
+
"idx_token_issuer_renaissance",
|
|
62
|
+
"idx_tokenfactory_owner_renaissance",
|
|
63
|
+
"idx_tx_sender_receiver_time",
|
|
64
|
+
"idx_balance_token_amount",
|
|
65
|
+
"idx_tx_ref_pk",
|
|
66
|
+
"idx_tx_ref_lookup",
|
|
67
|
+
"idx_tx_ref_time_lookup"
|
|
68
|
+
]) await sql.raw(`DROP INDEX IF EXISTS ${idx}`).execute(db);
|
|
69
|
+
await sql`DROP TABLE IF EXISTS tx_ref`.execute(db);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
export { _002_composite_indexes_exports, down, up };
|
package/esm/migrations/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { _001_genesis_exports } from "./001-genesis.mjs";
|
|
2
|
+
import { _002_composite_indexes_exports } from "./002-composite-indexes.mjs";
|
|
2
3
|
import { Migrator } from "kysely";
|
|
3
4
|
|
|
4
5
|
//#region src/migrations/index.ts
|
|
@@ -8,7 +9,10 @@ import { Migrator } from "kysely";
|
|
|
8
9
|
*/
|
|
9
10
|
var InMemoryMigrationProvider = class {
|
|
10
11
|
async getMigrations() {
|
|
11
|
-
return {
|
|
12
|
+
return {
|
|
13
|
+
"001-genesis": _001_genesis_exports,
|
|
14
|
+
"002-composite-indexes": _002_composite_indexes_exports
|
|
15
|
+
};
|
|
12
16
|
}
|
|
13
17
|
};
|
|
14
18
|
/**
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import SqliteTable from "./base.mjs";
|
|
2
|
+
import { TIndexedAccountState } from "@ocap/types";
|
|
3
|
+
|
|
4
|
+
//#region src/table/account.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Account table for SQLite IndexDB
|
|
8
|
+
* Extends base table to maintain the balance junction table
|
|
9
|
+
* for fast listTopAccounts queries with tokenAddress filter.
|
|
10
|
+
*/
|
|
11
|
+
declare class AccountTable extends SqliteTable<TIndexedAccountState> {
|
|
12
|
+
/**
|
|
13
|
+
* Override _insert to sync balance table after inserting an account.
|
|
14
|
+
*/
|
|
15
|
+
_insert(row: TIndexedAccountState | Record<string, unknown>): Promise<TIndexedAccountState>;
|
|
16
|
+
/**
|
|
17
|
+
* Override _update to sync balance table after updating an account.
|
|
18
|
+
*/
|
|
19
|
+
_update(key: string | Record<string, unknown>, updates: Partial<TIndexedAccountState>): Promise<TIndexedAccountState>;
|
|
20
|
+
/**
|
|
21
|
+
* Sync the balance table from account tokens.
|
|
22
|
+
* Deletes existing entries for this address, then inserts current token balances.
|
|
23
|
+
*/
|
|
24
|
+
private syncBalance;
|
|
25
|
+
/**
|
|
26
|
+
* Override _reset to also clear the balance table
|
|
27
|
+
*/
|
|
28
|
+
_reset(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { AccountTable as default };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import base_default from "./base.mjs";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
|
|
4
|
+
//#region src/table/account.ts
|
|
5
|
+
/**
|
|
6
|
+
* Account table for SQLite IndexDB
|
|
7
|
+
* Extends base table to maintain the balance junction table
|
|
8
|
+
* for fast listTopAccounts queries with tokenAddress filter.
|
|
9
|
+
*/
|
|
10
|
+
var AccountTable = class extends base_default {
|
|
11
|
+
/**
|
|
12
|
+
* Override _insert to sync balance table after inserting an account.
|
|
13
|
+
*/
|
|
14
|
+
async _insert(row) {
|
|
15
|
+
const result = await super._insert(row);
|
|
16
|
+
await this.syncBalance(row);
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Override _update to sync balance table after updating an account.
|
|
21
|
+
*/
|
|
22
|
+
async _update(key, updates) {
|
|
23
|
+
const result = await super._update(key, updates);
|
|
24
|
+
await this.syncBalance(result);
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Sync the balance table from account tokens.
|
|
29
|
+
* Deletes existing entries for this address, then inserts current token balances.
|
|
30
|
+
*/
|
|
31
|
+
async syncBalance(data) {
|
|
32
|
+
const address = data.address;
|
|
33
|
+
if (!address) return;
|
|
34
|
+
let tokens = data.tokens;
|
|
35
|
+
if (typeof tokens === "string") try {
|
|
36
|
+
tokens = JSON.parse(tokens);
|
|
37
|
+
} catch {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(tokens)) return;
|
|
41
|
+
await sql`DELETE FROM balance WHERE address = ${address}`.execute(this.db);
|
|
42
|
+
for (const t of tokens) if (t.address && t.balance && t.balance !== "0") {
|
|
43
|
+
const balanceNum = (t.balance.replace(/^0+/, "") || "0").padStart(40, "0");
|
|
44
|
+
await sql`INSERT INTO balance (address, tokenAddress, balance, balanceNum) VALUES (${address}, ${t.address}, ${t.balance}, ${balanceNum})`.execute(this.db);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Override _reset to also clear the balance table
|
|
49
|
+
*/
|
|
50
|
+
async _reset() {
|
|
51
|
+
await super._reset();
|
|
52
|
+
await this.db.deleteFrom("balance").execute();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var account_default = AccountTable;
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { account_default as default };
|
|
@@ -6,13 +6,19 @@ import { TIndexedTransaction } from "@ocap/types";
|
|
|
6
6
|
/**
|
|
7
7
|
* Transaction table for SQLite IndexDB
|
|
8
8
|
* Extends base table to call formatTxBeforeInsert on insert
|
|
9
|
+
* and maintain the tx_ref junction table for fast JSON array lookups
|
|
9
10
|
*/
|
|
10
11
|
declare class TransactionTable extends SqliteTable<TIndexedTransaction> {
|
|
11
12
|
/**
|
|
12
13
|
* Override _insert to format transaction before inserting
|
|
13
|
-
* This populates assets, tokens, factories, etc. from itxJson
|
|
14
|
+
* This populates assets, tokens, factories, etc. from itxJson,
|
|
15
|
+
* then inserts corresponding rows into tx_ref for indexed lookups.
|
|
14
16
|
*/
|
|
15
17
|
_insert(row: TIndexedTransaction | Record<string, unknown>): Promise<TIndexedTransaction>;
|
|
18
|
+
/**
|
|
19
|
+
* Override _reset to also clear the tx_ref junction table
|
|
20
|
+
*/
|
|
21
|
+
_reset(): Promise<void>;
|
|
16
22
|
}
|
|
17
23
|
//#endregion
|
|
18
24
|
export { TransactionTable as default };
|
|
@@ -1,19 +1,48 @@
|
|
|
1
1
|
import base_default from "./base.mjs";
|
|
2
|
+
import { sql } from "kysely";
|
|
2
3
|
import { formatTxBeforeInsert } from "@ocap/indexdb";
|
|
3
4
|
|
|
4
5
|
//#region src/table/transaction.ts
|
|
6
|
+
const TX_REF_TYPES = [
|
|
7
|
+
["factory", "factories"],
|
|
8
|
+
["token", "tokens"],
|
|
9
|
+
["asset", "assets"],
|
|
10
|
+
["account", "accounts"],
|
|
11
|
+
["rollup", "rollups"],
|
|
12
|
+
["stake", "stakes"],
|
|
13
|
+
["delegation", "delegations"],
|
|
14
|
+
["tokenFactory", "tokenFactories"]
|
|
15
|
+
];
|
|
5
16
|
/**
|
|
6
17
|
* Transaction table for SQLite IndexDB
|
|
7
18
|
* Extends base table to call formatTxBeforeInsert on insert
|
|
19
|
+
* and maintain the tx_ref junction table for fast JSON array lookups
|
|
8
20
|
*/
|
|
9
21
|
var TransactionTable = class extends base_default {
|
|
10
22
|
/**
|
|
11
23
|
* Override _insert to format transaction before inserting
|
|
12
|
-
* This populates assets, tokens, factories, etc. from itxJson
|
|
24
|
+
* This populates assets, tokens, factories, etc. from itxJson,
|
|
25
|
+
* then inserts corresponding rows into tx_ref for indexed lookups.
|
|
13
26
|
*/
|
|
14
27
|
async _insert(row) {
|
|
15
28
|
const formatted = formatTxBeforeInsert(row);
|
|
16
|
-
|
|
29
|
+
const result = await super._insert(formatted);
|
|
30
|
+
const data = formatted;
|
|
31
|
+
const hash = data.hash;
|
|
32
|
+
const time = data.time || null;
|
|
33
|
+
for (const [type, field] of TX_REF_TYPES) {
|
|
34
|
+
const values = data[field];
|
|
35
|
+
if (!Array.isArray(values) || values.length === 0) continue;
|
|
36
|
+
for (const v of values) if (typeof v === "string" && v) await sql`INSERT OR IGNORE INTO tx_ref (hash, type, value, time) VALUES (${hash}, ${type}, ${v}, ${time})`.execute(this.db);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Override _reset to also clear the tx_ref junction table
|
|
42
|
+
*/
|
|
43
|
+
async _reset() {
|
|
44
|
+
await super._reset();
|
|
45
|
+
await this.db.deleteFrom("tx_ref").execute();
|
|
17
46
|
}
|
|
18
47
|
};
|
|
19
48
|
var transaction_default = TransactionTable;
|
package/lib/db/base.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
2
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
3
|
+
let kysely = require("kysely");
|
|
3
4
|
let _ocap_indexdb = require("@ocap/indexdb");
|
|
4
5
|
let _arcblock_did_lib_type = require("@arcblock/did/lib/type");
|
|
5
|
-
let _ocap_util = require("@ocap/util");
|
|
6
6
|
let _ocap_util_lib_constant = require("@ocap/util/lib/constant");
|
|
7
7
|
let debug = require("debug");
|
|
8
8
|
debug = require_rolldown_runtime.__toESM(debug);
|
|
@@ -59,8 +59,9 @@ var SqliteBaseIndexDB = class extends _ocap_indexdb.BaseIndexDB {
|
|
|
59
59
|
includeItxData
|
|
60
60
|
});
|
|
61
61
|
let qb = this.db.selectFrom("tx").selectAll();
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const isMutual = !!(sender && receiver && direction === "MUTUAL");
|
|
63
|
+
const isUnion = !!(sender && receiver && direction !== "MUTUAL" && direction !== "ONE_WAY");
|
|
64
|
+
if (sender && receiver) if (direction === "MUTUAL") {} else if (direction === "ONE_WAY") qb = qb.where("sender", "=", sender).where("receiver", "=", receiver);
|
|
64
65
|
else qb = qb.where((eb) => eb.or([eb("sender", "=", sender), eb("receiver", "=", receiver)]));
|
|
65
66
|
else if (sender) qb = qb.where("sender", "=", sender);
|
|
66
67
|
else if (receiver) qb = qb.where("receiver", "=", receiver);
|
|
@@ -75,18 +76,98 @@ var SqliteBaseIndexDB = class extends _ocap_indexdb.BaseIndexDB {
|
|
|
75
76
|
const { validity } = validityFilter;
|
|
76
77
|
if (validity === "VALID") qb = qb.where("valid", "=", 1);
|
|
77
78
|
else if (validity === "INVALID") qb = qb.where("valid", "=", 0);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
const txRefTypeMap = {
|
|
80
|
+
factories: "factory",
|
|
81
|
+
tokens: "token",
|
|
82
|
+
assets: "asset",
|
|
83
|
+
accounts: "account",
|
|
84
|
+
rollups: "rollup",
|
|
85
|
+
stakes: "stake",
|
|
86
|
+
delegations: "delegation",
|
|
87
|
+
tokenFactories: "tokenFactory"
|
|
88
|
+
};
|
|
89
|
+
const applyJsonArrayFilter = (column, values) => {
|
|
90
|
+
const refType = txRefTypeMap[column];
|
|
91
|
+
qb = qb.where("hash", "in", this.db.selectFrom("tx_ref").select("hash").where("type", "=", refType).where("value", "in", values));
|
|
92
|
+
};
|
|
93
|
+
if (factories.length) applyJsonArrayFilter("factories", factories);
|
|
94
|
+
if (assets.length) applyJsonArrayFilter("assets", assets);
|
|
95
|
+
if (tokens.length) applyJsonArrayFilter("tokens", tokens);
|
|
96
|
+
if (accounts.length) applyJsonArrayFilter("accounts", accounts);
|
|
97
|
+
if (rollups.length) applyJsonArrayFilter("rollups", rollups);
|
|
98
|
+
if (stakes.length) applyJsonArrayFilter("stakes", stakes);
|
|
99
|
+
if (delegations.length) applyJsonArrayFilter("delegations", delegations);
|
|
100
|
+
if (tokenFactories.length) applyJsonArrayFilter("tokenFactories", tokenFactories);
|
|
101
|
+
let total;
|
|
102
|
+
const activeJsonFilters = [];
|
|
103
|
+
if (factories.length) activeJsonFilters.push(["factory", factories]);
|
|
104
|
+
if (assets.length) activeJsonFilters.push(["asset", assets]);
|
|
105
|
+
if (tokens.length) activeJsonFilters.push(["token", tokens]);
|
|
106
|
+
if (accounts.length) activeJsonFilters.push(["account", accounts]);
|
|
107
|
+
if (rollups.length) activeJsonFilters.push(["rollup", rollups]);
|
|
108
|
+
if (stakes.length) activeJsonFilters.push(["stake", stakes]);
|
|
109
|
+
if (delegations.length) activeJsonFilters.push(["delegation", delegations]);
|
|
110
|
+
if (tokenFactories.length) activeJsonFilters.push(["tokenFactory", tokenFactories]);
|
|
111
|
+
const hasNonJsonFilters = !!(sender || receiver || types.length || txs.length || parsedStart || parsedEnd || validity);
|
|
112
|
+
const isUnionOnly = isUnion && !types.length && !txs.length && !parsedStart && !parsedEnd && !validity && activeJsonFilters.length === 0;
|
|
113
|
+
const isMultiTypeOnly = types.length > 1 && !sender && !receiver && !txs.length && !parsedStart && !parsedEnd && !validity && activeJsonFilters.length === 0;
|
|
114
|
+
if (isMutual) {
|
|
115
|
+
const [c1, c2] = await Promise.all([this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("sender", "=", sender).where("receiver", "=", receiver).executeTakeFirst(), this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("sender", "=", receiver).where("receiver", "=", sender).executeTakeFirst()]);
|
|
116
|
+
total = (c1?.count ?? 0) + (c2?.count ?? 0);
|
|
117
|
+
} else if (!hasNonJsonFilters && activeJsonFilters.length === 1) {
|
|
118
|
+
const [refType, values] = activeJsonFilters[0];
|
|
119
|
+
total = (await this.db.selectFrom("tx_ref").select(this.db.fn.countAll().as("count")).where("type", "=", refType).where("value", "in", values).executeTakeFirst())?.count ?? 0;
|
|
120
|
+
} else if (isUnionOnly) {
|
|
121
|
+
const [senderCount, receiverCount, overlapCount] = await Promise.all([
|
|
122
|
+
this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("sender", "=", sender).executeTakeFirst(),
|
|
123
|
+
this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("receiver", "=", receiver).executeTakeFirst(),
|
|
124
|
+
this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("sender", "=", sender).where("receiver", "=", receiver).executeTakeFirst()
|
|
125
|
+
]);
|
|
126
|
+
total = (senderCount?.count ?? 0) + (receiverCount?.count ?? 0) - (overlapCount?.count ?? 0);
|
|
127
|
+
} else if (isMultiTypeOnly) total = (await Promise.all(types.map((t) => this.db.selectFrom("tx").select(this.db.fn.countAll().as("count")).where("type", "=", t).executeTakeFirst()))).reduce((sum, c) => sum + (c?.count ?? 0), 0);
|
|
128
|
+
else if (!hasNonJsonFilters && activeJsonFilters.length === 0) total = (await kysely.sql`SELECT max(rowid) as c FROM tx`.execute(this.db)).rows[0]?.c ?? 0;
|
|
129
|
+
else total = (await qb.clearSelect().select(this.db.fn.countAll().as("count")).executeTakeFirst())?.count ?? 0;
|
|
87
130
|
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
|
88
|
-
|
|
89
|
-
|
|
131
|
+
let rawResults;
|
|
132
|
+
if (isMutual) {
|
|
133
|
+
const q1 = this.db.selectFrom("tx").selectAll().where("sender", "=", sender).where("receiver", "=", receiver);
|
|
134
|
+
const q2 = this.db.selectFrom("tx").selectAll().where("sender", "=", receiver).where("receiver", "=", sender);
|
|
135
|
+
rawResults = await q1.unionAll(q2).orderBy(pagination.order.field, orderDir).limit(pagination.size).offset(pagination.cursor).execute();
|
|
136
|
+
} else if (isUnionOnly) {
|
|
137
|
+
const topN = pagination.cursor + pagination.size;
|
|
138
|
+
const dirRaw = kysely.sql.raw(orderDir === "desc" ? "DESC" : "ASC");
|
|
139
|
+
const colRef = kysely.sql.ref(pagination.order.field);
|
|
140
|
+
rawResults = (await kysely.sql`
|
|
141
|
+
SELECT * FROM (
|
|
142
|
+
SELECT * FROM (SELECT * FROM tx WHERE sender = ${sender} ORDER BY ${colRef} ${dirRaw} LIMIT ${topN})
|
|
143
|
+
UNION
|
|
144
|
+
SELECT * FROM (SELECT * FROM tx WHERE receiver = ${receiver} ORDER BY ${colRef} ${dirRaw} LIMIT ${topN})
|
|
145
|
+
) ORDER BY ${colRef} ${dirRaw}
|
|
146
|
+
LIMIT ${pagination.size} OFFSET ${pagination.cursor}
|
|
147
|
+
`.execute(this.db)).rows;
|
|
148
|
+
} else if (isMultiTypeOnly) {
|
|
149
|
+
const topN = pagination.cursor + pagination.size;
|
|
150
|
+
const dirRaw = kysely.sql.raw(orderDir === "desc" ? "DESC" : "ASC");
|
|
151
|
+
const colRef = kysely.sql.ref(pagination.order.field);
|
|
152
|
+
let unionQuery = kysely.sql`SELECT * FROM (SELECT * FROM tx WHERE type = ${types[0]} ORDER BY ${colRef} ${dirRaw} LIMIT ${topN})`;
|
|
153
|
+
for (let i = 1; i < types.length; i++) unionQuery = kysely.sql`${unionQuery} UNION ALL SELECT * FROM (SELECT * FROM tx WHERE type = ${types[i]} ORDER BY ${colRef} ${dirRaw} LIMIT ${topN})`;
|
|
154
|
+
rawResults = (await kysely.sql`
|
|
155
|
+
SELECT * FROM (${unionQuery}) ORDER BY ${colRef} ${dirRaw}
|
|
156
|
+
LIMIT ${pagination.size} OFFSET ${pagination.cursor}
|
|
157
|
+
`.execute(this.db)).rows;
|
|
158
|
+
} else if (!hasNonJsonFilters && activeJsonFilters.length === 1 && pagination.order.field === "time") {
|
|
159
|
+
const [refType, values] = activeJsonFilters[0];
|
|
160
|
+
const hashes = (await this.db.selectFrom("tx_ref").select("hash").where("type", "=", refType).where("value", "in", values).orderBy("time", orderDir).limit(pagination.size).offset(pagination.cursor).execute()).map((r) => r.hash);
|
|
161
|
+
if (hashes.length > 0) {
|
|
162
|
+
const dataRows = await this.db.selectFrom("tx").selectAll().where("hash", "in", hashes).execute();
|
|
163
|
+
const hashOrder = new Map(hashes.map((h, i) => [h, i]));
|
|
164
|
+
rawResults = [...dataRows].sort((a, b) => (hashOrder.get(a.hash) ?? 0) - (hashOrder.get(b.hash) ?? 0));
|
|
165
|
+
} else rawResults = [];
|
|
166
|
+
} else {
|
|
167
|
+
qb = qb.orderBy(pagination.order.field, orderDir).offset(pagination.cursor).limit(pagination.size);
|
|
168
|
+
rawResults = await qb.execute();
|
|
169
|
+
}
|
|
170
|
+
let transactions = rawResults.map((row) => {
|
|
90
171
|
const tx = { ...row };
|
|
91
172
|
[
|
|
92
173
|
"receipts",
|
|
@@ -216,7 +297,9 @@ var SqliteBaseIndexDB = class extends _ocap_indexdb.BaseIndexDB {
|
|
|
216
297
|
};
|
|
217
298
|
}
|
|
218
299
|
/**
|
|
219
|
-
* List top accounts by token balance
|
|
300
|
+
* List top accounts by token balance.
|
|
301
|
+
* For tokenAddress filter: uses balance table JOIN for indexed lookup.
|
|
302
|
+
* Without tokenAddress: uses SQL ORDER BY + LIMIT directly.
|
|
220
303
|
*/
|
|
221
304
|
async listTopAccounts(params = {}) {
|
|
222
305
|
const { paging, tokenAddress } = params;
|
|
@@ -234,12 +317,8 @@ var SqliteBaseIndexDB = class extends _ocap_indexdb.BaseIndexDB {
|
|
|
234
317
|
paging,
|
|
235
318
|
pagination
|
|
236
319
|
});
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
|
240
|
-
qb = qb.orderBy(pagination.order.field, orderDir);
|
|
241
|
-
}
|
|
242
|
-
let accounts = (await qb.execute()).map((row) => {
|
|
320
|
+
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
|
321
|
+
const parseAccount = (row) => {
|
|
243
322
|
const account = { ...row };
|
|
244
323
|
[
|
|
245
324
|
"stake",
|
|
@@ -255,31 +334,21 @@ var SqliteBaseIndexDB = class extends _ocap_indexdb.BaseIndexDB {
|
|
|
255
334
|
return token;
|
|
256
335
|
});
|
|
257
336
|
return account;
|
|
258
|
-
}
|
|
259
|
-
if (tokenAddress)
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const descending = pagination.order.type === "desc";
|
|
267
|
-
accounts.sort((a, b) => {
|
|
268
|
-
const tokensA = a.tokens;
|
|
269
|
-
const tokensB = b.tokens;
|
|
270
|
-
const tokenA = tokensA?.find((t) => t.address === tokenAddress);
|
|
271
|
-
const tokenB = tokensB?.find((t) => t.address === tokenAddress);
|
|
272
|
-
const balanceA = new _ocap_util.BN(tokenA ? tokenA.balance : "0");
|
|
273
|
-
const balanceB = new _ocap_util.BN(tokenB ? tokenB.balance : "0");
|
|
274
|
-
if (balanceB.gt(balanceA)) return descending ? 1 : -1;
|
|
275
|
-
if (balanceB.eq(balanceA)) return 0;
|
|
276
|
-
return descending ? -1 : 1;
|
|
277
|
-
});
|
|
337
|
+
};
|
|
338
|
+
if (tokenAddress) {
|
|
339
|
+
const total$1 = (await this.db.selectFrom("balance").select(this.db.fn.countAll().as("count")).where("tokenAddress", "=", tokenAddress).where("balance", "!=", "0").executeTakeFirst())?.count ?? 0;
|
|
340
|
+
const sortField$1 = pagination.order.field === "balance" ? "b.balanceNum" : `a.${pagination.order.field}`;
|
|
341
|
+
return {
|
|
342
|
+
accounts: (await this.db.selectFrom("balance as b").innerJoin("account as a", "a.address", "b.address").selectAll("a").where("b.tokenAddress", "=", tokenAddress).where("b.balance", "!=", "0").orderBy(sortField$1, orderDir).limit(pagination.size).offset(pagination.cursor).execute()).map(parseAccount),
|
|
343
|
+
paging: (0, _ocap_indexdb.formatNextPagination)(total$1, pagination)
|
|
344
|
+
};
|
|
278
345
|
}
|
|
279
|
-
|
|
280
|
-
|
|
346
|
+
let qb = this.db.selectFrom("account").selectAll();
|
|
347
|
+
const sortField = pagination.order.field === "balance" ? "balanceNum" : pagination.order.field;
|
|
348
|
+
qb = qb.orderBy(sortField, orderDir);
|
|
349
|
+
const total = (await this.db.selectFrom("account").select(this.db.fn.countAll().as("count")).executeTakeFirst())?.count ?? 0;
|
|
281
350
|
return {
|
|
282
|
-
accounts,
|
|
351
|
+
accounts: (await qb.limit(pagination.size).offset(pagination.cursor).execute()).map(parseAccount),
|
|
283
352
|
paging: (0, _ocap_indexdb.formatNextPagination)(total, pagination)
|
|
284
353
|
};
|
|
285
354
|
}
|
|
@@ -461,7 +530,10 @@ var SqliteBaseIndexDB = class extends _ocap_indexdb.BaseIndexDB {
|
|
|
461
530
|
if (parsedStart && parsedEnd) qb = qb.where("renaissanceTime", ">", parsedStart).where("renaissanceTime", "<=", parsedEnd);
|
|
462
531
|
else if (parsedStart) qb = qb.where("renaissanceTime", ">", parsedStart);
|
|
463
532
|
else if (parsedEnd) qb = qb.where("renaissanceTime", "<=", parsedEnd);
|
|
464
|
-
if (assets.length) qb = qb.where((eb) =>
|
|
533
|
+
if (assets.length) qb = qb.where((eb) => {
|
|
534
|
+
const conditions = assets.map((v) => eb(kysely.sql`INSTR(${kysely.sql.ref("assets")}, ${`"${v}"`})`, ">", 0));
|
|
535
|
+
return conditions.length === 1 ? conditions[0] : eb.or(conditions);
|
|
536
|
+
});
|
|
465
537
|
const total = (await qb.clearSelect().select(this.db.fn.countAll().as("count")).executeTakeFirst())?.count ?? 0;
|
|
466
538
|
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
|
467
539
|
qb = qb.orderBy(pagination.order.field, orderDir).offset(pagination.cursor).limit(pagination.size);
|
|
@@ -554,8 +626,14 @@ var SqliteBaseIndexDB = class extends _ocap_indexdb.BaseIndexDB {
|
|
|
554
626
|
if (parsedStart && parsedEnd) qb = qb.where("genesisTime", ">", parsedStart).where("genesisTime", "<=", parsedEnd);
|
|
555
627
|
else if (parsedStart) qb = qb.where("genesisTime", ">", parsedStart);
|
|
556
628
|
else if (parsedEnd) qb = qb.where("genesisTime", "<=", parsedEnd);
|
|
557
|
-
if (txs.length) qb = qb.where((eb) =>
|
|
558
|
-
|
|
629
|
+
if (txs.length) qb = qb.where((eb) => {
|
|
630
|
+
const conditions = txs.map((v) => eb(kysely.sql`INSTR(${kysely.sql.ref("txs")}, ${`"${v}"`})`, ">", 0));
|
|
631
|
+
return conditions.length === 1 ? conditions[0] : eb.or(conditions);
|
|
632
|
+
});
|
|
633
|
+
if (validators.length) qb = qb.where((eb) => {
|
|
634
|
+
const conditions = validators.map((v) => eb(kysely.sql`INSTR(${kysely.sql.ref("validators")}, ${`"${v}"`})`, ">", 0));
|
|
635
|
+
return conditions.length === 1 ? conditions[0] : eb.or(conditions);
|
|
636
|
+
});
|
|
559
637
|
if (tokenAddress) qb = qb.where((eb) => eb(eb.fn("json_extract", [eb.ref("tokenInfo"), eb.val("$.address")]), "=", tokenAddress));
|
|
560
638
|
const total = (await qb.clearSelect().select(this.db.fn.countAll().as("count")).executeTakeFirst())?.count ?? 0;
|
|
561
639
|
const orderDir = pagination.order.type === "desc" ? "desc" : "asc";
|
package/lib/db/base.d.cts
CHANGED
|
@@ -36,7 +36,9 @@ declare class SqliteBaseIndexDB extends BaseIndexDB implements IIndexDB {
|
|
|
36
36
|
*/
|
|
37
37
|
listAssets(params?: Partial<TRequestListAssets>): Promise<IListAssetsResult>;
|
|
38
38
|
/**
|
|
39
|
-
* List top accounts by token balance
|
|
39
|
+
* List top accounts by token balance.
|
|
40
|
+
* For tokenAddress filter: uses balance table JOIN for indexed lookup.
|
|
41
|
+
* Without tokenAddress: uses SQL ORDER BY + LIMIT directly.
|
|
40
42
|
*/
|
|
41
43
|
listTopAccounts(params?: Partial<TRequestListTopAccounts>): Promise<IListAccountsResult>;
|
|
42
44
|
/**
|
package/lib/db/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
2
2
|
const require_kysely = require('../kysely.cjs');
|
|
3
3
|
const require_migrations_index = require('../migrations/index.cjs');
|
|
4
4
|
const require_table_base = require('../table/base.cjs');
|
|
5
|
+
const require_table_account = require('../table/account.cjs');
|
|
5
6
|
const require_table_transaction = require('../table/transaction.cjs');
|
|
6
7
|
const require_db_base = require('./base.cjs');
|
|
7
8
|
const require_package = require('../package.cjs');
|
|
@@ -97,7 +98,7 @@ var SqliteIndexDB = class extends require_db_base.default {
|
|
|
97
98
|
]
|
|
98
99
|
}
|
|
99
100
|
});
|
|
100
|
-
this.account = new
|
|
101
|
+
this.account = new require_table_account.default({
|
|
101
102
|
name: "account",
|
|
102
103
|
uniqIndex: "address",
|
|
103
104
|
db: this.db,
|
package/lib/interfaces.d.cts
CHANGED
|
@@ -20,6 +20,7 @@ interface Database {
|
|
|
20
20
|
tokenDistribution: TokenDistributionTable;
|
|
21
21
|
tokenFactory: TokenFactoryTable;
|
|
22
22
|
balance: BalanceTable;
|
|
23
|
+
tx_ref: TxRefTable;
|
|
23
24
|
}
|
|
24
25
|
/**
|
|
25
26
|
* Transaction table for indexed transactions
|
|
@@ -318,6 +319,16 @@ interface BalanceTable {
|
|
|
318
319
|
balanceNum: string | null;
|
|
319
320
|
data: string | null;
|
|
320
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Junction table for fast tx JSON array lookups.
|
|
324
|
+
* Normalizes the JSON array columns (factories, tokens, etc.) into indexed rows.
|
|
325
|
+
*/
|
|
326
|
+
interface TxRefTable {
|
|
327
|
+
hash: string;
|
|
328
|
+
type: string;
|
|
329
|
+
value: string;
|
|
330
|
+
time: string | null;
|
|
331
|
+
}
|
|
321
332
|
/**
|
|
322
333
|
* SQLite-specific operation context with optional transaction
|
|
323
334
|
*/
|
|
@@ -325,4 +336,4 @@ interface SqliteOperationContext {
|
|
|
325
336
|
txn?: Transaction<Database>;
|
|
326
337
|
}
|
|
327
338
|
//#endregion
|
|
328
|
-
export { AccountTable, AssetTable, BalanceTable, Database, DelegationTable, FactoryTable, RollupBlockTable, RollupTable, RollupValidatorTable, SqliteOperationContext, StakeTable, TokenDistributionTable, TokenFactoryTable, TokenTable, TxTable };
|
|
339
|
+
export { AccountTable, AssetTable, BalanceTable, Database, DelegationTable, FactoryTable, RollupBlockTable, RollupTable, RollupValidatorTable, SqliteOperationContext, StakeTable, TokenDistributionTable, TokenFactoryTable, TokenTable, TxRefTable, TxTable };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
|
+
let kysely = require("kysely");
|
|
3
|
+
|
|
4
|
+
//#region src/migrations/002-composite-indexes.ts
|
|
5
|
+
var _002_composite_indexes_exports = /* @__PURE__ */ require_rolldown_runtime.__exportAll({
|
|
6
|
+
down: () => down,
|
|
7
|
+
up: () => up
|
|
8
|
+
});
|
|
9
|
+
/**
|
|
10
|
+
* Add composite indexes for covering WHERE + ORDER BY query patterns.
|
|
11
|
+
* These eliminate filesort when filtering by one column and sorting by another.
|
|
12
|
+
*/
|
|
13
|
+
async function up(db) {
|
|
14
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_tx_sender_time ON tx(sender, time DESC)`.execute(db);
|
|
15
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_tx_receiver_time ON tx(receiver, time DESC)`.execute(db);
|
|
16
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_tx_type_time ON tx(type, time DESC)`.execute(db);
|
|
17
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_tx_valid_time ON tx(valid, time DESC)`.execute(db);
|
|
18
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_asset_owner_renaissance ON asset(owner, renaissanceTime DESC)`.execute(db);
|
|
19
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_asset_parent_renaissance ON asset(parent, renaissanceTime DESC)`.execute(db);
|
|
20
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_stake_sender_renaissance ON stake(sender, renaissanceTime DESC)`.execute(db);
|
|
21
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_stake_receiver_renaissance ON stake(receiver, renaissanceTime DESC)`.execute(db);
|
|
22
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_delegation_from_renaissance ON delegation(from_, renaissanceTime DESC)`.execute(db);
|
|
23
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_delegation_to_renaissance ON delegation(to_, renaissanceTime DESC)`.execute(db);
|
|
24
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_rollupblock_rollup_genesis ON rollupBlock(rollup, genesisTime DESC)`.execute(db);
|
|
25
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_rollupvalidator_rollup_genesis ON rollupValidator(rollup, genesisTime DESC)`.execute(db);
|
|
26
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_factory_owner_renaissance ON factory(owner, renaissanceTime DESC)`.execute(db);
|
|
27
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_token_issuer_renaissance ON token(issuer, renaissanceTime DESC)`.execute(db);
|
|
28
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_tokenfactory_owner_renaissance ON tokenFactory(owner, renaissanceTime DESC)`.execute(db);
|
|
29
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_tx_sender_receiver_time ON tx(sender, receiver, time DESC)`.execute(db);
|
|
30
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_balance_token_amount ON balance(tokenAddress, balanceNum DESC)`.execute(db);
|
|
31
|
+
await kysely.sql`CREATE TABLE IF NOT EXISTS tx_ref (hash TEXT NOT NULL, type TEXT NOT NULL, value TEXT NOT NULL, time TEXT)`.execute(db);
|
|
32
|
+
await kysely.sql`CREATE UNIQUE INDEX IF NOT EXISTS idx_tx_ref_pk ON tx_ref(hash, type, value)`.execute(db);
|
|
33
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_tx_ref_lookup ON tx_ref(type, value)`.execute(db);
|
|
34
|
+
await kysely.sql`CREATE INDEX IF NOT EXISTS idx_tx_ref_time_lookup ON tx_ref(type, value, time DESC)`.execute(db);
|
|
35
|
+
for (const [type, column] of [
|
|
36
|
+
["factory", "factories"],
|
|
37
|
+
["token", "tokens"],
|
|
38
|
+
["asset", "assets"],
|
|
39
|
+
["account", "accounts"],
|
|
40
|
+
["rollup", "rollups"],
|
|
41
|
+
["stake", "stakes"],
|
|
42
|
+
["delegation", "delegations"],
|
|
43
|
+
["tokenFactory", "tokenFactories"]
|
|
44
|
+
]) await kysely.sql.raw(`INSERT OR IGNORE INTO tx_ref (hash, type, value, time) SELECT tx.hash, '${type}', je.value, tx.time FROM tx, json_each(tx.${column}) AS je WHERE tx.${column} IS NOT NULL AND tx.${column} != '[]'`).execute(db);
|
|
45
|
+
}
|
|
46
|
+
async function down(db) {
|
|
47
|
+
for (const idx of [
|
|
48
|
+
"idx_tx_sender_time",
|
|
49
|
+
"idx_tx_receiver_time",
|
|
50
|
+
"idx_tx_type_time",
|
|
51
|
+
"idx_tx_valid_time",
|
|
52
|
+
"idx_asset_owner_renaissance",
|
|
53
|
+
"idx_asset_parent_renaissance",
|
|
54
|
+
"idx_stake_sender_renaissance",
|
|
55
|
+
"idx_stake_receiver_renaissance",
|
|
56
|
+
"idx_delegation_from_renaissance",
|
|
57
|
+
"idx_delegation_to_renaissance",
|
|
58
|
+
"idx_rollupblock_rollup_genesis",
|
|
59
|
+
"idx_rollupvalidator_rollup_genesis",
|
|
60
|
+
"idx_factory_owner_renaissance",
|
|
61
|
+
"idx_token_issuer_renaissance",
|
|
62
|
+
"idx_tokenfactory_owner_renaissance",
|
|
63
|
+
"idx_tx_sender_receiver_time",
|
|
64
|
+
"idx_balance_token_amount",
|
|
65
|
+
"idx_tx_ref_pk",
|
|
66
|
+
"idx_tx_ref_lookup",
|
|
67
|
+
"idx_tx_ref_time_lookup"
|
|
68
|
+
]) await kysely.sql.raw(`DROP INDEX IF EXISTS ${idx}`).execute(db);
|
|
69
|
+
await kysely.sql`DROP TABLE IF EXISTS tx_ref`.execute(db);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
Object.defineProperty(exports, '_002_composite_indexes_exports', {
|
|
74
|
+
enumerable: true,
|
|
75
|
+
get: function () {
|
|
76
|
+
return _002_composite_indexes_exports;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
exports.down = down;
|
|
80
|
+
exports.up = up;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Kysely } from "kysely";
|
|
2
|
+
|
|
3
|
+
//#region src/migrations/002-composite-indexes.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Add composite indexes for covering WHERE + ORDER BY query patterns.
|
|
7
|
+
* These eliminate filesort when filtering by one column and sorting by another.
|
|
8
|
+
*/
|
|
9
|
+
declare function up(db: Kysely<unknown>): Promise<void>;
|
|
10
|
+
declare function down(db: Kysely<unknown>): Promise<void>;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { down, up };
|
package/lib/migrations/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
2
2
|
const require_migrations_001_genesis = require('./001-genesis.cjs');
|
|
3
|
+
const require_migrations_002_composite_indexes = require('./002-composite-indexes.cjs');
|
|
3
4
|
let kysely = require("kysely");
|
|
4
5
|
|
|
5
6
|
//#region src/migrations/index.ts
|
|
@@ -9,7 +10,10 @@ let kysely = require("kysely");
|
|
|
9
10
|
*/
|
|
10
11
|
var InMemoryMigrationProvider = class {
|
|
11
12
|
async getMigrations() {
|
|
12
|
-
return {
|
|
13
|
+
return {
|
|
14
|
+
"001-genesis": require_migrations_001_genesis._001_genesis_exports,
|
|
15
|
+
"002-composite-indexes": require_migrations_002_composite_indexes._002_composite_indexes_exports
|
|
16
|
+
};
|
|
13
17
|
}
|
|
14
18
|
};
|
|
15
19
|
/**
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
3
|
+
const require_table_base = require('./base.cjs');
|
|
4
|
+
let kysely = require("kysely");
|
|
5
|
+
|
|
6
|
+
//#region src/table/account.ts
|
|
7
|
+
/**
|
|
8
|
+
* Account table for SQLite IndexDB
|
|
9
|
+
* Extends base table to maintain the balance junction table
|
|
10
|
+
* for fast listTopAccounts queries with tokenAddress filter.
|
|
11
|
+
*/
|
|
12
|
+
var AccountTable = class extends require_table_base.default {
|
|
13
|
+
/**
|
|
14
|
+
* Override _insert to sync balance table after inserting an account.
|
|
15
|
+
*/
|
|
16
|
+
async _insert(row) {
|
|
17
|
+
const result = await super._insert(row);
|
|
18
|
+
await this.syncBalance(row);
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Override _update to sync balance table after updating an account.
|
|
23
|
+
*/
|
|
24
|
+
async _update(key, updates) {
|
|
25
|
+
const result = await super._update(key, updates);
|
|
26
|
+
await this.syncBalance(result);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Sync the balance table from account tokens.
|
|
31
|
+
* Deletes existing entries for this address, then inserts current token balances.
|
|
32
|
+
*/
|
|
33
|
+
async syncBalance(data) {
|
|
34
|
+
const address = data.address;
|
|
35
|
+
if (!address) return;
|
|
36
|
+
let tokens = data.tokens;
|
|
37
|
+
if (typeof tokens === "string") try {
|
|
38
|
+
tokens = JSON.parse(tokens);
|
|
39
|
+
} catch {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(tokens)) return;
|
|
43
|
+
await kysely.sql`DELETE FROM balance WHERE address = ${address}`.execute(this.db);
|
|
44
|
+
for (const t of tokens) if (t.address && t.balance && t.balance !== "0") {
|
|
45
|
+
const balanceNum = (t.balance.replace(/^0+/, "") || "0").padStart(40, "0");
|
|
46
|
+
await kysely.sql`INSERT INTO balance (address, tokenAddress, balance, balanceNum) VALUES (${address}, ${t.address}, ${t.balance}, ${balanceNum})`.execute(this.db);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Override _reset to also clear the balance table
|
|
51
|
+
*/
|
|
52
|
+
async _reset() {
|
|
53
|
+
await super._reset();
|
|
54
|
+
await this.db.deleteFrom("balance").execute();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var account_default = AccountTable;
|
|
58
|
+
|
|
59
|
+
//#endregion
|
|
60
|
+
exports.default = account_default;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import SqliteTable from "./base.cjs";
|
|
2
|
+
import { TIndexedAccountState } from "@ocap/types";
|
|
3
|
+
|
|
4
|
+
//#region src/table/account.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Account table for SQLite IndexDB
|
|
8
|
+
* Extends base table to maintain the balance junction table
|
|
9
|
+
* for fast listTopAccounts queries with tokenAddress filter.
|
|
10
|
+
*/
|
|
11
|
+
declare class AccountTable extends SqliteTable<TIndexedAccountState> {
|
|
12
|
+
/**
|
|
13
|
+
* Override _insert to sync balance table after inserting an account.
|
|
14
|
+
*/
|
|
15
|
+
_insert(row: TIndexedAccountState | Record<string, unknown>): Promise<TIndexedAccountState>;
|
|
16
|
+
/**
|
|
17
|
+
* Override _update to sync balance table after updating an account.
|
|
18
|
+
*/
|
|
19
|
+
_update(key: string | Record<string, unknown>, updates: Partial<TIndexedAccountState>): Promise<TIndexedAccountState>;
|
|
20
|
+
/**
|
|
21
|
+
* Sync the balance table from account tokens.
|
|
22
|
+
* Deletes existing entries for this address, then inserts current token balances.
|
|
23
|
+
*/
|
|
24
|
+
private syncBalance;
|
|
25
|
+
/**
|
|
26
|
+
* Override _reset to also clear the balance table
|
|
27
|
+
*/
|
|
28
|
+
_reset(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { AccountTable as default };
|
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
2
|
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.cjs');
|
|
3
3
|
const require_table_base = require('./base.cjs');
|
|
4
|
+
let kysely = require("kysely");
|
|
4
5
|
let _ocap_indexdb = require("@ocap/indexdb");
|
|
5
6
|
|
|
6
7
|
//#region src/table/transaction.ts
|
|
8
|
+
const TX_REF_TYPES = [
|
|
9
|
+
["factory", "factories"],
|
|
10
|
+
["token", "tokens"],
|
|
11
|
+
["asset", "assets"],
|
|
12
|
+
["account", "accounts"],
|
|
13
|
+
["rollup", "rollups"],
|
|
14
|
+
["stake", "stakes"],
|
|
15
|
+
["delegation", "delegations"],
|
|
16
|
+
["tokenFactory", "tokenFactories"]
|
|
17
|
+
];
|
|
7
18
|
/**
|
|
8
19
|
* Transaction table for SQLite IndexDB
|
|
9
20
|
* Extends base table to call formatTxBeforeInsert on insert
|
|
21
|
+
* and maintain the tx_ref junction table for fast JSON array lookups
|
|
10
22
|
*/
|
|
11
23
|
var TransactionTable = class extends require_table_base.default {
|
|
12
24
|
/**
|
|
13
25
|
* Override _insert to format transaction before inserting
|
|
14
|
-
* This populates assets, tokens, factories, etc. from itxJson
|
|
26
|
+
* This populates assets, tokens, factories, etc. from itxJson,
|
|
27
|
+
* then inserts corresponding rows into tx_ref for indexed lookups.
|
|
15
28
|
*/
|
|
16
29
|
async _insert(row) {
|
|
17
30
|
const formatted = (0, _ocap_indexdb.formatTxBeforeInsert)(row);
|
|
18
|
-
|
|
31
|
+
const result = await super._insert(formatted);
|
|
32
|
+
const data = formatted;
|
|
33
|
+
const hash = data.hash;
|
|
34
|
+
const time = data.time || null;
|
|
35
|
+
for (const [type, field] of TX_REF_TYPES) {
|
|
36
|
+
const values = data[field];
|
|
37
|
+
if (!Array.isArray(values) || values.length === 0) continue;
|
|
38
|
+
for (const v of values) if (typeof v === "string" && v) await kysely.sql`INSERT OR IGNORE INTO tx_ref (hash, type, value, time) VALUES (${hash}, ${type}, ${v}, ${time})`.execute(this.db);
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Override _reset to also clear the tx_ref junction table
|
|
44
|
+
*/
|
|
45
|
+
async _reset() {
|
|
46
|
+
await super._reset();
|
|
47
|
+
await this.db.deleteFrom("tx_ref").execute();
|
|
19
48
|
}
|
|
20
49
|
};
|
|
21
50
|
var transaction_default = TransactionTable;
|
|
@@ -6,13 +6,19 @@ import { TIndexedTransaction } from "@ocap/types";
|
|
|
6
6
|
/**
|
|
7
7
|
* Transaction table for SQLite IndexDB
|
|
8
8
|
* Extends base table to call formatTxBeforeInsert on insert
|
|
9
|
+
* and maintain the tx_ref junction table for fast JSON array lookups
|
|
9
10
|
*/
|
|
10
11
|
declare class TransactionTable extends SqliteTable<TIndexedTransaction> {
|
|
11
12
|
/**
|
|
12
13
|
* Override _insert to format transaction before inserting
|
|
13
|
-
* This populates assets, tokens, factories, etc. from itxJson
|
|
14
|
+
* This populates assets, tokens, factories, etc. from itxJson,
|
|
15
|
+
* then inserts corresponding rows into tx_ref for indexed lookups.
|
|
14
16
|
*/
|
|
15
17
|
_insert(row: TIndexedTransaction | Record<string, unknown>): Promise<TIndexedTransaction>;
|
|
18
|
+
/**
|
|
19
|
+
* Override _reset to also clear the tx_ref junction table
|
|
20
|
+
*/
|
|
21
|
+
_reset(): Promise<void>;
|
|
16
22
|
}
|
|
17
23
|
//#endregion
|
|
18
24
|
export { TransactionTable as default };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ocap/indexdb-sqlite",
|
|
3
3
|
"description": "OCAP indexdb adapter that uses SQLite as backend",
|
|
4
|
-
"version": "1.29.
|
|
4
|
+
"version": "1.29.11",
|
|
5
5
|
"author": "wangshijun <shijun@arcblock.io> (https://www.arcblock.io)",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/ArcBlock/blockchain/issues",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"wangshijun <shijun@arcblock.io> (https://www.arcblock.io)"
|
|
15
15
|
],
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@ocap/indexdb-test": "1.29.
|
|
17
|
+
"@ocap/indexdb-test": "1.29.11"
|
|
18
18
|
},
|
|
19
19
|
"homepage": "https://github.com/ArcBlock/blockchain/tree/master/indexdb/sqlite",
|
|
20
20
|
"keywords": [
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
"clean": "rm -rf lib esm"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@arcblock/did": "1.29.
|
|
67
|
-
"@ocap/indexdb": "1.29.
|
|
68
|
-
"@ocap/types": "1.29.
|
|
69
|
-
"@ocap/util": "1.29.
|
|
66
|
+
"@arcblock/did": "1.29.11",
|
|
67
|
+
"@ocap/indexdb": "1.29.11",
|
|
68
|
+
"@ocap/types": "1.29.11",
|
|
69
|
+
"@ocap/util": "1.29.11",
|
|
70
70
|
"debug": "^4.4.3",
|
|
71
71
|
"better-sqlite3": "^11.8.1",
|
|
72
72
|
"kysely": "^0.27.0",
|