mirror of
https://github.com/nbd-wtf/go-nostr.git
synced 2025-03-28 02:33:09 +01:00
239 lines
6.6 KiB
Go
239 lines
6.6 KiB
Go
package sqlh
|
|
|
|
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 SQLHints struct {
|
|
*sqlx.DB
|
|
|
|
interop interop
|
|
saves [7]*sqlx.Stmt
|
|
topN *sqlx.Stmt
|
|
}
|
|
|
|
// NewSQLHints takes an sqlx.DB connection (db) and a database type name (driverName ).
|
|
// driverName must be either "postgres" or "sqlite3" -- this is so we can slightly change the queries.
|
|
func NewSQLHints(db *sql.DB, driverName string) (SQLHints, error) {
|
|
sh := SQLHints{DB: sqlx.NewDb(db, driverName)}
|
|
|
|
switch driverName {
|
|
case "sqlite3":
|
|
sh.interop = sqliteInterop
|
|
case "postgres":
|
|
sh.interop = postgresInterop
|
|
default:
|
|
return sh, fmt.Errorf("unknown database driver '%s'", driverName)
|
|
}
|
|
|
|
// db migrations
|
|
if txn, err := sh.Beginx(); err != nil {
|
|
return SQLHints{}, err
|
|
} else {
|
|
if _, err := txn.Exec(`CREATE TABLE IF NOT EXISTS nostr_sdk_db_version (version int)`); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
var version int
|
|
if err := txn.Get(&version, `SELECT version FROM nostr_sdk_db_version`); err != nil && err != sql.ErrNoRows {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
|
|
if version == 0 {
|
|
if _, err := txn.Exec(`INSERT INTO nostr_sdk_db_version VALUES (0)`); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
version = 1
|
|
if _, err := txn.Exec(
|
|
`CREATE TABLE IF NOT EXISTS nostr_sdk_pubkey_relays (` +
|
|
`pubkey text, ` +
|
|
`relay text, ` +
|
|
`last_fetch_attempt integer, ` +
|
|
`most_recent_event_fetched integer, ` +
|
|
`last_in_relay_list integer, ` +
|
|
`last_in_tag integer, ` +
|
|
`last_in_nprofile integer, ` +
|
|
`last_in_nevent integer, ` +
|
|
`last_in_nip05 integer ` +
|
|
`)`,
|
|
); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
if _, err := txn.Exec(
|
|
`CREATE UNIQUE INDEX IF NOT EXISTS pkr ON nostr_sdk_pubkey_relays (pubkey, relay)`,
|
|
); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
if _, err := txn.Exec(
|
|
`CREATE INDEX IF NOT EXISTS bypk ON nostr_sdk_pubkey_relays (pubkey)`,
|
|
); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
}
|
|
|
|
if version == 1 {
|
|
version = 2
|
|
if _, err := txn.Exec(
|
|
`ALTER TABLE nostr_sdk_pubkey_relays DROP COLUMN last_in_tag`,
|
|
); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
if _, err := txn.Exec(
|
|
`ALTER TABLE nostr_sdk_pubkey_relays DROP COLUMN last_in_nprofile`,
|
|
); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
if _, err := txn.Exec(
|
|
`ALTER TABLE nostr_sdk_pubkey_relays DROP COLUMN last_in_nevent`,
|
|
); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
if _, err := txn.Exec(
|
|
`ALTER TABLE nostr_sdk_pubkey_relays DROP COLUMN last_in_nip05`,
|
|
); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
if _, err := txn.Exec(
|
|
`ALTER TABLE nostr_sdk_pubkey_relays ADD COLUMN last_in_hint integer`,
|
|
); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
}
|
|
|
|
if _, err := txn.Exec(
|
|
fmt.Sprintf(`UPDATE nostr_sdk_db_version SET version = %d`, version),
|
|
); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
if err := txn.Commit(); err != nil {
|
|
txn.Rollback()
|
|
return SQLHints{}, err
|
|
}
|
|
}
|
|
|
|
// prepare statements
|
|
for i := range hints.KeyBasePoints {
|
|
col := hints.HintKey(i).String()
|
|
|
|
stmt, err := sh.Preparex(
|
|
`INSERT INTO nostr_sdk_pubkey_relays (pubkey, relay, ` + col + `) VALUES (` + sh.interop.generateBindingSpots(0, 3) + `)
|
|
ON CONFLICT (pubkey, relay) DO UPDATE SET ` + col + ` = ` + sh.interop.maxFunc + `(` + sh.interop.generateBindingSpots(3, 1) + `, coalesce(excluded.` + col + `, 0))`,
|
|
)
|
|
if err != nil {
|
|
fmt.Println(
|
|
`INSERT INTO nostr_sdk_pubkey_relays (pubkey, relay, ` + col + `) VALUES (` + sh.interop.generateBindingSpots(0, 3) + `)
|
|
ON CONFLICT (pubkey, relay) DO UPDATE SET ` + col + ` = ` + sh.interop.maxFunc + `(` + sh.interop.generateBindingSpots(3, 1) + `, coalesce(excluded.` + col + `, 0))`,
|
|
)
|
|
return sh, fmt.Errorf("failed to prepare statement for %s: %w", col, err)
|
|
}
|
|
sh.saves[i] = stmt
|
|
}
|
|
|
|
{
|
|
stmt, err := sh.Preparex(
|
|
`SELECT relay FROM nostr_sdk_pubkey_relays WHERE pubkey = ` + sh.interop.generateBindingSpots(0, 1) + ` ORDER BY (` + sh.scorePartialQuery() + `) DESC LIMIT ` + sh.interop.generateBindingSpots(1, 1),
|
|
)
|
|
if err != nil {
|
|
return sh, fmt.Errorf("failed to prepare statement for querying: %w", err)
|
|
}
|
|
sh.topN = stmt
|
|
}
|
|
|
|
return sh, nil
|
|
}
|
|
|
|
func (sh SQLHints) 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/sql] unexpected error on query for %s: %s\n",
|
|
pubkey, err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func (sh SQLHints) Save(pubkey string, relay string, key hints.HintKey, ts nostr.Timestamp) {
|
|
if now := nostr.Now(); ts > now {
|
|
ts = now
|
|
}
|
|
|
|
_, err := sh.saves[key].Exec(pubkey, relay, ts, ts)
|
|
if err != nil {
|
|
nostr.InfoLogger.Printf("[sdk/hints/sql] unexpected error on insert for %s, %s, %d: %s\n",
|
|
pubkey, relay, ts, err)
|
|
}
|
|
}
|
|
|
|
func (sh SQLHints) PrintScores() {
|
|
fmt.Println("= print scores")
|
|
|
|
allpubkeys := make([]string, 0, 50)
|
|
if err := sh.Select(&allpubkeys, `SELECT DISTINCT pubkey FROM nostr_sdk_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(`+sh.scorePartialQuery()+`, 0) AS score
|
|
FROM nostr_sdk_pubkey_relays WHERE pubkey = `+sh.interop.generateBindingSpots(0, 1)+` 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 (sh SQLHints) scorePartialQuery() string {
|
|
calc := strings.Builder{}
|
|
calc.Grow(len(hints.KeyBasePoints) * (11 + 25 + 32 + 4 + 4 + 9 + 12 + 25 + 12 + 25 + 19 + 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(`)
|
|
calc.WriteString(sh.interop.maxFunc)
|
|
calc.WriteString(`(1, (`)
|
|
calc.WriteString(sh.interop.getUnixEpochFunc)
|
|
calc.WriteString(` + 86400) - `)
|
|
calc.WriteString(col)
|
|
calc.WriteString(`), 1.3) ELSE 0 END)`)
|
|
|
|
if i != len(hints.KeyBasePoints)-1 {
|
|
calc.WriteString(` + `)
|
|
}
|
|
}
|
|
|
|
return calc.String()
|
|
}
|