Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion build.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,48 @@ func canBeNumber(q query) bool {
return true
}

// positionalCondition reports whether a condition AST node produces a numeric
// or positional result.
func positionalCondition(n node) bool {
switch v := n.(type) {
case *operandNode:
_, ok := v.Val.(float64)
return ok
case *functionNode:
switch v.FuncName {
case "position", "last", "count", "sum", "string-length", "number",
"floor", "ceiling", "round":
return true
}
for _, arg := range v.Args {
if positionalCondition(arg) {
return true
}
}
return false
case *operatorNode:
switch v.Op {
case "+", "-", "*", "div", "mod":
return true
}
return positionalCondition(v.Left) || positionalCondition(v.Right)
default:
return false
}
}

// processFilterNode builds query for the XPath filter predicate.
func (b *builder) processFilter(root *filterNode, flags flag, props *builderProp) (query, error) {
first := (flags & flagsEnum.Filter) == 0

qyInput, err := b.processNode(root.Input, (flags | flagsEnum.Filter), props)
// Inspect the condition AST to decide whether to set flagsEnum.Filter.
// Positional predicates cannot be compiled into a descendantQuery.
inputFlags := flags
if positionalCondition(root.Condition) {
inputFlags |= flagsEnum.Filter
}

qyInput, err := b.processNode(root.Input, inputFlags, props)
if err != nil {
return nil, err
}
Expand Down
78 changes: 78 additions & 0 deletions xpath_bench_filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package xpath

import (
"fmt"
"testing"
)

func buildFilterBenchDoc(numSections, itemsPerSection int) *TNode {
root := createNode("root", ElementNode)
for s := 0; s < numSections; s++ {
section := addChild(root, createNode("section", ElementNode))
addChild(section, createNode(fmt.Sprintf("s%d", s), AttributeNode))
for i := 0; i < itemsPerSection; i++ {
item := addChild(section, createNode("item", ElementNode))
cls := "a"
if i%2 == 1 {
cls = "b"
}
addChild(item, createNode(cls, AttributeNode))
addChild(item, createNode(fmt.Sprintf("i%d", s*itemsPerSection+i), AttributeNode))
}
}
return root
}

func addChild(parent, child *TNode) *TNode {
child.Parent = parent
if parent.FirstChild == nil {
parent.FirstChild = child
} else {
last := parent.FirstChild
for last.NextSibling != nil {
last = last.NextSibling
}
last.NextSibling = child
child.PrevSibling = last
}
return child
}

func BenchmarkFilterNonPositional(b *testing.B) {
doc := buildFilterBenchDoc(10, 20) // 200 items across 10 sections, ~430 nodes total
expr := MustCompile(`//item[@class]`)
nav := createNavigator(doc)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
iter := expr.Select(nav)
for iter.MoveNext() {
}
}
}

func BenchmarkFilterNonPositionalAttrValue(b *testing.B) {
doc := buildFilterBenchDoc(10, 20)
expr := MustCompile(`//item[@class="a"]`)
nav := createNavigator(doc)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
iter := expr.Select(nav)
for iter.MoveNext() {
}
}
}

func BenchmarkFilterPositional(b *testing.B) {
doc := buildFilterBenchDoc(10, 20)
expr := MustCompile(`//item[1]`)
nav := createNavigator(doc)
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
iter := expr.Select(nav)
for iter.MoveNext() {
}
}
}
40 changes: 40 additions & 0 deletions xpath_predicate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,46 @@ import (
"testing"
)

func TestPositionalCondition(t *testing.T) {
tests := []struct {
name string
node node
want bool
}{
{"numeric literal", newOperandNode(float64(1)), true},
{"string literal", newOperandNode("text"), false},
{"position()", newFunctionNode("position", "", nil), true},
{"last()", newFunctionNode("last", "", nil), true},
{"count()", newFunctionNode("count", "", nil), true},
{"sum()", newFunctionNode("sum", "", nil), true},
{"string-length()", newFunctionNode("string-length", "", nil), true},
{"number()", newFunctionNode("number", "", nil), true},
{"floor()", newFunctionNode("floor", "", nil), true},
{"ceiling()", newFunctionNode("ceiling", "", nil), true},
{"round()", newFunctionNode("round", "", nil), true},
{"name()", newFunctionNode("name", "", nil), false},
{"normalize-space()", newFunctionNode("normalize-space", "", nil), false},
{"string(count())", newFunctionNode("string", "", []node{newFunctionNode("count", "", nil)}), true},
{"+", newOperatorNode("+", newOperandNode("a"), newOperandNode("b")), true},
{"-", newOperatorNode("-", newOperandNode("a"), newOperandNode("b")), true},
{"*", newOperatorNode("*", newOperandNode("a"), newOperandNode("b")), true},
{"div", newOperatorNode("div", newOperandNode("a"), newOperandNode("b")), true},
{"mod", newOperatorNode("mod", newOperandNode("a"), newOperandNode("b")), true},
{"@href", newAxisNode("attribute", AttributeNode, "href", "", "", nil), false},
{"= non-positional", newOperatorNode("=", newOperandNode("a"), newOperandNode("b")), false},
{"!= non-positional", newOperatorNode("!=", newOperandNode("a"), newOperandNode("b")), false},
{"position()=1", newOperatorNode("=", newFunctionNode("position", "", nil), newOperandNode(float64(1))), true},
{"last()-1", newOperatorNode("-", newFunctionNode("last", "", nil), newOperandNode(float64(1))), true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := positionalCondition(tt.node); got != tt.want {
t.Errorf("positionalCondition(%s) = %v, want %v", tt.name, got, tt.want)
}
})
}
}

func TestLogicals(t *testing.T) {
test_xpath_elements(t, book_example, `//book[1 + 1]`, 9)
test_xpath_elements(t, book_example, `//book[1 * 2]`, 9)
Expand Down