From 307dcda4a7f653835ed009ec6334b730d552ba5d Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Wed, 24 Sep 2025 16:21:26 -0700 Subject: [PATCH] Switch to squirrel --- go.mod | 3 ++ go.sum | 7 ++++ sqlite/delete.go | 5 ++- sqlite/helpers.go | 8 ----- sqlite/lib.go | 6 +--- sqlite/query.go | 89 +++++++++++++++++------------------------------ sqlite/replace.go | 3 -- sqlite/save.go | 37 +++++++++++--------- 8 files changed, 65 insertions(+), 93 deletions(-) delete mode 100644 sqlite/helpers.go diff --git a/go.mod b/go.mod index 3700297..03d00b6 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( require ( github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect github.com/PowerDNS/lmdb-go v1.9.3 // indirect github.com/andybalholm/brotli v1.1.1 // indirect github.com/bep/debounce v1.2.1 // indirect @@ -25,6 +26,8 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.18.0 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/liamg/magic v0.0.1 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index 693ffd3..2089d90 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg= github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3/go.mod h1:we0YA5CsBbH5+/NUzC/AlMmxaDtWlXeNsqrwXjTzmzA= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/PowerDNS/lmdb-go v1.9.3 h1:AUMY2pZT8WRpkEv39I9Id3MuoHd+NZbTVpNhruVkPTg= github.com/PowerDNS/lmdb-go v1.9.3/go.mod h1:TE0l+EZK8Z1B4dx070ZxkWTlp8RG1mjN0/+FkFRQMtU= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= @@ -39,6 +41,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/liamg/magic v0.0.1 h1:Ru22ElY+sCh6RvRTWjQzKKCxsEco8hE0co8n1qe7TBM= github.com/liamg/magic v0.0.1/go.mod h1:yQkOmZZI52EA+SQ2xyHpVw8fNvTBruF873Y+Vt6S+fk= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= @@ -65,6 +71,7 @@ github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38/go.mod h1:sM7Mt7uEo github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= diff --git a/sqlite/delete.go b/sqlite/delete.go index 645d7a5..b0a34bd 100644 --- a/sqlite/delete.go +++ b/sqlite/delete.go @@ -2,12 +2,11 @@ package sqlite import ( "fiatjaf.com/nostr" + "github.com/Masterminds/squirrel" ) func (s *SqliteBackend) DeleteEvent(id nostr.ID) error { - s.Lock() - defer s.Unlock() + _, err := squirrel.Delete("events").Where(squirrel.Eq{"id": id.Hex()}).RunWith(s.db).Exec() - _, err := s.db.Exec("DELETE FROM events WHERE id = ?", id.Hex()) return err } diff --git a/sqlite/helpers.go b/sqlite/helpers.go deleted file mode 100644 index 913e5f1..0000000 --- a/sqlite/helpers.go +++ /dev/null @@ -1,8 +0,0 @@ -package sqlite - -// Helper functions and constants for the SQLite eventstore - -const ( - // Database configuration - defaultTimeout = 30 // seconds -) diff --git a/sqlite/lib.go b/sqlite/lib.go index f4e27be..6d8c3d0 100644 --- a/sqlite/lib.go +++ b/sqlite/lib.go @@ -3,7 +3,6 @@ package sqlite import ( "database/sql" "fmt" - "sync" "fiatjaf.com/nostr/eventstore" _ "github.com/mattn/go-sqlite3" @@ -12,11 +11,8 @@ import ( var _ eventstore.Store = (*SqliteBackend)(nil) type SqliteBackend struct { - sync.RWMutex - // Path is where the database will be saved - Path string - db *sql.DB + Path string FTSAvailable bool } diff --git a/sqlite/query.go b/sqlite/query.go index 69551b4..3bfc56e 100644 --- a/sqlite/query.go +++ b/sqlite/query.go @@ -3,18 +3,14 @@ package sqlite import ( "encoding/hex" "encoding/json" - "fmt" "iter" - "strings" "fiatjaf.com/nostr" + "github.com/Masterminds/squirrel" ) func (s *SqliteBackend) QueryEvents(filter nostr.Filter, maxLimit int) iter.Seq[nostr.Event] { return func(yield func(nostr.Event) bool) { - s.RLock() - defer s.RUnlock() - if filter.LimitZero { return } @@ -24,9 +20,7 @@ func (s *SqliteBackend) QueryEvents(filter nostr.Filter, maxLimit int) iter.Seq[ limit = filter.Limit } - query, args := s.buildSelectQuery(filter, limit) - - rows, err := s.db.Query(query, args...) + rows, err := s.buildSelectQuery(filter, limit).RunWith(s.db).Query() if err != nil { return } @@ -80,92 +74,73 @@ func (s *SqliteBackend) QueryEvents(filter nostr.Filter, maxLimit int) iter.Seq[ } } -func (s *SqliteBackend) buildSelectQuery(filter nostr.Filter, limit int) (string, []interface{}) { - var conditions []string - var args []interface{} - var joins []string - - baseQuery := "SELECT id, created_at, kind, pubkey, content, tags, sig FROM events" +func (s *SqliteBackend) buildSelectQuery(filter nostr.Filter, limit int) squirrel.SelectBuilder { + qb := squirrel.Select("id", "created_at", "kind", "pubkey", "content", "tags", "sig"). + From("events"). + OrderBy("created_at DESC") // Handle search with FTS (if available) if filter.Search != "" && s.FTSAvailable { - joins = append(joins, "JOIN events_fts ON events.rowid = events_fts.rowid") - conditions = append(conditions, "events_fts MATCH ?") - args = append(args, filter.Search) + qb = qb.Join("events_fts ON events.rowid = events_fts.rowid"). + Where(squirrel.Eq{"events_fts": filter.Search}) } else if filter.Search != "" { // Fallback to LIKE search if FTS not available - conditions = append(conditions, "content LIKE ?") - args = append(args, "%"+filter.Search+"%") + qb = qb.Where(squirrel.Like{"content": "%" + filter.Search + "%"}) } - // Add WHERE clause conditions if len(filter.IDs) > 0 { - placeholders := make([]string, len(filter.IDs)) + idStrs := make([]interface{}, len(filter.IDs)) for i, id := range filter.IDs { - placeholders[i] = "?" - args = append(args, id.Hex()) + idStrs[i] = id.Hex() } - conditions = append(conditions, fmt.Sprintf("id IN (%s)", strings.Join(placeholders, ","))) + qb = qb.Where(squirrel.Eq{"id": idStrs}) } if len(filter.Authors) > 0 { - placeholders := make([]string, len(filter.Authors)) + authorStrs := make([]interface{}, len(filter.Authors)) for i, author := range filter.Authors { - placeholders[i] = "?" - args = append(args, author.Hex()) + authorStrs[i] = author.Hex() } - conditions = append(conditions, fmt.Sprintf("pubkey IN (%s)", strings.Join(placeholders, ","))) + qb = qb.Where(squirrel.Eq{"pubkey": authorStrs}) } if len(filter.Kinds) > 0 { - placeholders := make([]string, len(filter.Kinds)) + kindInts := make([]interface{}, len(filter.Kinds)) for i, kind := range filter.Kinds { - placeholders[i] = "?" - args = append(args, int(kind)) + kindInts[i] = int(kind) } - conditions = append(conditions, fmt.Sprintf("kind IN (%s)", strings.Join(placeholders, ","))) + qb = qb.Where(squirrel.Eq{"kind": kindInts}) } if filter.Since != 0 { - conditions = append(conditions, "created_at >= ?") - args = append(args, filter.Since) + qb = qb.Where(squirrel.GtOrEq{"created_at": filter.Since}) } if filter.Until != 0 { - conditions = append(conditions, "created_at <= ?") - args = append(args, filter.Until) + qb = qb.Where(squirrel.LtOrEq{"created_at": filter.Until}) } - // Handle tags - only filter single-letter tags using event_tags table for tagKey, tagValues := range filter.Tags { if len(tagValues) > 0 && len(tagKey) == 1 { - placeholders := make([]string, len(tagValues)) + tagValueInterfaces := make([]interface{}, len(tagValues)) for i, tagValue := range tagValues { - placeholders[i] = "?" - args = append(args, tagValue) + tagValueInterfaces[i] = tagValue } - conditions = append(conditions, fmt.Sprintf("id IN (SELECT event_id FROM event_tags WHERE key = ? AND value IN (%s))", strings.Join(placeholders, ","))) - args = append(args, tagKey) + + subQuery := squirrel.Select("event_id"). + From("event_tags"). + Where(squirrel.Eq{"key": tagKey}). + Where(squirrel.Eq{"value": tagValueInterfaces}) + + subQuerySql, subQueryArgs, _ := subQuery.ToSql() + qb = qb.Where("id IN ("+subQuerySql+")", subQueryArgs...) } } - // Build the complete query - if len(joins) > 0 { - baseQuery += " " + strings.Join(joins, " ") - } - - if len(conditions) > 0 { - baseQuery += " WHERE " + strings.Join(conditions, " AND ") - } - - // Order by created_at DESC for most recent first - baseQuery += " ORDER BY created_at DESC" - // Add limit if limit > 0 { - baseQuery += " LIMIT ?" - args = append(args, limit) + qb = qb.Limit(uint64(limit)) } - return baseQuery, args + return qb } diff --git a/sqlite/replace.go b/sqlite/replace.go index 8e2d069..1b3657a 100644 --- a/sqlite/replace.go +++ b/sqlite/replace.go @@ -8,9 +8,6 @@ import ( ) func (s *SqliteBackend) ReplaceEvent(evt nostr.Event) error { - s.Lock() - defer s.Unlock() - filter := nostr.Filter{Kinds: []nostr.Kind{evt.Kind}, Authors: []nostr.PubKey{evt.PubKey}} if evt.Kind.IsAddressable() { filter.Tags = nostr.TagMap{"d": []string{evt.Tags.GetD()}} diff --git a/sqlite/save.go b/sqlite/save.go index c444655..af75d81 100644 --- a/sqlite/save.go +++ b/sqlite/save.go @@ -7,15 +7,14 @@ import ( "fiatjaf.com/nostr" "fiatjaf.com/nostr/eventstore" + "github.com/Masterminds/squirrel" ) func (s *SqliteBackend) SaveEvent(evt nostr.Event) error { - s.Lock() - defer s.Unlock() - // Check if event already exists var existingID string - err := s.db.QueryRow("SELECT id FROM events WHERE id = ?", evt.ID.Hex()).Scan(&existingID) + qb := squirrel.Select("id").From("events").Where(squirrel.Eq{"id": evt.ID.Hex()}) + err := qb.RunWith(s.db).QueryRow().Scan(&existingID) if err == nil { // Event already exists return eventstore.ErrDupEvent @@ -28,18 +27,19 @@ func (s *SqliteBackend) SaveEvent(evt nostr.Event) error { } // Insert the event - query := `INSERT INTO events (id, created_at, kind, pubkey, content, tags, sig) - VALUES (?, ?, ?, ?, ?, ?, ?)` + insertQb := squirrel.Insert("events"). + Columns("id", "created_at", "kind", "pubkey", "content", "tags", "sig"). + Values( + evt.ID.Hex(), + int64(evt.CreatedAt), + int(evt.Kind), + evt.PubKey.Hex(), + evt.Content, + string(tagsJSON), + hex.EncodeToString(evt.Sig[:]), + ) - _, err = s.db.Exec(query, - evt.ID.Hex(), - int64(evt.CreatedAt), - int(evt.Kind), - evt.PubKey.Hex(), - evt.Content, - string(tagsJSON), - hex.EncodeToString(evt.Sig[:]), - ) + _, err = insertQb.RunWith(s.db).Exec() if err != nil { return fmt.Errorf("failed to save event '%s': %w", evt.ID, err) @@ -48,8 +48,11 @@ func (s *SqliteBackend) SaveEvent(evt nostr.Event) error { // Insert single-letter tags into event_tags table for _, tag := range evt.Tags { if len(tag) >= 2 && len(tag[0]) == 1 { - _, err := s.db.Exec("INSERT INTO event_tags (event_id, key, value) VALUES (?, ?, ?)", - evt.ID.Hex(), tag[0], tag[1]) + tagQb := squirrel.Insert("event_tags"). + Columns("event_id", "key", "value"). + Values(evt.ID.Hex(), tag[0], tag[1]) + + _, err := tagQb.RunWith(s.db).Exec() if err != nil { // Log error but don't fail the entire save operation continue