Skip to content

Commit 9815a0a

Browse files
authored
Merge pull request #520 from hack-a-chain-software/code-pg
Improvements changes
2 parents 7a6031a + 1cab901 commit 9815a0a

15 files changed

+405
-279
lines changed

backfill/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ FROM golang:1.23.3 AS builder
22
WORKDIR /app
33
COPY . .
44
RUN go mod download
5-
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o main .
5+
RUN CGO_ENABLED=0 GOOS=linux go build -o main .
66

77
FROM scratch
88
WORKDIR /app

backfill/Dockerfile.migrator

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ FROM golang:1.23.3 AS builder
22
WORKDIR /app
33
COPY . .
44
RUN go mod download
5-
RUN CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o db-migrator ./db-migrator/db-migrator.go
5+
RUN CGO_ENABLED=0 GOOS=linux go build -o db-migrator ./db-migrator/main.go
66

77
FROM scratch
88
WORKDIR /app
99
COPY ./global-bundle.pem ./global-bundle.pem
1010
COPY --from=builder /app/db-migrator .
11-
CMD ["./db-migrator"]
11+
ENTRYPOINT ["./db-migrator"]

backfill/db-migrator/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# DB Migrator
2+
3+
This tool provides various database migration utilities for the Kadena indexer.
4+
5+
Use this only in case you already running a indexer solution and some breaking changes were introduced in the changelog.
6+
7+
## Available Commands
8+
9+
The migrator supports the following commands:
10+
11+
- `code-to-text`: Convert code fields to text type
12+
- `creation-time`: Add creation time to events and transfers
13+
- `reconcile`: Run process to insert transfers through the reconcile event
14+
15+
## Usage
16+
17+
### Local Development
18+
19+
To run a migration locally:
20+
21+
```bash
22+
go run ./db-migrator/*.go -command=<command_name> -env=.env
23+
```
24+
25+
For example:
26+
27+
```bash
28+
cd backfill/
29+
go run ./db-migrator/*.go -command=code-to-text -env=.env
30+
```
31+
32+
### Using Docker
33+
34+
Build the image:
35+
36+
```bash
37+
cd backfill/
38+
docker build -f Dockerfile.migrator -t db-image .
39+
```
40+
41+
Run a specific migration:
42+
43+
```bash
44+
docker run -it --rm -env=.env db-image -command=code-to-text
45+
```
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
package main
2+
3+
import (
4+
"database/sql"
5+
"fmt"
6+
"go-backfill/config"
7+
"log"
8+
9+
_ "github.com/lib/pq" // PostgreSQL driver
10+
)
11+
12+
const (
13+
codeBatchSize = 500
14+
startTransactionIdForCode = 1
15+
)
16+
17+
// This script was created to convert the code column in the TransactionDetails table to text.
18+
// Use it ONLY if the migration 20251010161634-change-code-column-type-in-transactiondetails doesn't work
19+
// properly due lack of memory in the machine.
20+
21+
func updateCodeToText() error {
22+
env := config.GetConfig()
23+
connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
24+
env.DbHost, env.DbPort, env.DbUser, env.DbPassword, env.DbName)
25+
26+
db, err := sql.Open("postgres", connStr)
27+
if err != nil {
28+
return fmt.Errorf("failed to connect to database: %v", err)
29+
}
30+
defer db.Close()
31+
32+
log.Println("Connected to database")
33+
34+
// Test database connection
35+
if err := db.Ping(); err != nil {
36+
return fmt.Errorf("failed to ping database: %v", err)
37+
}
38+
39+
// Create codetext column if it doesn't exist
40+
_, err = db.Exec(`
41+
ALTER TABLE "TransactionDetails"
42+
ADD COLUMN IF NOT EXISTS codetext TEXT
43+
`)
44+
if err != nil {
45+
return fmt.Errorf("failed to create codetext column: %v", err)
46+
}
47+
48+
// Get max transaction ID to determine processing range
49+
var maxTransactionID int
50+
if err := db.QueryRow(`SELECT COALESCE(MAX(id), 0) FROM "TransactionDetails"`).Scan(&maxTransactionID); err != nil {
51+
return fmt.Errorf("failed to get max transaction ID: %v", err)
52+
}
53+
54+
if maxTransactionID == 0 {
55+
log.Println("No transaction details found; nothing to update")
56+
return nil
57+
}
58+
59+
// Process transactions in batches
60+
if err := processTransactionsBatchForCode(db, startTransactionIdForCode, maxTransactionID); err != nil {
61+
return fmt.Errorf("failed to process transactions: %v", err)
62+
}
63+
64+
// Drop code column
65+
_, err = db.Exec(`
66+
ALTER TABLE "TransactionDetails"
67+
DROP COLUMN IF EXISTS code
68+
`)
69+
if err != nil {
70+
return fmt.Errorf("failed to drop code column: %v", err)
71+
}
72+
73+
// Rename codetext column to code
74+
_, err = db.Exec(`
75+
ALTER TABLE "TransactionDetails"
76+
RENAME COLUMN codetext TO code
77+
`)
78+
if err != nil {
79+
return fmt.Errorf("failed to rename codetext column: %v", err)
80+
}
81+
82+
log.Println("Successfully updated all TransactionDetails code values to text")
83+
log.Printf("Max(TransactionDetails.id) processed: %d", maxTransactionID)
84+
return nil
85+
}
86+
87+
func processTransactionsBatchForCode(db *sql.DB, startId, endId int) error {
88+
currentMaxId := endId
89+
totalProcessed := 0
90+
totalTransactions := endId - startId + 1
91+
lastProgressPrinted := -1.0
92+
93+
log.Printf("Starting to process transactions from ID %d down to %d", endId, startId)
94+
log.Printf("Total transactions to process: %d", totalTransactions)
95+
96+
for currentMaxId >= startId {
97+
// Calculate this batch's lower bound (inclusive)
98+
batchMinId := currentMaxId - codeBatchSize + 1
99+
if batchMinId < startId {
100+
batchMinId = startId
101+
}
102+
103+
// Process this batch [batchMinId, currentMaxId]
104+
processed, err := processBatchForCode(db, batchMinId, currentMaxId)
105+
if err != nil {
106+
return fmt.Errorf("failed to process batch %d-%d: %v", batchMinId, currentMaxId, err)
107+
}
108+
109+
totalProcessed += processed
110+
111+
// Move to next window (just below the batch we processed)
112+
currentMaxId = batchMinId - 1
113+
114+
// Calculate progress percentage based on covered ID space
115+
processedSpan := endId - currentMaxId // how many IDs from the top have been covered
116+
if processedSpan > totalTransactions {
117+
processedSpan = totalTransactions
118+
}
119+
progressPercent := (float64(processedSpan) / float64(totalTransactions)) * 100.0
120+
121+
// Only print progress if it has increased by at least 0.1%
122+
if progressPercent-lastProgressPrinted >= 0.1 {
123+
log.Printf("Progress: %.1f%%, currentMaxId: %d", progressPercent, currentMaxId)
124+
lastProgressPrinted = progressPercent
125+
}
126+
}
127+
128+
log.Printf("Completed processing. Total TransactionDetails updated: %d (100.0%%)", totalProcessed)
129+
return nil
130+
}
131+
132+
func processBatchForCode(db *sql.DB, startId, endId int) (int, error) {
133+
// Begin transaction for atomic operation
134+
tx, err := db.Begin()
135+
if err != nil {
136+
return 0, fmt.Errorf("failed to begin transaction: %v", err)
137+
}
138+
defer tx.Rollback() // Will be ignored if tx.Commit() succeeds
139+
140+
// Get all records in this batch and validate them
141+
rows, err := tx.Query(`
142+
SELECT id, code
143+
FROM "TransactionDetails"
144+
WHERE id >= $1 AND id <= $2
145+
ORDER BY id DESC
146+
`, startId, endId)
147+
if err != nil {
148+
log.Fatalf("Failed to query records: %v", err)
149+
}
150+
defer rows.Close()
151+
152+
// Check each record in the batch
153+
for rows.Next() {
154+
var (
155+
id int
156+
code []byte
157+
)
158+
if err := rows.Scan(&id, &code); err != nil {
159+
log.Fatalf("Failed to scan record: %v", err)
160+
}
161+
162+
// Skip NULL values
163+
if code == nil {
164+
continue
165+
}
166+
167+
// Check if it's a string or {}
168+
isString := false
169+
isEmptyObject := string(code) == "{}"
170+
171+
if !isEmptyObject {
172+
// If it's not {}, check if it's a string
173+
isString = string(code)[0] == '"' && string(code)[len(string(code))-1] == '"'
174+
}
175+
176+
// If neither string nor {}, abort
177+
if !isString && !isEmptyObject {
178+
log.Fatalf("ABORTING: Found invalid code value at id %d", id)
179+
}
180+
}
181+
if err := rows.Err(); err != nil {
182+
log.Fatalf("Error iterating records: %v", err)
183+
}
184+
rows.Close()
185+
186+
// If we get here, all values in this batch are valid (string or {})
187+
log.Printf("About to update batch: startId=%d, endId=%d", startId, endId)
188+
189+
updateQuery := `
190+
UPDATE "TransactionDetails"
191+
SET codetext = CASE
192+
WHEN code IS NULL OR code = '{}'::jsonb THEN NULL
193+
ELSE code #>> '{}'
194+
END
195+
WHERE id >= $1 AND id <= $2
196+
RETURNING id
197+
`
198+
199+
updateRows, err := tx.Query(updateQuery, startId, endId)
200+
if err != nil {
201+
log.Fatalf("Failed to update records: %v", err)
202+
}
203+
defer updateRows.Close()
204+
205+
var processed int
206+
for updateRows.Next() {
207+
processed++
208+
}
209+
210+
log.Printf("Processed %d records in this batch", processed)
211+
212+
if err := updateRows.Err(); err != nil {
213+
log.Fatalf("Error iterating update rows: %v", err)
214+
}
215+
216+
// Commit the transaction
217+
if err := tx.Commit(); err != nil {
218+
log.Fatalf("Failed to commit transaction: %v", err)
219+
}
220+
221+
return processed, nil
222+
}
223+
224+
func CodeToText() {
225+
if err := updateCodeToText(); err != nil {
226+
log.Fatalf("Error: %v", err)
227+
}
228+
}

0 commit comments

Comments
 (0)