diff --git a/sdk/hints/memory/db.go b/sdk/hints/memoryh/db.go similarity index 99% rename from sdk/hints/memory/db.go rename to sdk/hints/memoryh/db.go index 7738ee9..999bad5 100644 --- a/sdk/hints/memory/db.go +++ b/sdk/hints/memoryh/db.go @@ -1,4 +1,4 @@ -package memory +package memoryh import ( "fmt" diff --git a/sdk/hints/sqlite/db.go b/sdk/hints/sqlh/db.go similarity index 53% rename from sdk/hints/sqlite/db.go rename to sdk/hints/sqlh/db.go index f53f202..1025723 100644 --- a/sdk/hints/sqlite/db.go +++ b/sdk/hints/sqlh/db.go @@ -1,4 +1,4 @@ -package sqlite +package sqlh import ( "database/sql" @@ -11,15 +11,27 @@ import ( "github.com/nbd-wtf/go-nostr/sdk/hints" ) -type SQLiteHints struct { +type SQLHints struct { *sqlx.DB - saves [7]*sqlx.Stmt - topN *sqlx.Stmt + interop interop + saves [7]*sqlx.Stmt + topN *sqlx.Stmt } -func NewSQLiteHints(db *sqlx.DB) (SQLiteHints, error) { - sh := SQLiteHints{DB: db} +// 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) + } // create table and indexes cols := strings.Builder{} @@ -37,17 +49,17 @@ func NewSQLiteHints(db *sqlx.DB) (SQLiteHints, error) { _, err := sh.Exec(`CREATE TABLE IF NOT EXISTS nostr_sdk_pubkey_relays (pubkey text, relay text, ` + cols.String()) if err != nil { - return SQLiteHints{}, err + return SQLHints{}, err } _, err = sh.Exec(`CREATE UNIQUE INDEX IF NOT EXISTS pkr ON nostr_sdk_pubkey_relays (pubkey, relay)`) if err != nil { - return SQLiteHints{}, err + return SQLHints{}, err } _, err = sh.Exec(`CREATE INDEX IF NOT EXISTS bypk ON nostr_sdk_pubkey_relays (pubkey)`) if err != nil { - return SQLiteHints{}, err + return SQLHints{}, err } // prepare statements @@ -55,10 +67,14 @@ func NewSQLiteHints(db *sqlx.DB) (SQLiteHints, error) { col := hints.HintKey(i).String() stmt, err := sh.Preparex( - `INSERT INTO nostr_sdk_pubkey_relays (pubkey, relay, ` + col + `) VALUES (?, ?, ?) - ON CONFLICT (pubkey, relay) DO UPDATE SET ` + col + ` = max(?, coalesce(` + col + `, 0))`, + `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 @@ -66,7 +82,7 @@ func NewSQLiteHints(db *sqlx.DB) (SQLiteHints, error) { { stmt, err := sh.Preparex( - `SELECT relay FROM nostr_sdk_pubkey_relays WHERE pubkey = ? ORDER BY (` + scorePartialQuery() + `) DESC LIMIT ?`, + `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) @@ -77,29 +93,29 @@ func NewSQLiteHints(db *sqlx.DB) (SQLiteHints, error) { return sh, nil } -func (sh SQLiteHints) TopN(pubkey string, n int) []string { +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/sqlite] unexpected error on query for %s: %s\n", + nostr.InfoLogger.Printf("[sdk/hints/sql] unexpected error on query for %s: %s\n", pubkey, err) } return res } -func (sh SQLiteHints) Save(pubkey string, relay string, key hints.HintKey, ts nostr.Timestamp) { +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/sqlite] unexpected error on insert for %s, %s, %d: %s\n", + nostr.InfoLogger.Printf("[sdk/hints/sql] unexpected error on insert for %s, %s, %d: %s\n", pubkey, relay, ts, err) } } -func (sh SQLiteHints) PrintScores() { +func (sh SQLHints) PrintScores() { fmt.Println("= print scores") allpubkeys := make([]string, 0, 50) @@ -115,8 +131,8 @@ func (sh SQLiteHints) PrintScores() { for _, pubkey := range allpubkeys { fmt.Println("== relay scores for", pubkey) if err := sh.Select(&allrelays, - `SELECT pubkey, relay, coalesce(`+scorePartialQuery()+`, 0) AS score - FROM nostr_sdk_pubkey_relays WHERE pubkey = ? ORDER BY score DESC`, pubkey); err != nil { + `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) } @@ -126,9 +142,9 @@ func (sh SQLiteHints) PrintScores() { } } -func scorePartialQuery() string { +func (sh SQLHints) scorePartialQuery() string { calc := strings.Builder{} - calc.Grow(len(hints.KeyBasePoints) * (10 + 25 + 51 + 25 + 24 + 4 + 12 + 3)) + 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() @@ -138,7 +154,11 @@ func scorePartialQuery() string { calc.WriteString(col) calc.WriteString(` IS NOT NULL THEN 10000000000 * `) calc.WriteString(multiplier) - calc.WriteString(` / power(max(1, (unixepoch() + 86400) - `) + 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)`) diff --git a/sdk/hints/sqlh/interop.go b/sdk/hints/sqlh/interop.go new file mode 100644 index 0000000..3cf787f --- /dev/null +++ b/sdk/hints/sqlh/interop.go @@ -0,0 +1,48 @@ +package sqlh + +import ( + "strconv" + "strings" +) + +type interop struct { + maxFunc string + getUnixEpochFunc string + generateBindingSpots func(start, n int) string +} + +var sqliteInterop = interop{ + maxFunc: "max", + getUnixEpochFunc: "unixepoch()", + generateBindingSpots: func(_, n int) string { + b := strings.Builder{} + b.Grow(n * 2) + for i := range n { + if i == n-1 { + b.WriteString("?") + } else { + b.WriteString("?,") + } + } + return b.String() + }, +} + +var postgresInterop = interop{ + maxFunc: "greatest", + getUnixEpochFunc: "extract(epoch from now())", + generateBindingSpots: func(start, n int) string { + b := strings.Builder{} + b.Grow(n * 2) + end := start + n + for i := start; i < end; i++ { + v := i + 1 + b.WriteRune('$') + b.WriteString(strconv.Itoa(v)) + if i != end-1 { + b.WriteRune(',') + } + } + return b.String() + }, +} diff --git a/sdk/hints/test/libsql_test.go b/sdk/hints/test/libsql_test.go index 0b6c8a2..18bb8d0 100644 --- a/sdk/hints/test/libsql_test.go +++ b/sdk/hints/test/libsql_test.go @@ -3,11 +3,11 @@ package test import ( + "database/sql" "os" "testing" - "github.com/jmoiron/sqlx" - "github.com/nbd-wtf/go-nostr/sdk/hints/sqlite" + "github.com/nbd-wtf/go-nostr/sdk/hints/sqlh" "github.com/stretchr/testify/require" _ "github.com/tursodatabase/go-libsql" ) @@ -16,12 +16,12 @@ func TestSQLiteHintsLibsql(t *testing.T) { path := "/tmp/tmpsdkhintssqlite" os.RemoveAll(path) - db, err := sqlx.Connect("libsql", "file://"+path) + db, err := sql.Open("libsql", "file://"+path) require.NoError(t, err, "failed to create sqlitehints db") db.SetMaxOpenConns(1) - sh, err := sqlite.NewSQLiteHints(db) + sh, err := sqlh.NewSQLHints(db, "sqlite3") require.NoError(t, err, "failed to setup sqlitehints db") runTestWith(t, sh) diff --git a/sdk/hints/test/mattnsqlite_test.go b/sdk/hints/test/mattnsqlite_test.go index bf48436..13569c4 100644 --- a/sdk/hints/test/mattnsqlite_test.go +++ b/sdk/hints/test/mattnsqlite_test.go @@ -3,12 +3,12 @@ package test import ( + "database/sql" "os" "testing" - "github.com/jmoiron/sqlx" _ "github.com/mattn/go-sqlite3" - "github.com/nbd-wtf/go-nostr/sdk/hints/sqlite" + "github.com/nbd-wtf/go-nostr/sdk/hints/sqlh" "github.com/stretchr/testify/require" ) @@ -16,12 +16,12 @@ func TestSQLiteHintsMattn(t *testing.T) { path := "/tmp/tmpsdkhintssqlite" os.RemoveAll(path) - db, err := sqlx.Connect("sqlite3", path) + db, err := sql.Open("sqlite3", path) require.NoError(t, err, "failed to create sqlitehints db") db.SetMaxOpenConns(1) - sh, err := sqlite.NewSQLiteHints(db) + sh, err := sqlh.NewSQLHints(db, "sqlite3") require.NoError(t, err, "failed to setup sqlitehints db") runTestWith(t, sh) diff --git a/sdk/hints/test/memory_test.go b/sdk/hints/test/memory_test.go index 425762f..6611659 100644 --- a/sdk/hints/test/memory_test.go +++ b/sdk/hints/test/memory_test.go @@ -3,9 +3,9 @@ package test import ( "testing" - "github.com/nbd-wtf/go-nostr/sdk/hints/memory" + "github.com/nbd-wtf/go-nostr/sdk/hints/memoryh" ) func TestMemoryHints(t *testing.T) { - runTestWith(t, memory.NewHintDB()) + runTestWith(t, memoryh.NewHintDB()) } diff --git a/sdk/hints/test/moderncsqlite_test.go b/sdk/hints/test/moderncsqlite_test.go index cdb0109..fe5e0d0 100644 --- a/sdk/hints/test/moderncsqlite_test.go +++ b/sdk/hints/test/moderncsqlite_test.go @@ -1,11 +1,11 @@ package test import ( + "database/sql" "os" "testing" - "github.com/jmoiron/sqlx" - "github.com/nbd-wtf/go-nostr/sdk/hints/sqlite" + "github.com/nbd-wtf/go-nostr/sdk/hints/sqlh" "github.com/stretchr/testify/require" _ "modernc.org/sqlite" ) @@ -14,12 +14,12 @@ func TestSQLiteHintsModernC(t *testing.T) { path := "/tmp/tmpsdkhintssqlite" os.RemoveAll(path) - db, err := sqlx.Connect("sqlite", path) + db, err := sql.Open("sqlite", path) require.NoError(t, err, "failed to create sqlitehints db") db.SetMaxOpenConns(1) - sh, err := sqlite.NewSQLiteHints(db) + sh, err := sqlh.NewSQLHints(db, "sqlite3") require.NoError(t, err, "failed to setup sqlitehints db") runTestWith(t, sh) diff --git a/sdk/hints/test/wasmsqlite_test.go b/sdk/hints/test/wasmsqlite_test.go index 4c67a60..2dba133 100644 --- a/sdk/hints/test/wasmsqlite_test.go +++ b/sdk/hints/test/wasmsqlite_test.go @@ -3,11 +3,11 @@ package test import ( + "database/sql" "os" "testing" - "github.com/jmoiron/sqlx" - "github.com/nbd-wtf/go-nostr/sdk/hints/sqlite" + "github.com/nbd-wtf/go-nostr/sdk/hints/sqlh" _ "github.com/ncruces/go-sqlite3/driver" _ "github.com/ncruces/go-sqlite3/embed" "github.com/stretchr/testify/require" @@ -17,12 +17,12 @@ func TestSQLiteHintsNcruces(t *testing.T) { path := "/tmp/tmpsdkhintssqlite" os.RemoveAll(path) - db, err := sqlx.Connect("sqlite3", path) + db, err := sql.Open("sqlite3", path) require.NoError(t, err, "failed to create sqlitehints db") db.SetMaxOpenConns(1) - sh, err := sqlite.NewSQLiteHints(db) + sh, err := sqlh.NewSQLHints(db, "sqlite3") require.NoError(t, err, "failed to setup sqlitehints db") runTestWith(t, sh)