Migrate to postgres
This commit is contained in:
+266
-212
@@ -1,6 +1,9 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"pool-stats/helpers"
|
||||
@@ -8,10 +11,8 @@ import (
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/ostafen/clover/v2"
|
||||
"github.com/ostafen/clover/v2/document"
|
||||
c "github.com/ostafen/clover/v2/query"
|
||||
badgerstore "github.com/ostafen/clover/v2/store/badger"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,258 +22,283 @@ const (
|
||||
DailyStatsCollectionName = "DailyStats"
|
||||
)
|
||||
|
||||
func InitDatabase(path string) (*clover.DB, error) {
|
||||
store, err := badgerstore.Open(path)
|
||||
//go:embed schema.sql
|
||||
var schemaSQL string
|
||||
|
||||
func InitDatabase(dsn string) (*pgxpool.Pool, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
pool, err := pgxpool.New(ctx, dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open BadgerDB store: %v", err)
|
||||
return nil, fmt.Errorf("failed to connect to PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
db, err := clover.OpenWithStore(store)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open CloverDB: %v", err)
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("failed to ping PostgreSQL: %w", err)
|
||||
}
|
||||
|
||||
// Ensure collection exists
|
||||
hasCollection, err := db.HasCollection(CollectionName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check collection: %v", err)
|
||||
if _, err := pool.Exec(ctx, schemaSQL); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("failed to apply schema: %w", err)
|
||||
}
|
||||
|
||||
if !hasCollection {
|
||||
if err := db.CreateCollection(CollectionName); err != nil {
|
||||
return nil, fmt.Errorf("failed to create collection: %v", err)
|
||||
}
|
||||
if err := db.CreateIndex(CollectionName, "CreateDate"); err != nil {
|
||||
return nil, fmt.Errorf("failed to create index: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Init TopShares collection
|
||||
hasTopSharesCollection, err := db.HasCollection(TopSharesCollectionName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check TopShares collection: %v", err)
|
||||
}
|
||||
|
||||
if !hasTopSharesCollection {
|
||||
if err := db.CreateCollection(TopSharesCollectionName); err != nil {
|
||||
return nil, fmt.Errorf("failed to create TopShares collection: %v", err)
|
||||
}
|
||||
|
||||
if err := db.CreateIndex(TopSharesCollectionName, "CreateDate"); err != nil {
|
||||
return nil, fmt.Errorf("failed to create index for TopShares: %v", err)
|
||||
}
|
||||
|
||||
if err := db.CreateIndex(TopSharesCollectionName, "SDiff"); err != nil {
|
||||
return nil, fmt.Errorf("failed to create index for TopShares SDiff: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Init TimeWindowHighShareStat collection
|
||||
hasTimeWindowCollection, err := db.HasCollection(TimeWindowHighShareCollectionName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check TimeWindowHighShare collection: %v", err)
|
||||
}
|
||||
|
||||
if !hasTimeWindowCollection {
|
||||
if err := db.CreateCollection(TimeWindowHighShareCollectionName); err != nil {
|
||||
return nil, fmt.Errorf("failed to create TimeWindowHighShare collection: %v", err)
|
||||
}
|
||||
if err := db.CreateIndex(TimeWindowHighShareCollectionName, "TimeWindowID"); err != nil {
|
||||
return nil, fmt.Errorf("failed to create index for TimeWindowHighShare: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Init DailyStats collection
|
||||
hasDailyStatsCollection, err := db.HasCollection(DailyStatsCollectionName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check DailyStats collection: %v", err)
|
||||
}
|
||||
if !hasDailyStatsCollection {
|
||||
if err := db.CreateCollection(DailyStatsCollectionName); err != nil {
|
||||
return nil, fmt.Errorf("failed to create DailyStats collection: %v", err)
|
||||
}
|
||||
if err := db.CreateIndex(DailyStatsCollectionName, "Date"); err != nil {
|
||||
return nil, fmt.Errorf("failed to create index for DailyStats: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return db, nil
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func GetHighestSharesInRange(db *clover.DB, collection string, since time.Time, count int) ([]models.ShareLog, error) {
|
||||
// Convert `since` to the format in `createdate`
|
||||
lower := since.Unix()
|
||||
upper := time.Now().Unix()
|
||||
func shareColumns() string {
|
||||
return `work_info_id, client_id, enonce1, nonce2, nonce, ntime,
|
||||
diff, sdiff, hash, result, errn,
|
||||
create_date, create_ts, create_by, create_code, create_inet,
|
||||
worker_name, username, address, agent`
|
||||
}
|
||||
|
||||
// Filter by timestamp range
|
||||
criteria := c.Field("CreateDate").GtEq(fmt.Sprint(lower)).
|
||||
And(c.Field("CreateDate").LtEq(fmt.Sprint(upper)))
|
||||
|
||||
// Query sorted by "sdiff" descending, limit 1
|
||||
results, err := db.FindAll(c.NewQuery(collection).
|
||||
Where(criteria).
|
||||
Sort(c.SortOption{Field: "SDiff", Direction: -1}).
|
||||
Limit(count))
|
||||
|
||||
if err != nil || len(results) == 0 {
|
||||
return nil, err
|
||||
}
|
||||
func scanShare(row pgx.Row) (models.ShareLog, error) {
|
||||
var s models.ShareLog
|
||||
var createTs time.Time
|
||||
err := row.Scan(
|
||||
&s.WorkInfoID, &s.ClientID, &s.Enonce1, &s.Nonce2, &s.Nonce, &s.NTime,
|
||||
&s.Diff, &s.SDiff, &s.Hash, &s.Result, &s.Errn,
|
||||
&s.CreateDate, &createTs, &s.CreateBy, &s.CreateCode, &s.CreateInet,
|
||||
&s.WorkerName, &s.Username, &s.Address, &s.Agent,
|
||||
)
|
||||
return s, err
|
||||
}
|
||||
|
||||
func scanShares(rows pgx.Rows) ([]models.ShareLog, error) {
|
||||
defer rows.Close()
|
||||
var shares []models.ShareLog
|
||||
for _, doc := range results {
|
||||
for rows.Next() {
|
||||
var s models.ShareLog
|
||||
if err := doc.Unmarshal(&s); err != nil {
|
||||
var createTs time.Time
|
||||
if err := rows.Scan(
|
||||
&s.WorkInfoID, &s.ClientID, &s.Enonce1, &s.Nonce2, &s.Nonce, &s.NTime,
|
||||
&s.Diff, &s.SDiff, &s.Hash, &s.Result, &s.Errn,
|
||||
&s.CreateDate, &createTs, &s.CreateBy, &s.CreateCode, &s.CreateInet,
|
||||
&s.WorkerName, &s.Username, &s.Address, &s.Agent,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
shares = append(shares, s)
|
||||
}
|
||||
|
||||
return shares, nil
|
||||
return shares, rows.Err()
|
||||
}
|
||||
|
||||
func PrintAllHashes(db *clover.DB) {
|
||||
docs, err := db.FindAll(c.NewQuery(CollectionName))
|
||||
func shareValues(s models.ShareLog) ([]any, error) {
|
||||
createTs, err := s.ParseCreateDate()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read from collection: %v", err)
|
||||
createTs = helpers.ParseCreateDate(s.CreateDate)
|
||||
}
|
||||
if createTs.IsZero() {
|
||||
createTs = time.Unix(0, 0)
|
||||
}
|
||||
return []any{
|
||||
s.WorkInfoID, s.ClientID, s.Enonce1, s.Nonce2, s.Nonce, s.NTime,
|
||||
s.Diff, s.SDiff, s.Hash, s.Result, s.Errn,
|
||||
s.CreateDate, createTs, s.CreateBy, s.CreateCode, s.CreateInet,
|
||||
s.WorkerName, s.Username, s.Address, s.Agent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
for _, doc := range docs {
|
||||
hash := doc.Get("Hash")
|
||||
fmt.Println(hash)
|
||||
func tableForCollection(collection string) string {
|
||||
switch collection {
|
||||
case TopSharesCollectionName:
|
||||
return "top_shares"
|
||||
default:
|
||||
return "shares"
|
||||
}
|
||||
}
|
||||
|
||||
func ListShares(db *clover.DB, offset int, count int) []models.ShareLog {
|
||||
results, err := db.FindAll(
|
||||
c.NewQuery(CollectionName).
|
||||
Sort(c.SortOption{Field: "CreateDate", Direction: -1}).
|
||||
Skip(offset).
|
||||
Limit(count),
|
||||
)
|
||||
func InsertShare(db *pgxpool.Pool, share models.ShareLog) error {
|
||||
ctx := context.Background()
|
||||
vals, err := shareValues(share)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = db.Exec(ctx, `
|
||||
INSERT INTO shares (`+shareColumns()+`)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20)`,
|
||||
vals...)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetHighestSharesInRange(db *pgxpool.Pool, collection string, since time.Time, count int) ([]models.ShareLog, error) {
|
||||
ctx := context.Background()
|
||||
table := tableForCollection(collection)
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT `+shareColumns()+`
|
||||
FROM `+table+`
|
||||
WHERE create_ts >= $1 AND create_ts <= $2
|
||||
ORDER BY sdiff DESC
|
||||
LIMIT $3`,
|
||||
since, time.Now(), count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scanShares(rows)
|
||||
}
|
||||
|
||||
func ListShares(db *pgxpool.Pool, offset int, count int) []models.ShareLog {
|
||||
ctx := context.Background()
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT `+shareColumns()+`
|
||||
FROM shares
|
||||
ORDER BY create_ts DESC
|
||||
OFFSET $1 LIMIT $2`,
|
||||
offset, count)
|
||||
if err != nil {
|
||||
log.Printf("failed to list shares: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
shareLogs := make([]models.ShareLog, len(results))
|
||||
for idx, doc := range results {
|
||||
var shareLog models.ShareLog
|
||||
doc.Unmarshal(&shareLog)
|
||||
shareLogs[idx] = shareLog
|
||||
shares, err := scanShares(rows)
|
||||
if err != nil {
|
||||
log.Printf("failed to scan shares: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return shareLogs
|
||||
return shares
|
||||
}
|
||||
|
||||
func ListTopShares(db *clover.DB) []models.ShareLog {
|
||||
results, err := db.FindAll(
|
||||
c.NewQuery(TopSharesCollectionName).
|
||||
Sort(c.SortOption{Field: "SDiff", Direction: -1}),
|
||||
)
|
||||
func ListTopShares(db *pgxpool.Pool) []models.ShareLog {
|
||||
ctx := context.Background()
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT `+shareColumns()+`
|
||||
FROM top_shares
|
||||
ORDER BY sdiff DESC`)
|
||||
if err != nil {
|
||||
log.Printf("failed to list top shares: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
topShares := make([]models.ShareLog, len(results))
|
||||
for idx, doc := range results {
|
||||
var shareLog models.ShareLog
|
||||
doc.Unmarshal(&shareLog)
|
||||
topShares[idx] = shareLog
|
||||
shares, err := scanShares(rows)
|
||||
if err != nil {
|
||||
log.Printf("failed to scan top shares: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return topShares
|
||||
return shares
|
||||
}
|
||||
|
||||
func ReplaceTopShares(db *clover.DB, shares []models.ShareLog) {
|
||||
db.Delete(c.NewQuery(TopSharesCollectionName))
|
||||
func ReplaceTopShares(db *pgxpool.Pool, shares []models.ShareLog) {
|
||||
ctx := context.Background()
|
||||
tx, err := db.Begin(ctx)
|
||||
if err != nil {
|
||||
log.Printf("failed to begin replace top shares: %v", err)
|
||||
return
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM top_shares`); err != nil {
|
||||
log.Printf("failed to clear top shares: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, share := range shares {
|
||||
doc := document.NewDocumentOf(&share)
|
||||
if _, err := db.InsertOne(TopSharesCollectionName, doc); err != nil {
|
||||
vals, err := shareValues(share)
|
||||
if err != nil {
|
||||
log.Printf("failed to prepare top share: %v", err)
|
||||
return
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO top_shares (`+shareColumns()+`)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20)`,
|
||||
vals...); err != nil {
|
||||
log.Printf("failed to insert top share: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
log.Printf("failed to commit top shares: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetTimeWindowHighShares(db *clover.DB) []models.TimeWindowHighShare {
|
||||
results, err := db.FindAll(
|
||||
c.NewQuery(TimeWindowHighShareCollectionName).
|
||||
Sort(c.SortOption{Field: "TimeWindowID", Direction: 1}),
|
||||
)
|
||||
func GetTimeWindowHighShares(db *pgxpool.Pool) []models.TimeWindowHighShare {
|
||||
ctx := context.Background()
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT time_window_id, time_window_name, sdiff, time
|
||||
FROM time_window_high_shares
|
||||
ORDER BY time_window_id ASC`)
|
||||
if err != nil {
|
||||
log.Printf("failed to list time window high shares: %v", err)
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
timeWindowHighShares := make([]models.TimeWindowHighShare, len(results))
|
||||
for idx, doc := range results {
|
||||
var timeWindowHighShare models.TimeWindowHighShare
|
||||
doc.Unmarshal(&timeWindowHighShare)
|
||||
timeWindowHighShares[idx] = timeWindowHighShare
|
||||
var result []models.TimeWindowHighShare
|
||||
for rows.Next() {
|
||||
var tw models.TimeWindowHighShare
|
||||
if err := rows.Scan(&tw.TimeWindowID, &tw.TimeWindowName, &tw.SDiff, &tw.Time); err != nil {
|
||||
log.Printf("failed to scan time window high share: %v", err)
|
||||
return nil
|
||||
}
|
||||
result = append(result, tw)
|
||||
}
|
||||
|
||||
return timeWindowHighShares
|
||||
return result
|
||||
}
|
||||
|
||||
func SetTimeWindowHighShare(db *clover.DB, share models.TimeWindowHighShare) error {
|
||||
db.Delete(
|
||||
c.NewQuery(TimeWindowHighShareCollectionName).
|
||||
Where(c.Field("TimeWindowID").
|
||||
Eq(share.TimeWindowID)))
|
||||
|
||||
doc := document.NewDocumentOf(&share)
|
||||
db.InsertOne(TimeWindowHighShareCollectionName, doc)
|
||||
|
||||
return nil
|
||||
func SetTimeWindowHighShare(db *pgxpool.Pool, share models.TimeWindowHighShare) error {
|
||||
ctx := context.Background()
|
||||
_, err := db.Exec(ctx, `
|
||||
INSERT INTO time_window_high_shares (time_window_id, time_window_name, sdiff, time)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (time_window_id) DO UPDATE SET
|
||||
time_window_name = EXCLUDED.time_window_name,
|
||||
sdiff = EXCLUDED.sdiff,
|
||||
time = EXCLUDED.time`,
|
||||
share.TimeWindowID, share.TimeWindowName, share.SDiff, share.Time)
|
||||
return err
|
||||
}
|
||||
|
||||
func ListSharesInTimeRange(db *clover.DB, since time.Time, till time.Time) []models.ShareLog {
|
||||
lower := since.Unix()
|
||||
upper := till.Unix()
|
||||
|
||||
results, err := db.FindAll(c.NewQuery(CollectionName).
|
||||
Where(c.Field("CreateDate").GtEq(fmt.Sprint(lower)).
|
||||
And(c.Field("CreateDate").LtEq(fmt.Sprint(upper)))).
|
||||
Sort(c.SortOption{Field: "CreateDate", Direction: -1}))
|
||||
|
||||
func ListSharesInTimeRange(db *pgxpool.Pool, since time.Time, till time.Time) []models.ShareLog {
|
||||
ctx := context.Background()
|
||||
rows, err := db.Query(ctx, `
|
||||
SELECT `+shareColumns()+`
|
||||
FROM shares
|
||||
WHERE create_ts >= $1 AND create_ts <= $2
|
||||
ORDER BY create_ts DESC`,
|
||||
since, till)
|
||||
if err != nil {
|
||||
log.Printf("failed to list shares in time range: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
shareLogs := make([]models.ShareLog, len(results))
|
||||
for idx, doc := range results {
|
||||
var shareLog models.ShareLog
|
||||
doc.Unmarshal(&shareLog)
|
||||
shareLogs[idx] = shareLog
|
||||
shares, err := scanShares(rows)
|
||||
if err != nil {
|
||||
log.Printf("failed to scan shares in time range: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return shareLogs
|
||||
return shares
|
||||
}
|
||||
|
||||
// GetStatsForDay retrieves daily statistics for a given date
|
||||
// Tries to find from DailyStats collection, calculates on the fly if not found and stores
|
||||
func GetDailyStats(db *clover.DB, date time.Time) (*models.DailyStats, error) {
|
||||
func GetDailyStats(db *pgxpool.Pool, date time.Time) (*models.DailyStats, error) {
|
||||
ctx := context.Background()
|
||||
dateStr := date.Format(time.DateOnly)
|
||||
|
||||
// Check if stats already exist
|
||||
existingDoc, err := db.FindFirst(c.NewQuery(DailyStatsCollectionName).
|
||||
Where(c.Field("Date").Eq(dateStr)))
|
||||
if err == nil && existingDoc != nil {
|
||||
expiresAt := existingDoc.ExpiresAt()
|
||||
if expiresAt != nil && expiresAt.After(time.Now()) {
|
||||
DeleteDailyStatsForDay(db, date)
|
||||
} else {
|
||||
var stats models.DailyStats
|
||||
if err := existingDoc.Unmarshal(&stats); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal daily stats: %v", err)
|
||||
var (
|
||||
stats models.DailyStats
|
||||
topShare []byte
|
||||
workers []byte
|
||||
expiresAt *time.Time
|
||||
)
|
||||
err := db.QueryRow(ctx, `
|
||||
SELECT date::text, share_count, top_share, pool_hashrate, workers, expires_at
|
||||
FROM daily_stats
|
||||
WHERE date = $1::date`, dateStr).Scan(
|
||||
&stats.Date, &stats.ShareCount, &topShare, &stats.PoolHashrate, &workers, &expiresAt,
|
||||
)
|
||||
if err == nil {
|
||||
validCache := expiresAt == nil || expiresAt.After(time.Now())
|
||||
if validCache {
|
||||
if err := json.Unmarshal(topShare, &stats.TopShare); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal top share: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(workers, &stats.Workers); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal workers: %w", err)
|
||||
}
|
||||
return &stats, nil
|
||||
}
|
||||
_ = DeleteDailyStatsForDay(db, date)
|
||||
} else if err != pgx.ErrNoRows {
|
||||
return nil, fmt.Errorf("failed to query daily stats: %w", err)
|
||||
}
|
||||
|
||||
// Get shares in range
|
||||
since := date.Truncate(24 * time.Hour)
|
||||
till := since.Add(24 * time.Hour)
|
||||
shares := ListSharesInTimeRange(db, since, till)
|
||||
@@ -280,19 +306,17 @@ func GetDailyStats(db *clover.DB, date time.Time) (*models.DailyStats, error) {
|
||||
return shares[i].SDiff > shares[j].SDiff
|
||||
})
|
||||
|
||||
// Calculate daily stats
|
||||
stats := &models.DailyStats{
|
||||
computed := &models.DailyStats{
|
||||
Date: dateStr,
|
||||
ShareCount: len(shares),
|
||||
Workers: make(map[string]models.WorkerDailyStats),
|
||||
}
|
||||
|
||||
if len(shares) > 0 {
|
||||
stats.TopShare = shares[0]
|
||||
stats.PoolHashrate = helpers.CalculateAverageHashrate(shares)
|
||||
computed.TopShare = shares[0]
|
||||
computed.PoolHashrate = helpers.CalculateAverageHashrate(shares)
|
||||
}
|
||||
|
||||
// Calculate worker stats
|
||||
sharesByWorker := make(map[string][]models.ShareLog)
|
||||
for _, share := range shares {
|
||||
sharesByWorker[share.WorkerName] = append(sharesByWorker[share.WorkerName], share)
|
||||
@@ -302,46 +326,76 @@ func GetDailyStats(db *clover.DB, date time.Time) (*models.DailyStats, error) {
|
||||
sort.Slice(workerShares, func(i, j int) bool {
|
||||
return workerShares[i].SDiff > workerShares[j].SDiff
|
||||
})
|
||||
workerTopShare := workerShares[0] // Already sorted by SDiff
|
||||
|
||||
stats.Workers[workerName] = models.WorkerDailyStats{
|
||||
TopShare: workerTopShare,
|
||||
computed.Workers[workerName] = models.WorkerDailyStats{
|
||||
TopShare: workerShares[0],
|
||||
Hashrate: workerHashrate,
|
||||
Shares: len(workerShares),
|
||||
}
|
||||
}
|
||||
|
||||
// Insert or update the daily stats in the collection
|
||||
doc := document.NewDocumentOf(stats)
|
||||
topShareJSON, err := json.Marshal(computed.TopShare)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal top share: %w", err)
|
||||
}
|
||||
workersJSON, err := json.Marshal(computed.Workers)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal workers: %w", err)
|
||||
}
|
||||
|
||||
var expires any
|
||||
isToday := dateStr == time.Now().UTC().Format(time.DateOnly)
|
||||
if isToday {
|
||||
doc.SetExpiresAt(time.Now().Add(5 * time.Minute))
|
||||
expires = time.Now().Add(5 * time.Minute)
|
||||
}
|
||||
|
||||
if _, err := db.InsertOne(DailyStatsCollectionName, doc); err != nil {
|
||||
return nil, fmt.Errorf("failed to insert daily stats: %v", err)
|
||||
_, err = db.Exec(ctx, `
|
||||
INSERT INTO daily_stats (date, share_count, top_share, pool_hashrate, workers, expires_at)
|
||||
VALUES ($1::date, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (date) DO UPDATE SET
|
||||
share_count = EXCLUDED.share_count,
|
||||
top_share = EXCLUDED.top_share,
|
||||
pool_hashrate = EXCLUDED.pool_hashrate,
|
||||
workers = EXCLUDED.workers,
|
||||
expires_at = EXCLUDED.expires_at`,
|
||||
dateStr, computed.ShareCount, topShareJSON, computed.PoolHashrate, workersJSON, expires)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to insert daily stats: %w", err)
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
return computed, nil
|
||||
}
|
||||
|
||||
func ClearDailyStats(db *clover.DB) error {
|
||||
// Delete all documents in DailyStats collection
|
||||
if err := db.Delete(c.NewQuery(DailyStatsCollectionName)); err != nil {
|
||||
return fmt.Errorf("failed to clear DailyStats collection: %v", err)
|
||||
func ClearDailyStats(db *pgxpool.Pool) error {
|
||||
ctx := context.Background()
|
||||
if _, err := db.Exec(ctx, `DELETE FROM daily_stats`); err != nil {
|
||||
return fmt.Errorf("failed to clear DailyStats: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteDailyStatsForDay(db *clover.DB, date time.Time) error {
|
||||
func DeleteDailyStatsForDay(db *pgxpool.Pool, date time.Time) error {
|
||||
ctx := context.Background()
|
||||
dateStr := date.Format(time.DateOnly)
|
||||
|
||||
// Delete the document for the specific date
|
||||
if err := db.Delete(c.NewQuery(DailyStatsCollectionName).
|
||||
Where(c.Field("Date").Eq(dateStr))); err != nil {
|
||||
return fmt.Errorf("failed to delete daily stats for %s: %v", dateStr, err)
|
||||
if _, err := db.Exec(ctx, `DELETE FROM daily_stats WHERE date = $1::date`, dateStr); err != nil {
|
||||
return fmt.Errorf("failed to delete daily stats for %s: %w", dateStr, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ShareRowValues returns ordered column values for COPY/batch insert.
|
||||
func ShareRowValues(s models.ShareLog) ([]any, error) {
|
||||
return shareValues(s)
|
||||
}
|
||||
|
||||
// ApplySchema runs the embedded schema against an existing pool.
|
||||
func ApplySchema(db *pgxpool.Pool) error {
|
||||
_, err := db.Exec(context.Background(), schemaSQL)
|
||||
return err
|
||||
}
|
||||
|
||||
// CountShares returns the number of rows in shares.
|
||||
func CountShares(db *pgxpool.Pool) (int64, error) {
|
||||
var n int64
|
||||
err := db.QueryRow(context.Background(), `SELECT COUNT(*) FROM shares`).Scan(&n)
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
CREATE TABLE IF NOT EXISTS shares (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
work_info_id BIGINT NOT NULL,
|
||||
client_id INT NOT NULL,
|
||||
enonce1 TEXT,
|
||||
nonce2 TEXT,
|
||||
nonce TEXT,
|
||||
ntime TEXT,
|
||||
diff DOUBLE PRECISION NOT NULL,
|
||||
sdiff DOUBLE PRECISION NOT NULL,
|
||||
hash TEXT,
|
||||
result BOOLEAN,
|
||||
errn INT,
|
||||
create_date TEXT NOT NULL,
|
||||
create_ts TIMESTAMPTZ NOT NULL,
|
||||
create_by TEXT,
|
||||
create_code TEXT,
|
||||
create_inet TEXT,
|
||||
worker_name TEXT,
|
||||
username TEXT,
|
||||
address TEXT,
|
||||
agent TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS shares_create_ts_desc ON shares (create_ts DESC);
|
||||
CREATE INDEX IF NOT EXISTS shares_sdiff_desc ON shares (sdiff DESC);
|
||||
CREATE INDEX IF NOT EXISTS shares_create_ts_sdiff ON shares (create_ts, sdiff DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS top_shares (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
work_info_id BIGINT NOT NULL,
|
||||
client_id INT NOT NULL,
|
||||
enonce1 TEXT,
|
||||
nonce2 TEXT,
|
||||
nonce TEXT,
|
||||
ntime TEXT,
|
||||
diff DOUBLE PRECISION NOT NULL,
|
||||
sdiff DOUBLE PRECISION NOT NULL,
|
||||
hash TEXT,
|
||||
result BOOLEAN,
|
||||
errn INT,
|
||||
create_date TEXT NOT NULL,
|
||||
create_ts TIMESTAMPTZ NOT NULL,
|
||||
create_by TEXT,
|
||||
create_code TEXT,
|
||||
create_inet TEXT,
|
||||
worker_name TEXT,
|
||||
username TEXT,
|
||||
address TEXT,
|
||||
agent TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS top_shares_sdiff_desc ON top_shares (sdiff DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS time_window_high_shares (
|
||||
time_window_id TEXT PRIMARY KEY,
|
||||
time_window_name TEXT NOT NULL,
|
||||
sdiff DOUBLE PRECISION NOT NULL,
|
||||
time TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS daily_stats (
|
||||
date DATE PRIMARY KEY,
|
||||
share_count INT NOT NULL,
|
||||
top_share JSONB NOT NULL,
|
||||
pool_hashrate DOUBLE PRECISION NOT NULL,
|
||||
workers JSONB NOT NULL,
|
||||
expires_at TIMESTAMPTZ NULL
|
||||
);
|
||||
Reference in New Issue
Block a user