Switch to squirrel
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
+2
-3
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package sqlite
|
||||
|
||||
// Helper functions and constants for the SQLite eventstore
|
||||
|
||||
const (
|
||||
// Database configuration
|
||||
defaultTimeout = 30 // seconds
|
||||
)
|
||||
+1
-5
@@ -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
|
||||
}
|
||||
|
||||
|
||||
+32
-57
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()}}
|
||||
|
||||
+20
-17
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user