From 34ad44385b68953473522a7f002d5db01e2b7e3c Mon Sep 17 00:00:00 2001 From: Attila Tajti Date: Tue, 2 Feb 2016 19:12:45 +0100 Subject: [PATCH] pkg/search: add parentof search expression Change-Id: I74f6f5411355a0b0864739f135331ba304245ddb --- doc/search-ui.txt | 2 ++ pkg/index/corpus.go | 20 +++++++++++ pkg/search/expr_test.go | 37 +++++++++++++++++++ pkg/search/predicate.go | 28 +++++++++++++++ pkg/search/query.go | 34 +++++++++++++----- pkg/search/query_test.go | 77 ++++++++++++++++++++++++++++++++++++++-- 6 files changed, 187 insertions(+), 11 deletions(-) diff --git a/doc/search-ui.txt b/doc/search-ui.txt index d67a2b276..2233fb4b6 100644 --- a/doc/search-ui.txt +++ b/doc/search-ui.txt @@ -50,3 +50,5 @@ Usable operators: filename: search for permanodes of files with this filename (case sensitive) childrenof: Find child permanodes of a parent permanode (or prefix of a parent permanode): childrenof:sha1-527cf12 + parentof: Find parent permanodes of a child permanode (or prefix + of a child permanode): parentof:sha1-527cf12 diff --git a/pkg/index/corpus.go b/pkg/index/corpus.go index 32df2f46e..1129e5edb 100644 --- a/pkg/index/corpus.go +++ b/pkg/index/corpus.go @@ -1287,6 +1287,26 @@ func (c *Corpus) PermanodeLatLongLocked(pn blob.Ref, at time.Time) (lat, long fl return } +// ForeachClaimLocked calls fn for each claim of permaNode. +// If at is zero, all claims are yielded. +// If at is non-zero, claims after that point are skipped. +// If fn returns false, iteration ends. +// Iteration is in an undefined order. +func (c *Corpus) ForeachClaimLocked(permaNode blob.Ref, at time.Time, fn func(*camtypes.Claim) bool) { + pm, ok := c.permanodes[permaNode] + if !ok { + return + } + for _, cl := range pm.Claims { + if !at.IsZero() && cl.Date.After(at) { + continue + } + if !fn(cl) { + return + } + } +} + // ForeachClaimBackLocked calls fn for each claim with a value referencing br. // If at is zero, all claims are yielded. // If at is non-zero, claims after that point are skipped. diff --git a/pkg/search/expr_test.go b/pkg/search/expr_test.go index ac94fcbe7..59624eeba 100644 --- a/pkg/search/expr_test.go +++ b/pkg/search/expr_test.go @@ -253,6 +253,29 @@ var parseExpressionTests = []struct { }, }, }, + + { + in: "parentof:sha1-f00ba4", + want: &SearchQuery{ + Constraint: &Constraint{ + Logical: &LogicalConstraint{ + Op: "and", + A: skiphiddenC, + B: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "child", + Any: &Constraint{ + BlobRefPrefix: "sha1-f00ba4", + }, + }, + }, + }, + }, + }, + }, + }, + // Location predicates { in: "loc:Uitdam", // Small dutch town @@ -760,6 +783,20 @@ var parseExpTests = []parserTestCase{ }, }, + { + in: "parentof:sha1-f00ba4", + want: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "child", + Any: &Constraint{ + BlobRefPrefix: "sha1-f00ba4", + }, + }, + }, + }, + }, + { name: "Unmatched quote", in: `is:pano and "foo`, diff --git a/pkg/search/predicate.go b/pkg/search/predicate.go index 8a92d5ec9..116e2f829 100644 --- a/pkg/search/predicate.go +++ b/pkg/search/predicate.go @@ -105,6 +105,7 @@ func init() { registerKeyword(newBefore()) registerKeyword(newAttribute()) registerKeyword(newChildrenOf()) + registerKeyword(newParentOf()) registerKeyword(newFormat()) registerKeyword(newTag()) registerKeyword(newTitle()) @@ -280,6 +281,33 @@ func (k childrenOf) Predicate(ctx context.Context, args []string) (*Constraint, return c, nil } +type parentOf struct { + matchPrefix +} + +func newParentOf() keyword { + return parentOf{newMatchPrefix("parentof")} +} + +func (k parentOf) Description() string { + return "Find parent permanodes of a child permanode (or prefix of a child\n" + + "permanode): parentof:sha1-527cf12 Only matches permanodes currently." +} + +func (k parentOf) Predicate(ctx context.Context, args []string) (*Constraint, error) { + c := &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "child", + Any: &Constraint{ + BlobRefPrefix: args[0], + }, + }, + }, + } + return c, nil +} + type format struct { matchPrefix } diff --git a/pkg/search/query.go b/pkg/search/query.go index be43516d3..90f1cbcb9 100644 --- a/pkg/search/query.go +++ b/pkg/search/query.go @@ -697,8 +697,8 @@ type RelationConstraint struct { } func (rc *RelationConstraint) checkValid() error { - if rc.Relation != "parent" { - return errors.New("only RelationConstraint.Relation of \"parent\" is currently supported") + if rc.Relation != "parent" && rc.Relation != "child" { + return errors.New("only RelationConstraint.Relation of \"parent\" or \"child\" is currently supported") } if (rc.Any == nil) == (rc.All == nil) { return errors.New("exactly one of RelationConstraint Any or All must be defined") @@ -721,7 +721,18 @@ func (rc *RelationConstraint) match(s *search, pn blob.Ref, at time.Time) (ok bo return false, errors.New("RelationConstraint requires an in-memory corpus") } - if rc.Relation != "parent" { + var foreachClaim func(pn blob.Ref, at time.Time, f func(cl *camtypes.Claim) bool) + // relationRef returns the relevant blobRef from the claim if cl defines + // the kind of relation we are looking for, (blob.Ref{}, false) otherwise. + var relationRef func(cl *camtypes.Claim) (blob.Ref, bool) + switch rc.Relation { + case "parent": + foreachClaim = corpus.ForeachClaimBackLocked + relationRef = func(cl *camtypes.Claim) (blob.Ref, bool) { return cl.Permanode, true } + case "child": + foreachClaim = corpus.ForeachClaimLocked + relationRef = func(cl *camtypes.Claim) (blob.Ref, bool) { return blob.Parse(cl.Value) } + default: panic("bogus") } @@ -736,7 +747,7 @@ func (rc *RelationConstraint) match(s *search, pn blob.Ref, at time.Time) (ok bo var anyBad bool var lastChecked blob.Ref var permanodesChecked map[blob.Ref]bool // lazily created to optimize for common case of 1 match - corpus.ForeachClaimBackLocked(pn, at, func(cl *camtypes.Claim) bool { + foreachClaim(pn, at, func(cl *camtypes.Claim) bool { if !rc.matchesAttr(cl.Attr) { return true // skip claim } @@ -747,7 +758,13 @@ func (rc *RelationConstraint) match(s *search, pn blob.Ref, at time.Time) (ok bo permanodesChecked[lastChecked] = true lastChecked = blob.Ref{} // back to zero } - if permanodesChecked[cl.Permanode] { + relRef, ok := relationRef(cl) + if !ok { + // The claim does not define the kind of relation we're looking for + // (e.g. it sets a tag vale), so we continue to the next claim. + return true + } + if permanodesChecked[relRef] { return true // skip checking } if !corpus.PermanodeHasAttrValueLocked(cl.Permanode, at, cl.Attr, cl.Value) { @@ -755,12 +772,11 @@ func (rc *RelationConstraint) match(s *search, pn blob.Ref, at time.Time) (ok bo } var bm camtypes.BlobMeta - bm, err = s.blobMeta(cl.Permanode) + bm, err = s.blobMeta(relRef) if err != nil { return false } - var ok bool - ok, err = matcher(s, cl.Permanode, bm) + ok, err = matcher(s, relRef, bm) if err != nil { return false } @@ -775,7 +791,7 @@ func (rc *RelationConstraint) match(s *search, pn blob.Ref, at time.Time) (ok bo return false // fail fast } } - lastChecked = cl.Permanode + lastChecked = relRef return true }) if err != nil { diff --git a/pkg/search/query_test.go b/pkg/search/query_test.go index 898094589..f0d219461 100644 --- a/pkg/search/query_test.go +++ b/pkg/search/query_test.go @@ -7,6 +7,7 @@ import ( "reflect" "sort" "strings" + "sync" "testing" "time" @@ -53,7 +54,14 @@ type queryTest struct { id *indextest.IndexDeps itype indexType - Handler func() *Handler + handlerOnce sync.Once + newHandler func() *Handler + handler *Handler // initialized with newHandler +} + +func (qt *queryTest) Handler() *Handler { + qt.handlerOnce.Do(func() { qt.handler = qt.newHandler() }) + return qt.handler } func querySetup(t testing.TB) (*indextest.IndexDeps, *Handler) { @@ -96,7 +104,7 @@ func testQueryType(t testing.TB, fn func(*queryTest), itype indexType) { itype: itype, } qt.id.Fataler = t - qt.Handler = func() *Handler { + qt.newHandler = func() *Handler { h := NewHandler(idx, qt.id.SignerBlobRef) if itype == indexCorpusScan { if corpus, err = idx.KeepInMemory(); err != nil { @@ -999,6 +1007,71 @@ func TestQueryChildren(t *testing.T) { }) } +func TestQueryParent(t *testing.T) { + testQueryTypes(t, memIndexTypes, func(qt *queryTest) { + id := qt.id + + pdir1 := id.NewPlannedPermanode("some_dir_1") + pdir2 := id.NewPlannedPermanode("some_dir_2") + p1 := id.NewPlannedPermanode("1") + p2 := id.NewPlannedPermanode("2") + p3 := id.NewPlannedPermanode("3") + + id.AddAttribute(pdir1, "camliMember", p1.String()) + id.AddAttribute(pdir1, "camliPath:foo", p2.String()) + id.AddAttribute(pdir1, "other", p3.String()) + + id.AddAttribute(pdir2, "camliPath:bar", p1.String()) + + // Make p1, p2, and p3 actually exist. (permanodes without attributes are dead) + id.AddAttribute(p1, "x", "x") + id.AddAttribute(p2, "x", "x") + id.AddAttribute(p3, "x", "x") + + sq := &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "child", + Any: &Constraint{ + BlobRefPrefix: p1.String(), + }, + }, + }, + }, + } + qt.wantRes(sq, pdir1, pdir2) + + sq = &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "child", + Any: &Constraint{ + BlobRefPrefix: p2.String(), + }, + }, + }, + }, + } + qt.wantRes(sq, pdir1) + + sq = &SearchQuery{ + Constraint: &Constraint{ + Permanode: &PermanodeConstraint{ + Relation: &RelationConstraint{ + Relation: "child", + Any: &Constraint{ + BlobRefPrefix: p3.String(), + }, + }, + }, + }, + } + qt.wantRes(sq) + }) +} + // 13 permanodes are created. 1 of them the parent, 11 are children // (== results), 1 is unrelated to the parent. // limit is the limit on the number of results.