mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-07-28 12:12:12 +02:00
sdk/hints: sqlite backend and tests.
This commit is contained in:
@@ -5,4 +5,5 @@ import "github.com/nbd-wtf/go-nostr"
|
||||
type HintsDB interface {
|
||||
TopN(pubkey string, n int) []string
|
||||
Save(pubkey string, relay string, key HintKey, score nostr.Timestamp)
|
||||
PrintScores()
|
||||
}
|
||||
|
@@ -27,13 +27,6 @@ func NewHintDB() *HintDB {
|
||||
}
|
||||
|
||||
func (db *HintDB) Save(pubkey string, relay string, key hints.HintKey, ts nostr.Timestamp) {
|
||||
now := nostr.Now()
|
||||
// this is used for calculating what counts as a usable hint
|
||||
threshold := (now - 60*60*24*180)
|
||||
if threshold < 0 {
|
||||
threshold = 0
|
||||
}
|
||||
|
||||
relayIndex := slices.Index(db.RelayBySerial, relay)
|
||||
if relayIndex == -1 {
|
||||
relayIndex = len(db.RelayBySerial)
|
||||
@@ -104,6 +97,9 @@ func (db *HintDB) PrintScores() {
|
||||
fmt.Println("== relay scores for", pubkey)
|
||||
for i, re := range rfpk.Entries {
|
||||
fmt.Printf(" %3d :: %30s (%3d) ::> %12d\n", i, db.RelayBySerial[re.Relay], re.Relay, re.Sum())
|
||||
// for i, ts := range re.Timestamps {
|
||||
// fmt.Printf(" %-10d %s\n", ts, hints.HintKey(i).String())
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +110,7 @@ type RelaysForPubKey struct {
|
||||
|
||||
type RelayEntry struct {
|
||||
Relay int
|
||||
Timestamps [8]nostr.Timestamp
|
||||
Timestamps [7]nostr.Timestamp
|
||||
}
|
||||
|
||||
func (re RelayEntry) Sum() int64 {
|
||||
@@ -125,18 +121,9 @@ func (re RelayEntry) Sum() int64 {
|
||||
continue
|
||||
}
|
||||
|
||||
hk := hints.HintKey(i)
|
||||
divisor := int64(now - ts)
|
||||
if divisor == 0 {
|
||||
divisor = 1
|
||||
} else {
|
||||
divisor = int64(math.Pow(float64(divisor), 1.3))
|
||||
}
|
||||
|
||||
multiplier := hk.BasePoints()
|
||||
value := multiplier * 10000000000 / divisor
|
||||
value := float64(hints.HintKey(i).BasePoints()) * 10000000000 / math.Pow(float64(max(now-ts, 1)), 1.3)
|
||||
// fmt.Println(" ", i, "value:", value)
|
||||
sum += value
|
||||
sum += int64(value)
|
||||
}
|
||||
return sum
|
||||
}
|
147
sdk/hints/sqlite/db.go
Normal file
147
sdk/hints/sqlite/db.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/sdk/hints"
|
||||
)
|
||||
|
||||
type SQLiteHints struct {
|
||||
*sqlx.DB
|
||||
|
||||
saves [7]*sqlx.Stmt
|
||||
topN *sqlx.Stmt
|
||||
}
|
||||
|
||||
func NewSQLiteHints(db *sqlx.DB) (SQLiteHints, error) {
|
||||
sh := SQLiteHints{DB: db}
|
||||
|
||||
// create table and indexes
|
||||
cols := strings.Builder{}
|
||||
cols.Grow(len(hints.KeyBasePoints) * 20)
|
||||
for i := range hints.KeyBasePoints {
|
||||
name := hints.HintKey(i).String()
|
||||
cols.WriteString(name)
|
||||
cols.WriteString(" integer")
|
||||
if i == len(hints.KeyBasePoints)-1 {
|
||||
cols.WriteString(")")
|
||||
} else {
|
||||
cols.WriteString(",")
|
||||
}
|
||||
}
|
||||
|
||||
_, err := sh.Exec(`CREATE TABLE pubkey_relays (pubkey text, relay text, ` + cols.String())
|
||||
if err != nil {
|
||||
return SQLiteHints{}, err
|
||||
}
|
||||
|
||||
_, err = sh.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS pkr ON pubkey_relays (pubkey, relay)`)
|
||||
if err != nil {
|
||||
return SQLiteHints{}, err
|
||||
}
|
||||
|
||||
_, err = sh.Exec(`CREATE INDEX IF NOT EXISTS bypk ON pubkey_relays (pubkey)`)
|
||||
if err != nil {
|
||||
return SQLiteHints{}, err
|
||||
}
|
||||
|
||||
// prepare statements
|
||||
for i := range hints.KeyBasePoints {
|
||||
col := hints.HintKey(i).String()
|
||||
|
||||
stmt, err := sh.Preparex(
|
||||
`INSERT INTO pubkey_relays (pubkey, relay, ` + col + `) VALUES (?, ?, ?)
|
||||
ON CONFLICT (pubkey, relay) DO UPDATE SET ` + col + ` = max(?, coalesce(` + col + `, 0))`,
|
||||
)
|
||||
if err != nil {
|
||||
return sh, fmt.Errorf("failed to prepare statement for %s: %w", col, err)
|
||||
}
|
||||
sh.saves[i] = stmt
|
||||
}
|
||||
|
||||
{
|
||||
stmt, err := sh.Preparex(
|
||||
`SELECT relay FROM pubkey_relays WHERE pubkey = ? ORDER BY (` + scorePartialQuery() + `) DESC LIMIT ?`,
|
||||
)
|
||||
if err != nil {
|
||||
return sh, fmt.Errorf("failed to prepare statement for querying: %w", err)
|
||||
}
|
||||
sh.topN = stmt
|
||||
}
|
||||
|
||||
return sh, nil
|
||||
}
|
||||
|
||||
func (sh SQLiteHints) TopN(pubkey string, n int) []string {
|
||||
res := make([]string, 0, n)
|
||||
err := sh.topN.Select(&res, pubkey, n)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
nostr.InfoLogger.Printf("[sdk/hints/sqlite] unexpected error on query for %s: %s\n",
|
||||
pubkey, err)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
func (sh SQLiteHints) Save(pubkey string, relay string, key hints.HintKey, score nostr.Timestamp) {
|
||||
_, err := sh.saves[key].Exec(pubkey, relay, score, score)
|
||||
if err != nil {
|
||||
nostr.InfoLogger.Printf("[sdk/hints/sqlite] unexpected error on insert for %s, %s, %d: %s\n",
|
||||
pubkey, relay, score, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (sh SQLiteHints) PrintScores() {
|
||||
fmt.Println("= print scores")
|
||||
|
||||
allpubkeys := make([]string, 0, 50)
|
||||
if err := sh.Select(&allpubkeys, `SELECT DISTINCT pubkey FROM pubkey_relays`); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
allrelays := make([]struct {
|
||||
PubKey string `db:"pubkey"`
|
||||
Relay string `db:"relay"`
|
||||
Score float64 `db:"score"`
|
||||
}, 0, 20)
|
||||
for _, pubkey := range allpubkeys {
|
||||
fmt.Println("== relay scores for", pubkey)
|
||||
if err := sh.Select(&allrelays,
|
||||
`SELECT pubkey, relay, coalesce(`+scorePartialQuery()+`, 0) AS score
|
||||
FROM pubkey_relays WHERE pubkey = ? ORDER BY score DESC`, pubkey); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for i, re := range allrelays {
|
||||
fmt.Printf(" %3d :: %30s ::> %12d\n", i, re.Relay, int(re.Score))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scorePartialQuery() string {
|
||||
calc := strings.Builder{}
|
||||
calc.Grow(len(hints.KeyBasePoints) * (10 + 25 + 51 + 25 + 24 + 4 + 12 + 3))
|
||||
|
||||
for i, points := range hints.KeyBasePoints {
|
||||
col := hints.HintKey(i).String()
|
||||
multiplier := strconv.FormatInt(points, 10)
|
||||
|
||||
calc.WriteString(`(CASE WHEN `)
|
||||
calc.WriteString(col)
|
||||
calc.WriteString(` IS NOT NULL THEN 10000000000 * `)
|
||||
calc.WriteString(multiplier)
|
||||
calc.WriteString(` / power(max(1, (unixepoch() + 86400) - `)
|
||||
calc.WriteString(col)
|
||||
calc.WriteString(`), 1.3) ELSE 0 END)`)
|
||||
|
||||
if i != len(hints.KeyBasePoints)-1 {
|
||||
calc.WriteString(` + `)
|
||||
}
|
||||
}
|
||||
|
||||
return calc.String()
|
||||
}
|
28
sdk/hints/test/libsql_test.go
Normal file
28
sdk/hints/test/libsql_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build !sqlite_math_functions
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/nbd-wtf/go-nostr/sdk/hints/sqlite"
|
||||
"github.com/stretchr/testify/require"
|
||||
_ "github.com/tursodatabase/go-libsql"
|
||||
)
|
||||
|
||||
func TestSQLiteHintsLibsql(t *testing.T) {
|
||||
path := "/tmp/tmpsdkhintssqlite"
|
||||
os.RemoveAll(path)
|
||||
|
||||
db, err := sqlx.Connect("libsql", "file://"+path)
|
||||
|
||||
require.NoError(t, err, "failed to create sqlitehints db")
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
sh, err := sqlite.NewSQLiteHints(db)
|
||||
require.NoError(t, err, "failed to setup sqlitehints db")
|
||||
|
||||
runTestWith(t, sh)
|
||||
}
|
28
sdk/hints/test/mattnsqlite_test.go
Normal file
28
sdk/hints/test/mattnsqlite_test.go
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build sqlite_math_functions
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/nbd-wtf/go-nostr/sdk/hints/sqlite"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSQLiteHintsMattn(t *testing.T) {
|
||||
path := "/tmp/tmpsdkhintssqlite"
|
||||
os.RemoveAll(path)
|
||||
|
||||
db, err := sqlx.Connect("sqlite3", path)
|
||||
|
||||
require.NoError(t, err, "failed to create sqlitehints db")
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
sh, err := sqlite.NewSQLiteHints(db)
|
||||
require.NoError(t, err, "failed to setup sqlitehints db")
|
||||
|
||||
runTestWith(t, sh)
|
||||
}
|
11
sdk/hints/test/memory_test.go
Normal file
11
sdk/hints/test/memory_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr/sdk/hints/memory"
|
||||
)
|
||||
|
||||
func TestMemoryHints(t *testing.T) {
|
||||
runTestWith(t, memory.NewHintDB())
|
||||
}
|
26
sdk/hints/test/moderncsqlite_test.go
Normal file
26
sdk/hints/test/moderncsqlite_test.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/nbd-wtf/go-nostr/sdk/hints/sqlite"
|
||||
"github.com/stretchr/testify/require"
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
func TestSQLiteHintsModernC(t *testing.T) {
|
||||
path := "/tmp/tmpsdkhintssqlite"
|
||||
os.RemoveAll(path)
|
||||
|
||||
db, err := sqlx.Connect("sqlite", path)
|
||||
|
||||
require.NoError(t, err, "failed to create sqlitehints db")
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
sh, err := sqlite.NewSQLiteHints(db)
|
||||
require.NoError(t, err, "failed to setup sqlitehints db")
|
||||
|
||||
runTestWith(t, sh)
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package memory
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -9,16 +9,14 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRelayPicking(t *testing.T) {
|
||||
hdb := NewHintDB()
|
||||
|
||||
func runTestWith(t *testing.T, hdb hints.HintsDB) {
|
||||
const key1 = "0000000000000000000000000000000000000000000000000000000000000001"
|
||||
const key2 = "0000000000000000000000000000000000000000000000000000000000000002"
|
||||
const key3 = "0000000000000000000000000000000000000000000000000000000000000003"
|
||||
const key4 = "0000000000000000000000000000000000000000000000000000000000000004"
|
||||
const relayA = "wss://aaa.com"
|
||||
const relayB = "wss://bbb.online"
|
||||
const relayC = "wss://ccc.technology"
|
||||
const relayB = "wss://bbb.net"
|
||||
const relayC = "wss://ccc.org"
|
||||
|
||||
hour := nostr.Timestamp((time.Hour).Seconds())
|
||||
day := hour * 24
|
29
sdk/hints/test/wasmsqlite_test.go
Normal file
29
sdk/hints/test/wasmsqlite_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
//go:build !sqlite_math_functions
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/nbd-wtf/go-nostr/sdk/hints/sqlite"
|
||||
_ "github.com/ncruces/go-sqlite3/driver"
|
||||
_ "github.com/ncruces/go-sqlite3/embed"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSQLiteHintsNcruces(t *testing.T) {
|
||||
path := "/tmp/tmpsdkhintssqlite"
|
||||
os.RemoveAll(path)
|
||||
|
||||
db, err := sqlx.Connect("sqlite3", path)
|
||||
|
||||
require.NoError(t, err, "failed to create sqlitehints db")
|
||||
db.SetMaxOpenConns(1)
|
||||
|
||||
sh, err := sqlite.NewSQLiteHints(db)
|
||||
require.NoError(t, err, "failed to setup sqlitehints db")
|
||||
|
||||
runTestWith(t, sh)
|
||||
}
|
Reference in New Issue
Block a user