-
Notifications
You must be signed in to change notification settings - Fork 12
Convert indexer tables to TimescaleDB hypertables #486
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
00f02ad
266433a
2f1c2ae
b153a37
325becd
6a46a31
3c0acf5
8bfdebc
b8cdb5d
3027c5a
625b900
e48c293
409835c
13f2368
f712167
1ab6a1e
5ba65c2
83e05e5
72ce607
7e3cd12
28e8ed1
a5c7b65
b834e24
5a953de
b07c821
72ac8b0
31d9c57
f39aafc
1e4d338
573cef0
7302397
116b18d
e0a9aa9
bb23e58
6311a01
10cc35b
e5fc412
2fcec72
47b76f9
b42a661
da23caa
4e4d47b
6998704
96ededa
5a9a4c7
99d4369
7c7a22a
4e502d9
a6e7c29
4686772
db01b6d
013bccc
e65e68c
dc00ea3
cb3a14e
0213054
803e86d
17f3536
2ac4a85
4bfe8b4
7763eb7
a5fdd2d
b608656
e8e1ae9
039dc35
cf8e1c0
beb871e
e873498
18589ee
a6f6ac3
0b47039
c97772a
92b65a0
7795e37
c358be6
ffb8ade
0b33e31
3b8f21c
3159523
dca7e9a
30b2104
57fe265
b14f72a
8e81d18
6a3a3d3
e7bb990
0ff1c87
10dd1e5
ebb008b
c06fe20
b3f22df
cef0fa7
cfb2c82
633341c
6c44225
2d0ebf0
4531c59
6e0c696
120b252
fe40b54
dee8de5
12b192b
a5df424
f8aca24
da4c44e
f23fd75
096a08e
705d2a4
dacec16
8487254
fa60b23
ac3b07e
331461b
01b06d3
9021603
db17bf3
ba42d49
9c1e726
5a10c3b
e209dc5
c6c22e9
3bbc0a7
e4199c3
4677df9
af12032
39e24ec
6fb0b76
7c5089e
4ad1ede
4d825e1
c6682d5
6f73fdf
e743465
5d16532
1ad897d
caacd85
a041358
4c1e5dc
09348d9
d238b0b
d6526ef
6ff993f
5058261
6477fa9
1829d3e
c844520
b5ac8ad
c23554e
23ba111
a347cc7
ddaeeee
aee79f2
00eeaa3
ec34cef
b58b994
525a59d
5e221f7
ce12f8a
52bdb28
5bb41f3
a5429a0
41dfbf7
bd27099
762593e
5f26b91
25ccec9
b84442d
39b68c5
3121832
52ee484
b2d0a81
15b03df
979d422
349143c
d20dcdc
3051d59
5a0f134
67232ae
6c80306
2a6b96b
ec94e45
18d96ca
e9c4884
01f97ac
0728469
ed35eaa
36976b1
85c6dac
89c1639
7dad1fb
f30b61a
0e8a0f5
6548f2b
e7abf42
351ce26
378091f
b1d069d
4eb8035
c1f6c5d
33e1cee
e647156
f94310d
2f6d95f
056b1d8
9b4cc4f
381a586
f06b52e
fe8828c
18b7767
e0dafc2
917b32f
d05fc9b
9d2536d
6bf87f5
e9637a7
beeab09
63a4007
6b261a8
8e5ed67
83c7b35
159d476
86b4fe8
367bd89
a19ea7b
2ea2f0e
3bbb050
87a6c59
87dd75c
14c1559
24c5143
3799f62
034b6a8
75e30e7
52f84b3
e41dcfd
c44edcf
ba3ef60
05ae454
e253998
131f3b1
ce16112
8104698
b2d4f45
869d762
eb0522b
2f1b518
aa3801f
ab7985b
80a5e88
c3f871f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,3 +9,4 @@ __debug_bin* | |
| *.out | ||
| CLAUDE.md | ||
| .claude/ | ||
| .serena | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -269,137 +269,13 @@ func (m *OperationModel) BatchGetByStateChangeIDs(ctx context.Context, scToIDs [ | |
| return operationsWithStateChanges, nil | ||
| } | ||
|
|
||
| // BatchInsert inserts the operations and the operations_accounts links. | ||
| // It returns the IDs of the successfully inserted operations. | ||
| func (m *OperationModel) BatchInsert( | ||
| ctx context.Context, | ||
| sqlExecuter db.SQLExecuter, | ||
| operations []*types.Operation, | ||
| stellarAddressesByOpID map[int64]set.Set[string], | ||
| ) ([]int64, error) { | ||
| if sqlExecuter == nil { | ||
| sqlExecuter = m.DB | ||
| } | ||
|
|
||
| // 1. Flatten the operations into parallel slices | ||
| ids := make([]int64, len(operations)) | ||
| operationTypes := make([]string, len(operations)) | ||
| operationXDRs := make([][]byte, len(operations)) | ||
| resultCodes := make([]string, len(operations)) | ||
| successfulFlags := make([]bool, len(operations)) | ||
| ledgerNumbers := make([]uint32, len(operations)) | ||
| ledgerCreatedAts := make([]time.Time, len(operations)) | ||
|
|
||
| for i, op := range operations { | ||
| ids[i] = op.ID | ||
| operationTypes[i] = string(op.OperationType) | ||
| operationXDRs[i] = []byte(op.OperationXDR) | ||
| resultCodes[i] = op.ResultCode | ||
| successfulFlags[i] = op.Successful | ||
| ledgerNumbers[i] = op.LedgerNumber | ||
| ledgerCreatedAts[i] = op.LedgerCreatedAt | ||
| } | ||
|
|
||
| // 2. Flatten the stellarAddressesByOpID into parallel slices, converting to BYTEA | ||
| var opIDs []int64 | ||
| var stellarAddressBytes [][]byte | ||
| for opID, addresses := range stellarAddressesByOpID { | ||
| for address := range addresses.Iter() { | ||
| opIDs = append(opIDs, opID) | ||
| addrBytesValue, err := types.AddressBytea(address).Value() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("converting address %s to bytes: %w", address, err) | ||
| } | ||
| addrBytes, ok := addrBytesValue.([]byte) | ||
| if !ok || addrBytes == nil { | ||
| return nil, fmt.Errorf("converting address %s to bytes: unexpected value %T", address, addrBytesValue) | ||
| } | ||
| stellarAddressBytes = append(stellarAddressBytes, addrBytes) | ||
| } | ||
| } | ||
|
|
||
| // Insert operations and operations_accounts links. | ||
| const insertQuery = ` | ||
| WITH | ||
| -- Insert operations | ||
| inserted_operations AS ( | ||
| INSERT INTO operations | ||
| (id, operation_type, operation_xdr, result_code, successful, ledger_number, ledger_created_at) | ||
| SELECT | ||
| o.id, o.operation_type, o.operation_xdr, o.result_code, o.successful, o.ledger_number, o.ledger_created_at | ||
| FROM ( | ||
| SELECT | ||
| UNNEST($1::bigint[]) AS id, | ||
| UNNEST($2::text[]) AS operation_type, | ||
| UNNEST($3::bytea[]) AS operation_xdr, | ||
| UNNEST($4::text[]) AS result_code, | ||
| UNNEST($5::boolean[]) AS successful, | ||
| UNNEST($6::bigint[]) AS ledger_number, | ||
| UNNEST($7::timestamptz[]) AS ledger_created_at | ||
| ) o | ||
| ON CONFLICT (id) DO NOTHING | ||
| RETURNING id | ||
| ), | ||
|
|
||
| -- Insert operations_accounts links | ||
| inserted_operations_accounts AS ( | ||
| INSERT INTO operations_accounts | ||
| (operation_id, account_id) | ||
| SELECT | ||
| oa.op_id, oa.account_id | ||
| FROM ( | ||
| SELECT | ||
| UNNEST($8::bigint[]) AS op_id, | ||
| UNNEST($9::bytea[]) AS account_id | ||
| ) oa | ||
| ON CONFLICT DO NOTHING | ||
| ) | ||
|
|
||
| -- Return the IDs of successfully inserted operations | ||
| SELECT id FROM inserted_operations; | ||
| ` | ||
|
|
||
| start := time.Now() | ||
| var insertedIDs []int64 | ||
| err := sqlExecuter.SelectContext(ctx, &insertedIDs, insertQuery, | ||
| pq.Array(ids), | ||
| pq.Array(operationTypes), | ||
| pq.Array(operationXDRs), | ||
| pq.Array(resultCodes), | ||
| pq.Array(successfulFlags), | ||
| pq.Array(ledgerNumbers), | ||
| pq.Array(ledgerCreatedAts), | ||
| pq.Array(opIDs), | ||
| pq.Array(stellarAddressBytes), | ||
| ) | ||
| duration := time.Since(start).Seconds() | ||
| for _, dbTableName := range []string{"operations", "operations_accounts"} { | ||
| m.MetricsService.ObserveDBQueryDuration("BatchInsert", dbTableName, duration) | ||
| if dbTableName == "operations" { | ||
| m.MetricsService.ObserveDBBatchSize("BatchInsert", dbTableName, len(operations)) | ||
| } | ||
| if err == nil { | ||
| m.MetricsService.IncDBQuery("BatchInsert", dbTableName) | ||
| } | ||
| } | ||
| if err != nil { | ||
| for _, dbTableName := range []string{"operations", "operations_accounts"} { | ||
| m.MetricsService.IncDBQueryError("BatchInsert", dbTableName, utils.GetDBErrorType(err)) | ||
| } | ||
| return nil, fmt.Errorf("batch inserting operations and operations_accounts: %w", err) | ||
| } | ||
|
|
||
| return insertedIDs, nil | ||
| } | ||
|
|
||
| // BatchCopy inserts operations using pgx's binary COPY protocol. | ||
| // Uses pgx.Tx for binary format which is faster than lib/pq's text format. | ||
| // Uses native pgtype types for optimal performance (see https://github.com/jackc/pgx/issues/763). | ||
| // | ||
| // IMPORTANT: Unlike BatchInsert which uses ON CONFLICT DO NOTHING, BatchCopy will FAIL | ||
| // if any duplicate records exist. The PostgreSQL COPY protocol does not support conflict | ||
| // handling. Callers must ensure no duplicates exist before calling this method, or handle | ||
| // the unique constraint violation error appropriately. | ||
| // IMPORTANT: BatchCopy will FAIL if any duplicate records exist. The PostgreSQL COPY | ||
| // protocol does not support conflict handling. Callers must ensure no duplicates exist | ||
| // before calling this method, or handle the unique constraint violation error appropriately. | ||
| func (m *OperationModel) BatchCopy( | ||
| ctx context.Context, | ||
| pgxTx pgx.Tx, | ||
|
|
@@ -440,23 +316,34 @@ func (m *OperationModel) BatchCopy( | |
|
|
||
| // COPY operations_accounts using pgx binary format with native pgtype types | ||
| if len(stellarAddressesByOpID) > 0 { | ||
| // Build OpID -> LedgerCreatedAt lookup from operations | ||
| ledgerCreatedAtByOpID := make(map[int64]time.Time, len(operations)) | ||
| for _, op := range operations { | ||
| ledgerCreatedAtByOpID[op.ID] = op.LedgerCreatedAt | ||
| } | ||
|
|
||
| var oaRows [][]any | ||
| for opID, addresses := range stellarAddressesByOpID { | ||
| ledgerCreatedAt := ledgerCreatedAtByOpID[opID] | ||
| ledgerCreatedAtPgtype := pgtype.Timestamptz{Time: ledgerCreatedAt, Valid: true} | ||
| opIDPgtype := pgtype.Int8{Int64: opID, Valid: true} | ||
| for _, addr := range addresses.ToSlice() { | ||
| var addrBytes any | ||
| addrBytes, err = types.AddressBytea(addr).Value() | ||
| if err != nil { | ||
| return 0, fmt.Errorf("converting address %s to bytes: %w", addr, err) | ||
| addrBytes, addrErr := types.AddressBytea(addr).Value() | ||
| if addrErr != nil { | ||
| return 0, fmt.Errorf("converting address %s to bytes: %w", addr, addrErr) | ||
| } | ||
| oaRows = append(oaRows, []any{opIDPgtype, addrBytes}) | ||
| oaRows = append(oaRows, []any{ | ||
| ledgerCreatedAtPgtype, | ||
| opIDPgtype, | ||
| addrBytes, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
In practice this likely can't happen today: addresses come from Either a nil guard or a brief comment noting the upstream guarantee would close this off. 🤖 Generated with Claude Code |
||
| }) | ||
| } | ||
| } | ||
|
|
||
| _, err = pgxTx.CopyFrom( | ||
| ctx, | ||
| pgx.Identifier{"operations_accounts"}, | ||
| []string{"operation_id", "account_id"}, | ||
| []string{"ledger_created_at", "operation_id", "account_id"}, | ||
| pgx.CopyFromRows(oaRows), | ||
| ) | ||
| if err != nil { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.