From e4c702d06f7056450f0d6e0907d8e9dae4592ab3 Mon Sep 17 00:00:00 2001 From: Dionysos <75300347+ice-dionysos@users.noreply.github.com> Date: Mon, 2 Dec 2024 20:46:10 +0000 Subject: [PATCH] database: extend `expiration` filter (#34) --- database/query/query_fuzz_test.go | 8 +- database/query/query_where_builder.go | 11 ++- database/query/query_where_builder_filter.go | 6 ++ .../query/query_where_dependencies_parser.go | 5 +- .../query_where_dependencies_parser_test.go | 73 +++++++++++++++++++ 5 files changed, 99 insertions(+), 4 deletions(-) diff --git a/database/query/query_fuzz_test.go b/database/query/query_fuzz_test.go index 45dcd8c..2af94c8 100644 --- a/database/query/query_fuzz_test.go +++ b/database/query/query_fuzz_test.go @@ -193,7 +193,8 @@ func TestQueryFuzzNoUseTempBTREEOrScan(t *testing.T) { t.Run("Fuzz", func(t *testing.T) { for i, set := range sets { - sql, params, err := generateSelectEventsSQL(&model.Subscription{Filters: model.Filters{helperNewFilterFromElements(t, set)}}, 0, 100) + filter := helperNewFilterFromElements(t, set) + sql, params, err := generateSelectEventsSQL(&model.Subscription{Filters: model.Filters{filter}}, 0, 100) require.NoErrorf(t, err, "failed to generate select events sql for set #%d (%#v)", i+1, set) sql = "EXPLAIN QUERY PLAN " + sql @@ -208,6 +209,11 @@ func TestQueryFuzzNoUseTempBTREEOrScan(t *testing.T) { require.NoError(t, err) op[s4]++ if s4 == "USE TEMP B-TREE FOR ORDER BY" || (strings.HasPrefix(s4, "SCAN ") && !strings.Contains(s4, "INDEX")) { + if strings.Contains(filter.Search, "Expiration:true") { + // It uses SCAN over CTE, which is expected. + continue + } + t.Logf("filter: %#v", filter) t.Logf("set #%d: %s (%+v)", i+1, sql, params) t.Log(s1, s2, s3, s4) t.FailNow() diff --git a/database/query/query_where_builder.go b/database/query/query_where_builder.go index 2aa78a3..a3325b0 100644 --- a/database/query/query_where_builder.go +++ b/database/query/query_where_builder.go @@ -520,7 +520,7 @@ func (w *whereBuilder) createWhereForDepFilter(filterID, cteName, field string, } func (w *whereBuilder) applyDepFilter(filterID, cteName string, filter *filterDependencies) { - if filter.Reduce.Kinds[0] == model.KindDVMCountResponse { + if len(filter.Reduce.Kinds) > 0 && filter.Reduce.Kinds[0] == model.KindDVMCountResponse { w.WriteString(` union all select @@ -572,6 +572,15 @@ where w.WriteString(`) AND `) } + if filter.Expiration != nil && *filter.Expiration { + w.WriteString(`e.master_pubkey IN (select master_pubkey from ` + cteName + `) +and e.hidden=0 +and e.kind in (1, 30023) +and exists (select true from event_tags where event_id = e.id and event_tag_key = 'expiration' and cast(` + tagValueExpiration + ` as integer) > unixepoch())`) + + return + } + switch filter.Reduce.Kinds[0] { case nostr.KindTextNote, nostr.KindRepost, nostr.KindReaction, nostr.KindArticle, nostr.KindGenericRepost: w.WriteString("e.kind = :") diff --git a/database/query/query_where_builder_filter.go b/database/query/query_where_builder_filter.go index f92f974..5cd8c65 100644 --- a/database/query/query_where_builder_filter.go +++ b/database/query/query_where_builder_filter.go @@ -114,6 +114,12 @@ func parseNostrFilter(filter model.Filter) (*databaseFilterSearch, error) { return nil, err } + if f.Expiration != nil && *f.Expiration { + f.Dependencies = append(f.Dependencies, &filterDependencies{ + Expiration: f.Expiration, + }) + } + f.Search = strings.TrimSpace(f.Search) return f, nil diff --git a/database/query/query_where_dependencies_parser.go b/database/query/query_where_dependencies_parser.go index 4746c4c..227d674 100644 --- a/database/query/query_where_dependencies_parser.go +++ b/database/query/query_where_dependencies_parser.go @@ -27,8 +27,9 @@ type ( } filterDependencies struct { - Start filterDependenciesStart - Reduce filterDependenciesReduce + Start filterDependenciesStart + Reduce filterDependenciesReduce + Expiration *bool } filterSequence struct { diff --git a/database/query/query_where_dependencies_parser_test.go b/database/query/query_where_dependencies_parser_test.go index b1e4e25..aa862c2 100644 --- a/database/query/query_where_dependencies_parser_test.go +++ b/database/query/query_where_dependencies_parser_test.go @@ -5,7 +5,9 @@ package query import ( "context" "encoding/json" + "strconv" "testing" + "time" "github.com/nbd-wtf/go-nostr" "github.com/stretchr/testify/require" @@ -678,3 +680,74 @@ func TestSelectWithDependencies(t *testing.T) { }) }) } + +func TestSelectExpirationWithDependencies(t *testing.T) { + t.Parallel() + db := helperNewDatabase(t) + defer db.Close() + + now := time.Now().Unix() + expiration := strconv.FormatInt(now+3600, 10) + + t.Run("Insert", func(t *testing.T) { + require.NoError(t, db.AcceptEvents(context.Background(), + &model.Event{ + Event: nostr.Event{ + ID: "t1id1", + PubKey: "t1pk1", + Content: "content1", + Kind: nostr.KindTextNote, + CreatedAt: 1, + Tags: model.Tags{{"expiration", expiration}}, + }, + }, + &model.Event{ + Event: nostr.Event{ + ID: "t1id2", + PubKey: "t1pk2", + Content: "content2", + Kind: nostr.KindTextNote, + CreatedAt: 2, + Tags: model.Tags{{"expiration", expiration}}, + }, + }, + &model.Event{ + Event: nostr.Event{ + ID: "t1id3", + PubKey: "t1pk2", + Content: "content3", + Kind: nostr.KindTextNote, + CreatedAt: 3, + Tags: model.Tags{{"expiration", expiration}}, + }, + }, + &model.Event{ + Event: nostr.Event{ + ID: "t1id4", + PubKey: "t1pk4", + Content: "content4", + Kind: nostr.KindTextNote, + CreatedAt: 4, + Tags: model.Tags{{"expiration", expiration}}, + }, + }, + &model.Event{ + Event: nostr.Event{ + ID: "t1id5", + PubKey: "t1pk1", + Content: "content5", + Kind: nostr.KindTextNote, + CreatedAt: 5, + Tags: model.Tags{{"expiration", expiration}}, + }, + }, + )) + }) + + events := helperSelectEvents(t, db, model.Filter{Limit: 2, Search: "expiration:true"}) + require.Len(t, events, 3) // main: (t1pk1 + t1pk4) + dep: (t1pk1 / t1id1). + for i, pubkey := range []string{"t1pk1", "t1pk4", "t1pk1"} { + t.Logf("event %d: %+v", i, events[i]) + require.Equal(t, pubkey, events[i].PubKey) + } +}