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() }