From d0ca579f5c4e8089c061e1b9b70d4ed4bcd6098a Mon Sep 17 00:00:00 2001 From: Alex Roitman Date: Mon, 10 Jun 2019 14:47:42 -0700 Subject: [PATCH 1/9] Add area expressions. Add parser. Hook up to test command. Add tests for expressions in test command. --- internal/server/expression.go | 192 ++++++++++++++++++++++++++++++++++ internal/server/test.go | 17 +-- internal/server/token.go | 143 +++++++++++++++++++++++++ tests/testcmd_test.go | 49 +++++++++ 4 files changed, 395 insertions(+), 6 deletions(-) create mode 100644 internal/server/expression.go diff --git a/internal/server/expression.go b/internal/server/expression.go new file mode 100644 index 00000000..0fd87471 --- /dev/null +++ b/internal/server/expression.go @@ -0,0 +1,192 @@ +package server + +import ( + "strings" + + "github.com/tidwall/geojson" +) + +type BinaryOp byte + +const ( + NOOP BinaryOp = iota + AND + OR +) + +// areaExpression is either an object or operator+children +type areaExpression struct { + negate bool + obj geojson.Object + op BinaryOp + children children +} + +type children []*areaExpression + +func (e *areaExpression) String() string { + if e.obj != nil { + return e.obj.String() + } + var chStrings []string + for _, c := range e.children { + chStrings = append(chStrings, c.String()) + } + switch e.op { + case NOOP: + return "empty operator" + case AND: + return "(" + strings.Join(chStrings, " AND ") + ")" + case OR: + return "(" + strings.Join(chStrings, " OR ") + ")" + } + return "unknown operator" +} + +// Return boolean value modulo negate field of the expression. +func (e *areaExpression) booleanize(val bool) bool { + if e.negate { + return !val + } + return val +} + +func (e *areaExpression) Intersects(o geojson.Object) bool { + if e.obj != nil { + return e.booleanize(e.obj.Intersects(o)) + } + switch e.op { + case AND: + for _, c := range e.children { + if !c.Intersects(o) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c := range e.children { + if c.Intersects(o) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} + +// object within an expression means anything of this expression contains object +func (e *areaExpression) Contains(o geojson.Object) bool { + if e.obj != nil { + return e.booleanize(e.obj.Contains(o)) + } + switch e.op { + case AND: + for _, c:= range e.children { + if !c.Contains(o) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c:= range e.children { + if c.Contains(o) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} + +func (e *areaExpression) Within(o geojson.Object) bool { + if e.obj != nil { + return e.booleanize(e.obj.Within(o)) + } + switch e.op { + case AND: + for _, c:= range e.children { + if !c.Within(o) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c:= range e.children { + if c.Within(o) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} + +func (e *areaExpression) IntersectsExpr(oe *areaExpression) bool { + if oe.obj != nil { + return oe.booleanize(e.Intersects(oe.obj)) + } + switch oe.op { + case AND: + for _, c := range oe.children { + if !e.IntersectsExpr(c) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c := range oe.children { + if e.IntersectsExpr(c) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) + +} + +func (e *areaExpression) WithinExpr(oe *areaExpression) bool { + if oe.obj != nil { + return oe.booleanize(e.Within(oe.obj)) + } + switch oe.op { + case AND: + for _, c:= range oe.children { + if !e.WithinExpr(c) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c:= range oe.children { + if e.WithinExpr(c) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} + +func (e *areaExpression) ContainsExpr(oe *areaExpression) bool { + if oe.obj != nil { + return oe.booleanize(e.Contains(oe.obj)) + } + switch oe.op { + case AND: + for _, c:= range oe.children { + if !e.ContainsExpr(c) { + return e.booleanize(false) + } + } + return e.booleanize(true) + case OR: + for _, c:= range oe.children { + if e.ContainsExpr(c) { + return e.booleanize(true) + } + } + return e.booleanize(false) + } + return e.booleanize(false) +} diff --git a/internal/server/test.go b/internal/server/test.go index 267f3c27..e276367c 100644 --- a/internal/server/test.go +++ b/internal/server/test.go @@ -231,8 +231,9 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) { var ok bool var test string - var obj1, obj2, clipped geojson.Object - if vs, obj1, err = s.parseArea(vs, false); err != nil { + var clipped geojson.Object + var area1, area2 *areaExpression + if vs, area1, err = s.parseAreaExpression(vs, false); err != nil { return } if vs, test, ok = tokenval(vs); !ok || test == "" { @@ -259,7 +260,11 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) { doClip = true } } - if vs, obj2, err = s.parseArea(vs, doClip); err != nil { + if vs, area2, err = s.parseAreaExpression(vs, doClip); err != nil { + return + } + if doClip && (area1.obj == nil || area2.obj == nil) { + err = errInvalidArgument("clip") return } if len(vs) != 0 { @@ -268,14 +273,14 @@ func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) { var result int if lTest == "within" { - if obj1.Within(obj2) { + if area1.WithinExpr(area2) { result = 1 } } else if lTest == "intersects" { - if obj1.Intersects(obj2) { + if area1.IntersectsExpr(area2) { result = 1 if doClip { - clipped = clip.Clip(obj1, obj2) + clipped = clip.Clip(area1.obj, area2.obj) } } } diff --git a/internal/server/token.go b/internal/server/token.go index 71a81044..5adcdc56 100644 --- a/internal/server/token.go +++ b/internal/server/token.go @@ -688,3 +688,146 @@ func (c *Server) parseSearchScanBaseTokens( tout = t return } + +type parentStack []*areaExpression + +func (ps *parentStack) isEmpty() bool { + return len(*ps) == 0 +} + +func (ps *parentStack) push(e *areaExpression) { + *ps = append(*ps, e) +} + +func (ps *parentStack) pop() (e *areaExpression, empty bool) { + n := len(*ps) + if n == 0 { + return nil, true + } + x := (*ps)[n-1] + *ps = (*ps)[:n-1] + return x, false +} + +func (s *Server) parseAreaExpression(vsin []string, doClip bool) (vsout []string, ae *areaExpression, err error) { + ps := &parentStack{} + vsout = vsin[:] + var negate bool +loop: + for { + nvs, wtok, ok := tokenval(vsout) + if !ok || len(wtok) == 0 { + break + } + switch strings.ToLower(wtok) { + case "(": + newExpr := &areaExpression{negate: negate, op: NOOP} + negate = false + if ae != nil { + ps.push(ae) + ae.children = append(ae.children, newExpr) + } + ae = newExpr + vsout = nvs + case ")": + if negate { + err = errInvalidArgument("NOT") + return + } + if parent, empty := ps.pop(); empty { + err = errInvalidArgument(")") + return + } else { + ae = parent + } + vsout = nvs + case "not": + negate = true + vsout = nvs + case "and": + if negate { + err = errInvalidArgument("NOT") + return + } + if ae == nil { + err = errInvalidArgument("AND") + return + } else if ae.obj == nil { + switch ae.op { + case OR: + numChildren := len(ae.children) + if numChildren < 2 { + err = errInvalidNumberOfArguments + return + } else { + ae.children = append( + ae.children[:numChildren-1], + &areaExpression{ + op: AND, + children: []*areaExpression{ae.children[numChildren-1]}}) + } + case NOOP: + ae.op = AND + } + } else { + ae = &areaExpression{op: AND, children: []*areaExpression{ae}} + } + vsout = nvs + case "or": + if negate { + err = errInvalidArgument("NOT") + return + } + if ae == nil { + err = errInvalidArgument("OR") + return + } else if ae.obj == nil { + switch ae.op { + case AND: + if len(ae.children) < 2 { + err = errInvalidNumberOfArguments + return + } else { + parent, empty := ps.pop() + if empty { + parent = ae + } + parent.children = append( + parent.children, + &areaExpression{op: OR}) + ps.push(parent) + } + case NOOP: + ae.op = OR + } + } else { + ae = &areaExpression{op: OR, children: []*areaExpression{ae}} + } + vsout = nvs + case "point", "circle", "object", "bounds", "hash", "quadkey", "tile", "get": + if parsedVs, parsedObj, areaErr := s.parseArea(vsout, doClip); areaErr != nil { + err = areaErr + return + } else { + newExpr := &areaExpression{negate: negate, obj: parsedObj, op: NOOP} + negate = false + if ae == nil { + ae = newExpr + } else { + ae.children = append(ae.children, newExpr) + } + vsout = parsedVs + } + default: + if negate { + err = errInvalidArgument("NOT") + return + } + break loop + } + } + if prevExpr, empty := ps.pop(); !empty { + ae = prevExpr + } + return +} diff --git a/tests/testcmd_test.go b/tests/testcmd_test.go index d6d65231..2b67d3ef 100644 --- a/tests/testcmd_test.go +++ b/tests/testcmd_test.go @@ -8,6 +8,7 @@ func subTestTestCmd(t *testing.T, mc *mockServer) { runStep(t, mc, "WITHIN", testcmd_WITHIN_test) runStep(t, mc, "INTERSECTS", testcmd_INTERSECTS_test) runStep(t, mc, "INTERSECTS_CLIP", testcmd_INTERSECTS_CLIP_test) + runStep(t, mc, "Expressions", testcmd_expression_test) } func testcmd_WITHIN_test(mc *mockServer) error { @@ -115,3 +116,51 @@ func testcmd_INTERSECTS_CLIP_test(mc *mockServer) error { {"TEST", "OBJECT", poly101, "INTERSECTS", "CLIP", "BOUNDS", 37.73315644825698, -122.44054287672043, 37.73349585185455, -122.44008690118788}, {"0"}, }) } + +func testcmd_expression_test(mc *mockServer) error { + poly := `{ + "type": "Polygon", + "coordinates": [ + [ + [-122.44126439094543,37.732906137107], + [-122.43980526924135,37.732906137107], + [-122.43980526924135,37.73421283683962], + [-122.44126439094543,37.73421283683962], + [-122.44126439094543,37.732906137107] + ] + ] + }` + poly8 := `{"type":"Polygon","coordinates":[[[-122.4408378,37.7341129],[-122.4408378,37.733],[-122.44,37.733],[-122.44,37.7341129],[-122.4408378,37.7341129]],[[-122.44060993194579,37.73345766902749],[-122.44044363498686,37.73345766902749],[-122.44044363498686,37.73355524732416],[-122.44060993194579,37.73355524732416],[-122.44060993194579,37.73345766902749]],[[-122.44060724973677,37.7336888869566],[-122.4402102828026,37.7336888869566],[-122.4402102828026,37.7339752567853],[-122.44060724973677,37.7339752567853],[-122.44060724973677,37.7336888869566]]]}` + poly9 := `{"type": "Polygon","coordinates": [[[-122.44037926197052,37.73313523548048],[-122.44017541408539,37.73313523548048],[-122.44017541408539,37.73336857568778],[-122.44037926197052,37.73336857568778],[-122.44037926197052,37.73313523548048]]]}` + + return mc.DoBatch([][]interface{}{ + {"SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, + {"SET", "mykey", "poly8", "OBJECT", poly8}, {"OK"}, + + {"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "OR", "OBJECT", poly}, {"1"}, + + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3"}, {"0"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")"}, {"0"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "GET", "mykey", "line3"}, {"1"}, + + {"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, + + {"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "line3"}, {"0"}, + {"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "AND", "GET", "mykey", "line3", ")"}, {"0"}, + {"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "OR", "GET", "mykey", "line3", ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "WITHIN", "GET", "mykey", "poly8", "AND", + "(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "WITHIN", "NOT", "GET", "mykey", "line3"}, {"1"}, + }) + +} From 496ace25d3099bb49ba011336dff793286428de4 Mon Sep 17 00:00:00 2001 From: Alex Roitman Date: Tue, 11 Jun 2019 17:13:33 -0700 Subject: [PATCH 2/9] Refactoring/cleanup/fixes. --- internal/server/expression.go | 246 ++++++++++++++++++++-------------- internal/server/token.go | 24 ++-- tests/testcmd_test.go | 1 - 3 files changed, 160 insertions(+), 111 deletions(-) diff --git a/internal/server/expression.go b/internal/server/expression.go index 0fd87471..6ad4478f 100644 --- a/internal/server/expression.go +++ b/internal/server/expression.go @@ -12,6 +12,11 @@ const ( NOOP BinaryOp = iota AND OR + tokenAND = "and" + tokenOR = "or" + tokenNOT = "not" + tokenLParen = "(" + tokenRParen = ")" ) // areaExpression is either an object or operator+children @@ -24,169 +29,214 @@ type areaExpression struct { type children []*areaExpression -func (e *areaExpression) String() string { +func (e *areaExpression) String() (res string) { if e.obj != nil { - return e.obj.String() + res = e.obj.String() + } else { + var chStrings []string + for _, c := range e.children { + chStrings = append(chStrings, c.String()) + } + switch e.op { + case NOOP: + res = "empty operator" + case AND: + res = "(" + strings.Join(chStrings, " "+tokenAND+" ") + ")" + case OR: + res = "(" + strings.Join(chStrings, " "+tokenOR+" ") + ")" + default: + res = "unknown operator" + } } - var chStrings []string - for _, c := range e.children { - chStrings = append(chStrings, c.String()) + if e.negate { + res = tokenNOT + " " + res } - switch e.op { - case NOOP: - return "empty operator" - case AND: - return "(" + strings.Join(chStrings, " AND ") + ")" - case OR: - return "(" + strings.Join(chStrings, " OR ") + ")" - } - return "unknown operator" + return } // Return boolean value modulo negate field of the expression. -func (e *areaExpression) booleanize(val bool) bool { +func (e *areaExpression) maybeNegate(val bool) bool { if e.negate { return !val } return val } -func (e *areaExpression) Intersects(o geojson.Object) bool { +// Methods for testing an areaExpression against the spatial object +func (e *areaExpression) rawIntersects(o geojson.Object) bool { if e.obj != nil { - return e.booleanize(e.obj.Intersects(o)) + return e.obj.Intersects(o) } switch e.op { case AND: for _, c := range e.children { if !c.Intersects(o) { - return e.booleanize(false) + return false } } - return e.booleanize(true) + return true case OR: for _, c := range e.children { if c.Intersects(o) { - return e.booleanize(true) + return true } } - return e.booleanize(false) + return false } - return e.booleanize(false) + return false } -// object within an expression means anything of this expression contains object -func (e *areaExpression) Contains(o geojson.Object) bool { +func (e *areaExpression) rawContains(o geojson.Object) bool { if e.obj != nil { - return e.booleanize(e.obj.Contains(o)) + return e.obj.Contains(o) } switch e.op { case AND: for _, c:= range e.children { if !c.Contains(o) { - return e.booleanize(false) + return false } } - return e.booleanize(true) + return true case OR: for _, c:= range e.children { if c.Contains(o) { - return e.booleanize(true) + return true } } - return e.booleanize(false) + return false } - return e.booleanize(false) + return false } -func (e *areaExpression) Within(o geojson.Object) bool { +func (e *areaExpression) rawWithin(o geojson.Object) bool { if e.obj != nil { - return e.booleanize(e.obj.Within(o)) + return e.obj.Within(o) } switch e.op { case AND: for _, c:= range e.children { if !c.Within(o) { - return e.booleanize(false) + return false } } - return e.booleanize(true) + return true case OR: for _, c:= range e.children { if c.Within(o) { - return e.booleanize(true) + return true } } - return e.booleanize(false) + return false } - return e.booleanize(false) + return false +} + +func (e *areaExpression) Intersects(o geojson.Object) bool { + return e.maybeNegate(e.rawIntersects(o)) +} + +func (e *areaExpression) Contains(o geojson.Object) bool { + return e.maybeNegate(e.rawContains(o)) +} + +func (e *areaExpression) Within(o geojson.Object) bool { + return e.maybeNegate(e.rawWithin(o)) +} + +// Methods for testing an areaExpression against another areaExpression +func (e *areaExpression) rawIntersectsExpr(oe *areaExpression) bool { + if oe.negate { + e2 := &areaExpression{negate:!e.negate, obj:e.obj, op: e.op, children:e.children} + oe2 := &areaExpression{negate:false, obj:oe.obj, op:oe.op, children:oe.children} + return e2.rawIntersectsExpr(oe2) + } + if oe.obj != nil { + return e.Intersects(oe.obj) + } + switch oe.op { + case AND: + for _, c := range oe.children { + if !e.rawIntersectsExpr(c) { + return false + } + } + return true + case OR: + for _, c := range oe.children { + if e.rawIntersectsExpr(c) { + return true + } + } + return false + } + return false +} + +func (e *areaExpression) rawWithinExpr(oe *areaExpression) bool { + if oe.negate { + e2 := &areaExpression{negate:!e.negate, obj:e.obj, op: e.op, children:e.children} + oe2 := &areaExpression{negate:false, obj:oe.obj, op:oe.op, children:oe.children} + return e2.rawWithinExpr(oe2) + } + if oe.obj != nil { + return e.Within(oe.obj) + } + switch oe.op { + case AND: + for _, c:= range oe.children { + if !e.rawWithinExpr(c) { + return false + } + } + return true + case OR: + for _, c:= range oe.children { + if e.rawWithinExpr(c) { + return true + } + } + return false + } + return false +} + +func (e *areaExpression) rawContainsExpr(oe *areaExpression) bool { + if oe.negate { + e2 := &areaExpression{negate:!e.negate, obj:e.obj, op: e.op, children:e.children} + oe2 := &areaExpression{negate:false, obj:oe.obj, op:oe.op, children:oe.children} + return e2.rawContainsExpr(oe2) + } + if oe.obj != nil { + return e.Contains(oe.obj) + } + switch oe.op { + case AND: + for _, c:= range oe.children { + if !e.rawContainsExpr(c) { + return false + } + } + return true + case OR: + for _, c:= range oe.children { + if e.rawContainsExpr(c) { + return true + } + } + return false + } + return false } func (e *areaExpression) IntersectsExpr(oe *areaExpression) bool { - if oe.obj != nil { - return oe.booleanize(e.Intersects(oe.obj)) - } - switch oe.op { - case AND: - for _, c := range oe.children { - if !e.IntersectsExpr(c) { - return e.booleanize(false) - } - } - return e.booleanize(true) - case OR: - for _, c := range oe.children { - if e.IntersectsExpr(c) { - return e.booleanize(true) - } - } - return e.booleanize(false) - } - return e.booleanize(false) - + return e.maybeNegate(e.rawIntersectsExpr(oe)) } func (e *areaExpression) WithinExpr(oe *areaExpression) bool { - if oe.obj != nil { - return oe.booleanize(e.Within(oe.obj)) - } - switch oe.op { - case AND: - for _, c:= range oe.children { - if !e.WithinExpr(c) { - return e.booleanize(false) - } - } - return e.booleanize(true) - case OR: - for _, c:= range oe.children { - if e.WithinExpr(c) { - return e.booleanize(true) - } - } - return e.booleanize(false) - } - return e.booleanize(false) + return e.maybeNegate(e.rawWithinExpr(oe)) } func (e *areaExpression) ContainsExpr(oe *areaExpression) bool { - if oe.obj != nil { - return oe.booleanize(e.Contains(oe.obj)) - } - switch oe.op { - case AND: - for _, c:= range oe.children { - if !e.ContainsExpr(c) { - return e.booleanize(false) - } - } - return e.booleanize(true) - case OR: - for _, c:= range oe.children { - if e.ContainsExpr(c) { - return e.booleanize(true) - } - } - return e.booleanize(false) - } - return e.booleanize(false) + return e.maybeNegate(e.rawContainsExpr(oe)) } diff --git a/internal/server/token.go b/internal/server/token.go index 5adcdc56..5b8c26a5 100644 --- a/internal/server/token.go +++ b/internal/server/token.go @@ -720,7 +720,7 @@ loop: break } switch strings.ToLower(wtok) { - case "(": + case tokenLParen: newExpr := &areaExpression{negate: negate, op: NOOP} negate = false if ae != nil { @@ -729,28 +729,28 @@ loop: } ae = newExpr vsout = nvs - case ")": + case tokenRParen: if negate { - err = errInvalidArgument("NOT") + err = errInvalidArgument(tokenNOT) return } if parent, empty := ps.pop(); empty { - err = errInvalidArgument(")") + err = errInvalidArgument(tokenRParen) return } else { ae = parent } vsout = nvs - case "not": + case tokenNOT: negate = true vsout = nvs - case "and": + case tokenAND: if negate { - err = errInvalidArgument("NOT") + err = errInvalidArgument(tokenNOT) return } if ae == nil { - err = errInvalidArgument("AND") + err = errInvalidArgument(tokenAND) return } else if ae.obj == nil { switch ae.op { @@ -773,13 +773,13 @@ loop: ae = &areaExpression{op: AND, children: []*areaExpression{ae}} } vsout = nvs - case "or": + case tokenOR: if negate { - err = errInvalidArgument("NOT") + err = errInvalidArgument(tokenNOT) return } if ae == nil { - err = errInvalidArgument("OR") + err = errInvalidArgument(tokenOR) return } else if ae.obj == nil { switch ae.op { @@ -820,7 +820,7 @@ loop: } default: if negate { - err = errInvalidArgument("NOT") + err = errInvalidArgument(tokenNOT) return } break loop diff --git a/tests/testcmd_test.go b/tests/testcmd_test.go index 2b67d3ef..0a5ba0a1 100644 --- a/tests/testcmd_test.go +++ b/tests/testcmd_test.go @@ -162,5 +162,4 @@ func testcmd_expression_test(mc *mockServer) error { "(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"}, {"TEST", "OBJECT", poly9, "WITHIN", "NOT", "GET", "mykey", "line3"}, {"1"}, }) - } From 81f57ba6f6799e6b6110dcb7a96b928262ba6467 Mon Sep 17 00:00:00 2001 From: Alex Roitman Date: Thu, 13 Jun 2019 09:53:17 -0700 Subject: [PATCH 3/9] Fix one case in expression/expression tests --- internal/server/expression.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/server/expression.go b/internal/server/expression.go index 6ad4478f..39c95b58 100644 --- a/internal/server/expression.go +++ b/internal/server/expression.go @@ -149,10 +149,10 @@ func (e *areaExpression) rawIntersectsExpr(oe *areaExpression) bool { if oe.negate { e2 := &areaExpression{negate:!e.negate, obj:e.obj, op: e.op, children:e.children} oe2 := &areaExpression{negate:false, obj:oe.obj, op:oe.op, children:oe.children} - return e2.rawIntersectsExpr(oe2) + return e2.IntersectsExpr(oe2) } if oe.obj != nil { - return e.Intersects(oe.obj) + return e.rawIntersects(oe.obj) } switch oe.op { case AND: @@ -177,10 +177,10 @@ func (e *areaExpression) rawWithinExpr(oe *areaExpression) bool { if oe.negate { e2 := &areaExpression{negate:!e.negate, obj:e.obj, op: e.op, children:e.children} oe2 := &areaExpression{negate:false, obj:oe.obj, op:oe.op, children:oe.children} - return e2.rawWithinExpr(oe2) + return e2.WithinExpr(oe2) } if oe.obj != nil { - return e.Within(oe.obj) + return e.rawWithin(oe.obj) } switch oe.op { case AND: @@ -205,10 +205,10 @@ func (e *areaExpression) rawContainsExpr(oe *areaExpression) bool { if oe.negate { e2 := &areaExpression{negate:!e.negate, obj:e.obj, op: e.op, children:e.children} oe2 := &areaExpression{negate:false, obj:oe.obj, op:oe.op, children:oe.children} - return e2.rawContainsExpr(oe2) + return e2.ContainsExpr(oe2) } if oe.obj != nil { - return e.Contains(oe.obj) + return e.rawContains(oe.obj) } switch oe.op { case AND: From 3ded4e3a4493c40f4b9eda4a5861fda174abd530 Mon Sep 17 00:00:00 2001 From: Alex Roitman Date: Thu, 13 Jun 2019 10:56:33 -0700 Subject: [PATCH 4/9] Refactor using method expressions, to avoid repeating code. --- internal/server/expression.go | 152 +++++++++++----------------------- 1 file changed, 48 insertions(+), 104 deletions(-) diff --git a/internal/server/expression.go b/internal/server/expression.go index 39c95b58..4bc51d7f 100644 --- a/internal/server/expression.go +++ b/internal/server/expression.go @@ -21,14 +21,15 @@ const ( // areaExpression is either an object or operator+children type areaExpression struct { - negate bool - obj geojson.Object - op BinaryOp + negate bool + obj geojson.Object + op BinaryOp children children } type children []*areaExpression +// String representation, helpful in logging. func (e *areaExpression) String() (res string) { if e.obj != nil { res = e.obj.String() @@ -63,21 +64,25 @@ func (e *areaExpression) maybeNegate(val bool) bool { } // Methods for testing an areaExpression against the spatial object -func (e *areaExpression) rawIntersects(o geojson.Object) bool { +func (e *areaExpression) testObject( + o geojson.Object, + objObjTest func(o1, o2 geojson.Object) bool, + exprObjTest func(ae *areaExpression, ob geojson.Object) bool, +) bool { if e.obj != nil { - return e.obj.Intersects(o) + return objObjTest(e.obj, o) } switch e.op { case AND: for _, c := range e.children { - if !c.Intersects(o) { + if !exprObjTest(c, o) { return false } } return true case OR: for _, c := range e.children { - if c.Intersects(o) { + if exprObjTest(c, o) { return true } } @@ -86,50 +91,16 @@ func (e *areaExpression) rawIntersects(o geojson.Object) bool { return false } +func (e *areaExpression) rawIntersects(o geojson.Object) bool { + return e.testObject(o, geojson.Object.Intersects, (*areaExpression).Intersects) +} + func (e *areaExpression) rawContains(o geojson.Object) bool { - if e.obj != nil { - return e.obj.Contains(o) - } - switch e.op { - case AND: - for _, c:= range e.children { - if !c.Contains(o) { - return false - } - } - return true - case OR: - for _, c:= range e.children { - if c.Contains(o) { - return true - } - } - return false - } - return false + return e.testObject(o, geojson.Object.Contains, (*areaExpression).Contains) } func (e *areaExpression) rawWithin(o geojson.Object) bool { - if e.obj != nil { - return e.obj.Within(o) - } - switch e.op { - case AND: - for _, c:= range e.children { - if !c.Within(o) { - return false - } - } - return true - case OR: - for _, c:= range e.children { - if c.Within(o) { - return true - } - } - return false - } - return false + return e.testObject(o, geojson.Object.Within, (*areaExpression).Within) } func (e *areaExpression) Intersects(o geojson.Object) bool { @@ -145,26 +116,31 @@ func (e *areaExpression) Within(o geojson.Object) bool { } // Methods for testing an areaExpression against another areaExpression -func (e *areaExpression) rawIntersectsExpr(oe *areaExpression) bool { +func (e *areaExpression) testExpression( + oe *areaExpression, + exprObjTest func(ae *areaExpression, ob geojson.Object) bool, + rawExprExprTest func(ae1, ae2 *areaExpression) bool, + exprExprTest func(ae1, ae2 *areaExpression) bool, +) bool { if oe.negate { - e2 := &areaExpression{negate:!e.negate, obj:e.obj, op: e.op, children:e.children} - oe2 := &areaExpression{negate:false, obj:oe.obj, op:oe.op, children:oe.children} - return e2.IntersectsExpr(oe2) + e2 := &areaExpression{negate: !e.negate, obj: e.obj, op: e.op, children: e.children} + oe2 := &areaExpression{negate: false, obj: oe.obj, op: oe.op, children: oe.children} + return exprExprTest(e2, oe2) } if oe.obj != nil { - return e.rawIntersects(oe.obj) + return exprObjTest(e, oe.obj) } switch oe.op { case AND: for _, c := range oe.children { - if !e.rawIntersectsExpr(c) { + if !rawExprExprTest(e, c) { return false } } return true case OR: for _, c := range oe.children { - if e.rawIntersectsExpr(c) { + if rawExprExprTest(e, c) { return true } } @@ -173,60 +149,28 @@ func (e *areaExpression) rawIntersectsExpr(oe *areaExpression) bool { return false } +func (e *areaExpression) rawIntersectsExpr(oe *areaExpression) bool { + return e.testExpression( + oe, + (*areaExpression).rawIntersects, + (*areaExpression).rawIntersectsExpr, + (*areaExpression).IntersectsExpr) +} + func (e *areaExpression) rawWithinExpr(oe *areaExpression) bool { - if oe.negate { - e2 := &areaExpression{negate:!e.negate, obj:e.obj, op: e.op, children:e.children} - oe2 := &areaExpression{negate:false, obj:oe.obj, op:oe.op, children:oe.children} - return e2.WithinExpr(oe2) - } - if oe.obj != nil { - return e.rawWithin(oe.obj) - } - switch oe.op { - case AND: - for _, c:= range oe.children { - if !e.rawWithinExpr(c) { - return false - } - } - return true - case OR: - for _, c:= range oe.children { - if e.rawWithinExpr(c) { - return true - } - } - return false - } - return false + return e.testExpression( + oe, + (*areaExpression).rawWithin, + (*areaExpression).rawWithinExpr, + (*areaExpression).WithinExpr) } func (e *areaExpression) rawContainsExpr(oe *areaExpression) bool { - if oe.negate { - e2 := &areaExpression{negate:!e.negate, obj:e.obj, op: e.op, children:e.children} - oe2 := &areaExpression{negate:false, obj:oe.obj, op:oe.op, children:oe.children} - return e2.ContainsExpr(oe2) - } - if oe.obj != nil { - return e.rawContains(oe.obj) - } - switch oe.op { - case AND: - for _, c:= range oe.children { - if !e.rawContainsExpr(c) { - return false - } - } - return true - case OR: - for _, c:= range oe.children { - if e.rawContainsExpr(c) { - return true - } - } - return false - } - return false + return e.testExpression( + oe, + (*areaExpression).rawContains, + (*areaExpression).rawContainsExpr, + (*areaExpression).ContainsExpr) } func (e *areaExpression) IntersectsExpr(oe *areaExpression) bool { From 0c3a5d02cab9c36fc1be9217adf32729334a6fa2 Mon Sep 17 00:00:00 2001 From: Alex Roitman Date: Thu, 13 Jun 2019 12:04:04 -0700 Subject: [PATCH 5/9] Fixes --- internal/server/token.go | 14 ++------------ tests/testcmd_test.go | 9 +++++++++ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/internal/server/token.go b/internal/server/token.go index 5b8c26a5..d1397fe6 100644 --- a/internal/server/token.go +++ b/internal/server/token.go @@ -724,10 +724,10 @@ loop: newExpr := &areaExpression{negate: negate, op: NOOP} negate = false if ae != nil { - ps.push(ae) ae.children = append(ae.children, newExpr) } ae = newExpr + ps.push(ae) vsout = nvs case tokenRParen: if negate { @@ -788,14 +788,7 @@ loop: err = errInvalidNumberOfArguments return } else { - parent, empty := ps.pop() - if empty { - parent = ae - } - parent.children = append( - parent.children, - &areaExpression{op: OR}) - ps.push(parent) + ae = &areaExpression{op: OR, children: []*areaExpression{ae}} } case NOOP: ae.op = OR @@ -826,8 +819,5 @@ loop: break loop } } - if prevExpr, empty := ps.pop(); !empty { - ae = prevExpr - } return } diff --git a/tests/testcmd_test.go b/tests/testcmd_test.go index 0a5ba0a1..9cb833b0 100644 --- a/tests/testcmd_test.go +++ b/tests/testcmd_test.go @@ -149,6 +149,15 @@ func testcmd_expression_test(mc *mockServer) error { {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "AND", "(", "OBJECT", poly, "AND", "NOT", "GET", "mykey", "line3", ")"}, {"1"}, {"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "GET", "mykey", "line3"}, {"1"}, + {"TEST", "NOT", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3", + "OR", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly, + "OR", "GET", "mykey", "line3"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "line3", "OR", + "(", "OBJECT", poly8, "AND", "OBJECT", poly, ")"}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", + "(", "GET", "mykey", "line3", "OR", "OBJECT", poly8, ")", "AND", "OBJECT", poly}, {"1"}, {"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"}, {"TEST", "OBJECT", poly9, "WITHIN", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, From 2d83e189345717a345d798647065505111d0034b Mon Sep 17 00:00:00 2001 From: Alex Roitman Date: Thu, 13 Jun 2019 13:10:47 -0700 Subject: [PATCH 6/9] Add expression errors test. Make parser stricter. --- internal/server/token.go | 26 +++++++++++++++++--------- tests/testcmd_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/internal/server/token.go b/internal/server/token.go index d1397fe6..e7b94d2f 100644 --- a/internal/server/token.go +++ b/internal/server/token.go @@ -712,7 +712,7 @@ func (ps *parentStack) pop() (e *areaExpression, empty bool) { func (s *Server) parseAreaExpression(vsin []string, doClip bool) (vsout []string, ae *areaExpression, err error) { ps := &parentStack{} vsout = vsin[:] - var negate bool + var negate, needObj bool loop: for { nvs, wtok, ok := tokenval(vsout) @@ -723,6 +723,7 @@ loop: case tokenLParen: newExpr := &areaExpression{negate: negate, op: NOOP} negate = false + needObj = false if ae != nil { ae.children = append(ae.children, newExpr) } @@ -730,8 +731,8 @@ loop: ps.push(ae) vsout = nvs case tokenRParen: - if negate { - err = errInvalidArgument(tokenNOT) + if needObj { + err = errInvalidArgument(tokenRParen) return } if parent, empty := ps.pop(); empty { @@ -743,12 +744,14 @@ loop: vsout = nvs case tokenNOT: negate = true + needObj = true vsout = nvs case tokenAND: - if negate { - err = errInvalidArgument(tokenNOT) + if needObj { + err = errInvalidArgument(tokenAND) return } + needObj = true if ae == nil { err = errInvalidArgument(tokenAND) return @@ -774,10 +777,11 @@ loop: } vsout = nvs case tokenOR: - if negate { - err = errInvalidArgument(tokenNOT) + if needObj { + err = errInvalidArgument(tokenOR) return } + needObj = true if ae == nil { err = errInvalidArgument(tokenOR) return @@ -804,6 +808,7 @@ loop: } else { newExpr := &areaExpression{negate: negate, obj: parsedObj, op: NOOP} negate = false + needObj = false if ae == nil { ae = newExpr } else { @@ -812,12 +817,15 @@ loop: vsout = parsedVs } default: - if negate { - err = errInvalidArgument(tokenNOT) + if needObj { + err = errInvalidArgument(wtok) return } break loop } } + if !ps.isEmpty() || needObj || ae == nil || (ae.obj == nil && len(ae.children) == 0) { + err = errInvalidNumberOfArguments + } return } diff --git a/tests/testcmd_test.go b/tests/testcmd_test.go index 9cb833b0..82d6a4c6 100644 --- a/tests/testcmd_test.go +++ b/tests/testcmd_test.go @@ -8,6 +8,7 @@ func subTestTestCmd(t *testing.T, mc *mockServer) { runStep(t, mc, "WITHIN", testcmd_WITHIN_test) runStep(t, mc, "INTERSECTS", testcmd_INTERSECTS_test) runStep(t, mc, "INTERSECTS_CLIP", testcmd_INTERSECTS_CLIP_test) + runStep(t, mc, "ExpressionErrors", testcmd_expressionErrors_test) runStep(t, mc, "Expressions", testcmd_expression_test) } @@ -117,6 +118,40 @@ func testcmd_INTERSECTS_CLIP_test(mc *mockServer) error { }) } +func testcmd_expressionErrors_test(mc *mockServer) error { + return mc.DoBatch([][]interface{}{ + {"SET", "mykey", "foo", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, + {"SET", "mykey", "bar", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, + {"SET", "mykey", "baz", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, + + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "(", "GET", "mykey", "bar"}, { + "ERR wrong number of arguments for 'test' command"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", ")"}, { + "ERR invalid argument ')'"}, + + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "OR", "GET", "mykey", "bar"}, { + "ERR invalid argument 'or'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "AND", "GET", "mykey", "bar"}, { + "ERR invalid argument 'and'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "AND", "GET", "mykey", "baz"}, { + "ERR invalid argument 'and'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "OR", "GET", "mykey", "baz"}, { + "ERR invalid argument 'or'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR", "OR", "GET", "mykey", "baz"}, { + "ERR invalid argument 'or'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND", "AND", "GET", "mykey", "baz"}, { + "ERR invalid argument 'and'"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "OR"}, { + "ERR wrong number of arguments for 'test' command"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "AND"}, { + "ERR wrong number of arguments for 'test' command"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT"}, { + "ERR wrong number of arguments for 'test' command"}, + {"TEST", "GET", "mykey", "foo", "INTERSECTS", "GET", "mykey", "bar", "NOT", "AND", "GET", "mykey", "baz"}, { + "ERR invalid argument 'and'"}, + }) +} + func testcmd_expression_test(mc *mockServer) error { poly := `{ "type": "Polygon", From eb214cb889c260213c72402bdbce649d68e1f5ce Mon Sep 17 00:00:00 2001 From: Alex Roitman Date: Thu, 13 Jun 2019 13:33:07 -0700 Subject: [PATCH 7/9] Better naming and comments. --- internal/server/expression.go | 50 +++++++++++++++++------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/internal/server/expression.go b/internal/server/expression.go index 4bc51d7f..624a236f 100644 --- a/internal/server/expression.go +++ b/internal/server/expression.go @@ -19,7 +19,7 @@ const ( tokenRParen = ")" ) -// areaExpression is either an object or operator+children +// areaExpression is (maybe negated) either an spatial object or operator + children (other expressions). type areaExpression struct { negate bool obj geojson.Object @@ -63,7 +63,7 @@ func (e *areaExpression) maybeNegate(val bool) bool { return val } -// Methods for testing an areaExpression against the spatial object +// Methods for testing an areaExpression against the spatial object. func (e *areaExpression) testObject( o geojson.Object, objObjTest func(o1, o2 geojson.Object) bool, @@ -115,31 +115,31 @@ func (e *areaExpression) Within(o geojson.Object) bool { return e.maybeNegate(e.rawWithin(o)) } -// Methods for testing an areaExpression against another areaExpression +// Methods for testing an areaExpression against another areaExpression. func (e *areaExpression) testExpression( - oe *areaExpression, + other *areaExpression, exprObjTest func(ae *areaExpression, ob geojson.Object) bool, rawExprExprTest func(ae1, ae2 *areaExpression) bool, exprExprTest func(ae1, ae2 *areaExpression) bool, ) bool { - if oe.negate { - e2 := &areaExpression{negate: !e.negate, obj: e.obj, op: e.op, children: e.children} - oe2 := &areaExpression{negate: false, obj: oe.obj, op: oe.op, children: oe.children} - return exprExprTest(e2, oe2) + if other.negate { + oppositeExp := &areaExpression{negate: !e.negate, obj: e.obj, op: e.op, children: e.children} + nonNegateOther := &areaExpression{obj: other.obj, op: other.op, children: other.children} + return exprExprTest(oppositeExp, nonNegateOther) } - if oe.obj != nil { - return exprObjTest(e, oe.obj) + if other.obj != nil { + return exprObjTest(e, other.obj) } - switch oe.op { + switch other.op { case AND: - for _, c := range oe.children { + for _, c := range other.children { if !rawExprExprTest(e, c) { return false } } return true case OR: - for _, c := range oe.children { + for _, c := range other.children { if rawExprExprTest(e, c) { return true } @@ -149,38 +149,38 @@ func (e *areaExpression) testExpression( return false } -func (e *areaExpression) rawIntersectsExpr(oe *areaExpression) bool { +func (e *areaExpression) rawIntersectsExpr(other *areaExpression) bool { return e.testExpression( - oe, + other, (*areaExpression).rawIntersects, (*areaExpression).rawIntersectsExpr, (*areaExpression).IntersectsExpr) } -func (e *areaExpression) rawWithinExpr(oe *areaExpression) bool { +func (e *areaExpression) rawWithinExpr(other *areaExpression) bool { return e.testExpression( - oe, + other, (*areaExpression).rawWithin, (*areaExpression).rawWithinExpr, (*areaExpression).WithinExpr) } -func (e *areaExpression) rawContainsExpr(oe *areaExpression) bool { +func (e *areaExpression) rawContainsExpr(other *areaExpression) bool { return e.testExpression( - oe, + other, (*areaExpression).rawContains, (*areaExpression).rawContainsExpr, (*areaExpression).ContainsExpr) } -func (e *areaExpression) IntersectsExpr(oe *areaExpression) bool { - return e.maybeNegate(e.rawIntersectsExpr(oe)) +func (e *areaExpression) IntersectsExpr(other *areaExpression) bool { + return e.maybeNegate(e.rawIntersectsExpr(other)) } -func (e *areaExpression) WithinExpr(oe *areaExpression) bool { - return e.maybeNegate(e.rawWithinExpr(oe)) +func (e *areaExpression) WithinExpr(other *areaExpression) bool { + return e.maybeNegate(e.rawWithinExpr(other)) } -func (e *areaExpression) ContainsExpr(oe *areaExpression) bool { - return e.maybeNegate(e.rawContainsExpr(oe)) +func (e *areaExpression) ContainsExpr(other *areaExpression) bool { + return e.maybeNegate(e.rawContainsExpr(other)) } From 7c541949b12a65a255e90ff4dbb8479a7ee3c5a0 Mon Sep 17 00:00:00 2001 From: Alex Roitman Date: Thu, 13 Jun 2019 14:12:42 -0700 Subject: [PATCH 8/9] Unnecessary code. --- internal/server/token.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/server/token.go b/internal/server/token.go index e7b94d2f..a02a884c 100644 --- a/internal/server/token.go +++ b/internal/server/token.go @@ -817,10 +817,6 @@ loop: vsout = parsedVs } default: - if needObj { - err = errInvalidArgument(wtok) - return - } break loop } } From dd09ffbe1310ad175bcf4a5eba7b79c9081880c5 Mon Sep 17 00:00:00 2001 From: Alex Roitman Date: Fri, 14 Jun 2019 10:02:26 -0700 Subject: [PATCH 9/9] Fix parser for multiple negations. --- internal/server/token.go | 2 +- tests/testcmd_test.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/server/token.go b/internal/server/token.go index a02a884c..8d1afa1d 100644 --- a/internal/server/token.go +++ b/internal/server/token.go @@ -743,7 +743,7 @@ loop: } vsout = nvs case tokenNOT: - negate = true + negate = !negate needObj = true vsout = nvs case tokenAND: diff --git a/tests/testcmd_test.go b/tests/testcmd_test.go index 82d6a4c6..51c55da2 100644 --- a/tests/testcmd_test.go +++ b/tests/testcmd_test.go @@ -172,6 +172,10 @@ func testcmd_expression_test(mc *mockServer) error { {"SET", "mykey", "line3", "OBJECT", `{"type":"LineString","coordinates":[[-122.4408378,37.7341129],[-122.4408378,37.733]]}`}, {"OK"}, {"SET", "mykey", "poly8", "OBJECT", poly8}, {"OK"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "OBJECT", poly}, {"0"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "OBJECT", poly}, {"1"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "NOT", "NOT", "NOT", "OBJECT", poly}, {"0"}, + {"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "OR", "OBJECT", poly}, {"1"}, {"TEST", "OBJECT", poly9, "INTERSECTS", "OBJECT", poly8, "AND", "OBJECT", poly}, {"1"}, {"TEST", "OBJECT", poly9, "INTERSECTS", "GET", "mykey", "poly8", "OR", "OBJECT", poly}, {"1"},