sparkql 1.0.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- Mjk5YmNlMjA3ZDg1N2VjMzIzZDczMWMzNzdiZGU2YTMwY2E0ZTZlYQ==
4
+ Y2ExNzBkYzg0N2RiZmFkOWRlMGI2NTg0NzJiOGQ5NzNlNTA2OTA5Mw==
5
5
  data.tar.gz: !binary |-
6
- MzJlZGNmM2Q5YTdiODA3M2UwNDNhNWNlZjdlZDU5MjMwY2RhNGU3OQ==
6
+ YmYyYTBkZGQxMTFkNGZhNDg2MTA1NmZjN2M4YjcyZTZiMWQ0Njk5Ng==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- YjdhZDliN2QxZDY3NzkyNzc0Zjk4NTRlZGYwZmUzODRlNDUzMDBkNjY1ZDVh
10
- MjczZjI2ODc1Zjk0MzBlYWRlY2FhMTY3ZDAzYjgwOGIzYzY1NjM0MjQ0ZDQ5
11
- ODRlMmI1YTUyNTRjMmIyZmIwOTE2NjA2MTVhNTIxMjBhZTA4OGQ=
9
+ Mzk4MmY3OTE2MzNiYmEyOTg3NGMzOTdhZTRjY2NhZjhmZjgxMzE1NTJkM2Vj
10
+ OWIxZGU0YTBjNTI3M2ZhZjUxNjdlOTY4MTgzMDRmNjhiOWE3MjEwNTU4YWU0
11
+ NmQ4MDYwZDRkMDIxYzkyNWI1MjQxMThhYTFiNGFmOTZmNzA1MjI=
12
12
  data.tar.gz: !binary |-
13
- NDc1NjI2NjQ0OTg4ZTEzNzU1NmNkMjZiOGY4Mzk0MWM1OThlNDUyMGYxOWM4
14
- MzQ4MmVhZWE5NDVmZmUzNmRkZmNmYmFkNmNlNzYwN2Y0MGMyYjQyZTk4ZGMw
15
- NjRlMzg2MmEyMDc0MGU0MjJiNjM3NzQwNzA3NmVhMTNlZTAyZTU=
13
+ ZGEyMzQ1YzU2NDU5YjNhNmVkOTEwNjBlYTI2MWIxMmRiY2E3NWU1MjU3Nzg1
14
+ OTgxMzc3MTc3ZGI5Njc1NWZmNGJkY2FkMWNhZDdkNmY4NThkOWUyZmUzMWRj
15
+ NDViYmQzNzEzNjBkOThiMmViYjQ4YTRhNjBhYmVlYzE1NWQwMDg=
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ v1.1.0, 2016-07-28 ([changes](https://github.com/sparkapi/sparkql/compare/v1.0.3...v1.1.0))
2
+ -------------------
3
+ * [IMPROVEMENT] Evaluation class for sparkql boolean algebra processing
4
+
1
5
  v1.0.3, 2016-06-06 ([changes](https://github.com/sparkapi/sparkql/compare/v1.0.2...v1.0.3))
2
6
  -------------------
3
7
  * [IMPROVEMENT] Expression limit lifted to 75 expressions
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.3
1
+ 1.1.0
@@ -0,0 +1,152 @@
1
+ # Using an instance of ExpressionResolver to resolve the individual expressions,
2
+ # this class will evaluate the rest of a parsed sparkql string to true or false.
3
+ # Namely, this class will handle all the nesting, boolean algebra, and dropped
4
+ # fields. Plus, it has some optimizations built in to skip the processing for
5
+ # any expressions that don't contribute to the net result of the filter.
6
+ class Sparkql::Evaluator
7
+
8
+ attr_reader :processed_count
9
+
10
+ def initialize expression_resolver
11
+ @resolver = expression_resolver
12
+ end
13
+
14
+ def evaluate(expressions)
15
+ @processed_count = 0
16
+ @index = {
17
+ level: 0,
18
+ block_group: 0,
19
+ conjunction: "And",
20
+ conjunction_level: 0,
21
+ match: true,
22
+ good_ors: false,
23
+ expressions: 0
24
+ }
25
+ @groups = [@index]
26
+ expressions.each do |expression|
27
+ handle_group(expression)
28
+ adjust_expression_for_dropped_field(expression)
29
+ check_for_good_ors(expression)
30
+ next if skip?(expression)
31
+ evaluate_expression(expression)
32
+ end
33
+ cleanup
34
+ return @index[:match]
35
+ end
36
+
37
+ private
38
+
39
+ # prepare the group stack for the next expression
40
+ def handle_group(expression)
41
+ if @index[:block_group] == expression[:block_group]
42
+ # Noop
43
+ elsif @index[:block_group] < expression[:block_group]
44
+ @index = new_group(expression)
45
+ @groups.push(@index)
46
+ else
47
+ # Turn the group into an expression, resolve down to previous group(s)
48
+ smoosh_group(expression)
49
+ end
50
+ end
51
+
52
+ # Here's the real meat. We use an internal stack to represent the result of
53
+ # each block_group. This logic is re-used when merging the final result of one
54
+ # block group with the previous.
55
+ def evaluate_expression(expression)
56
+ @processed_count += 1
57
+ evaluate_node(expression, @resolver.resolve(expression))
58
+ end
59
+ def evaluate_node(node, result)
60
+ if result == :drop
61
+ @dropped_expression = node
62
+ return result
63
+ end
64
+ if node[:unary] == "Not"
65
+ result = !result
66
+ end
67
+ if node[:conjunction] == 'Not' &&
68
+ (node[:conjunction_level] == node[:level] ||
69
+ node[:conjunction_level] == @index[:level])
70
+ @index[:match] = !result
71
+ elsif node[:conjunction] == 'And' || @index[:expressions] == 0
72
+ @index[:match] = result if @index[:match]
73
+ elsif node[:conjunction] == 'Or' && result
74
+ @index[:match] = result
75
+ end
76
+ @index[:expressions] += 1
77
+ result
78
+ end
79
+
80
+ # Optimization logic, once we find any set of And'd expressions that pass and
81
+ # run into an Or at the same level, we can skip further processing at that
82
+ # level.
83
+ def check_for_good_ors(expression)
84
+ if expression[:conjunction] == 'Or'
85
+ good_index = @index
86
+ unless expression[:conjunction_level] == @index[:level]
87
+ good_index = nil
88
+ # Well crap, now we need to go back and find that level by hand
89
+ @groups.reverse_each do |i|
90
+ if i[:level] == expression[:conjunction_level]
91
+ good_index = i
92
+ end
93
+ end
94
+ end
95
+ if !good_index.nil? && good_index[:expressions] > 0 && good_index[:match]
96
+ good_index[:good_ors] = true
97
+ end
98
+ end
99
+ end
100
+
101
+ # We can skip further expression processing when And-d with a false expression
102
+ # or a "good Or" was already encountered.
103
+ def skip?(expression)
104
+ @index[:good_ors] ||
105
+ !@index[:match] && expression[:conjunction] == 'And'
106
+ end
107
+
108
+ def new_group(expression)
109
+ {
110
+ level: expression[:level],
111
+ block_group: expression[:block_group],
112
+ conjunction: expression[:conjunction],
113
+ conjunction_level: expression[:conjunction_level],
114
+ match: true,
115
+ good_ors: false,
116
+ expressions: 0
117
+ }
118
+ end
119
+
120
+ # When the last expression was dropped, we need to repair the filter by
121
+ # stealing the conjunction of that dropped field.
122
+ def adjust_expression_for_dropped_field(expression)
123
+ if @dropped_expression.nil?
124
+ return
125
+ elsif @dropped_expression[:block_group] == expression[:block_group]
126
+ expression[:conjunction] = @dropped_expression[:conjunction]
127
+ expression[:conjunction_level] = @dropped_expression[:conjunction_level]
128
+ end
129
+ @dropped_expression = nil
130
+ end
131
+
132
+ # This is similar to the cleanup step, but happens when we return from a
133
+ # nesting level. Before we can proceed, we need wrap up the result of the
134
+ # nested group.
135
+ def smoosh_group(expression)
136
+ until @groups.last[:block_group] == expression[:block_group]
137
+ last = @groups.pop
138
+ @index = @groups.last
139
+ evaluate_node(last, last[:match])
140
+ end
141
+ end
142
+
143
+ # pop off the group stack, evaluating each group with the previous as we go.
144
+ def cleanup
145
+ while @groups.size > 1
146
+ last = @groups.pop
147
+ @index = @groups.last
148
+ evaluate_node(last, last[:match])
149
+ end
150
+ @groups.last[:match]
151
+ end
152
+ end
@@ -0,0 +1,11 @@
1
+ # Base class for handling expression resolution
2
+ class Sparkql::ExpressionResolver
3
+
4
+ VALID_RESULTS = [true, false, :drop]
5
+
6
+ # Evaluate the result of this expression. Allows for any of the values in
7
+ # VALID_RESULTS
8
+ def resolve(expression)
9
+ true
10
+ end
11
+ end
@@ -60,16 +60,17 @@ module Sparkql::ParserTools
60
60
  end
61
61
 
62
62
  def tokenize_unary_conjunction(conj, exp)
63
-
64
63
  # Handles the case when a SparkQL filter string
65
64
  # begins with a unary operator, and is nested, such as:
66
- # Not (Not Field Eq 1)
65
+ # Not (Not Field Eq 1)
66
+ # In this instance we treat the outer unary as a conjunction.
67
67
  if @expression_count == 1 && @lexer.level > 0
68
- exp.first[:conjunction] = conj
68
+ exp.first[:conjunction] = conj
69
+ exp.first[:conjunction_level] = @lexer.level - 1
69
70
  end
70
-
71
71
  exp.first[:unary] = conj
72
72
  exp.first[:unary_level] = @lexer.level
73
+
73
74
  exp
74
75
  end
75
76
 
data/lib/sparkql.rb CHANGED
@@ -2,6 +2,8 @@ require "sparkql/version"
2
2
  require "sparkql/token"
3
3
  require "sparkql/errors"
4
4
  require "sparkql/expression_state"
5
+ require "sparkql/expression_resolver"
6
+ require "sparkql/evaluator"
5
7
  require "sparkql/lexer"
6
8
  require "sparkql/function_resolver"
7
9
  require "sparkql/parser_tools"
@@ -0,0 +1,12 @@
1
+ # A super simple expression resolver for testing... returns the boolean value as
2
+ # the result for the expression, or when not a boolean, drops the expression.
3
+ class BooleanOrBustExpressionResolver < Sparkql::ExpressionResolver
4
+
5
+ def resolve(expression)
6
+ if expression[:type] == :boolean
7
+ "true" == expression[:value]
8
+ else
9
+ :drop
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,84 @@
1
+ require 'test_helper'
2
+ require 'support/boolean_or_bust_expression_resolver'
3
+
4
+ class EvaluatorTest < Test::Unit::TestCase
5
+ include Sparkql
6
+
7
+ def test_simple
8
+ assert sample('Test Eq true')
9
+ assert !sample('Test Eq false')
10
+ assert sample("Test Eq 'Drop'")
11
+ end
12
+
13
+ def test_conjunction
14
+ assert sample('Test Eq true And Test Eq true')
15
+ assert !sample('Test Eq false And Test Eq true')
16
+ assert !sample('Test Eq false And Test Eq false')
17
+ # Ors
18
+ assert sample("Test Eq true Or Test Eq true")
19
+ assert sample("Test Eq true Or Test Eq false")
20
+ assert sample("Test Eq false Or Test Eq true")
21
+ assert !sample("Test Eq false Or Test Eq false")
22
+ end
23
+
24
+ def test_dropped_field_handling
25
+ assert sample("Test Eq 'Drop' And Test Eq true")
26
+ assert !sample("Test Eq 'Drop' And Test Eq false")
27
+ assert !sample("Test Eq 'Drop' Or Test Eq false")
28
+ assert sample("Test Eq 'Drop' Or Test Eq true")
29
+ assert sample("Test Eq false And Test Eq 'Drop' Or Test Eq true")
30
+ assert sample("Test Eq false Or (Test Eq 'Drop' And Test Eq true)")
31
+ end
32
+
33
+ def test_nesting
34
+ assert sample("Test Eq true Or (Test Eq true) And Test Eq false And (Test Eq true)")
35
+ assert sample("Test Eq true Or ((Test Eq false) And Test Eq false) And (Test Eq false)")
36
+ assert sample("(Test Eq false Or Test Eq true) Or (Test Eq false Or Test Eq false)")
37
+ assert sample("(Test Eq true And Test Eq true) Or (Test Eq false)")
38
+ assert sample("(Test Eq true And Test Eq true) Or (Test Eq false And Test Eq true)")
39
+ assert !sample("(Test Eq false And Test Eq true) Or (Test Eq false)")
40
+ assert sample("Test Eq true And ((Test Eq true And Test Eq false) Or Test Eq true)")
41
+ assert !sample("Test Eq true And ((Test Eq true And Test Eq false) Or Test Eq false) And Test Eq true")
42
+ assert !sample("Test Eq true And ((Test Eq true And Test Eq false) Or Test Eq false) Or Test Eq false")
43
+ assert sample("Test Eq true And ((Test Eq true And Test Eq false) Or Test Eq false) Or Test Eq true")
44
+ end
45
+
46
+ def test_nots
47
+ assert !sample("Not Test Eq true")
48
+ assert sample("Not Test Eq false")
49
+ assert !sample("Not (Test Eq true)")
50
+ assert sample("Not (Test Eq false)")
51
+ assert sample("Test Eq true Not Test Eq false")
52
+ assert !sample("Test Eq true Not Test Eq true")
53
+ assert sample("Test Eq true Not (Test Eq false Or Test Eq false)")
54
+ assert sample("Test Eq true Not (Test Eq false And Test Eq false)")
55
+ assert !sample("Test Eq true Not (Test Eq false Or Test Eq true)")
56
+ assert !sample("Test Eq true Not (Test Eq true Or Test Eq false)")
57
+ assert !sample("Test Eq true Not (Not Test Eq false)")
58
+ assert sample("Not (Not Test Eq true)")
59
+ assert sample("Not (Not(Not Test Eq true))")
60
+ end
61
+
62
+ def test_optimizations
63
+ assert sample("Test Eq true Or Test Eq false And Test Eq false")
64
+ assert_equal 1, @evaluator.processed_count
65
+ assert sample("Test Eq false Or Test Eq true And Test Eq true")
66
+ assert_equal 3, @evaluator.processed_count
67
+ assert sample("(Test Eq true Or Test Eq false) And Test Eq true")
68
+ assert_equal 2, @evaluator.processed_count
69
+ assert sample("(Test Eq false Or Test Eq true) And Test Eq true")
70
+ assert_equal 3, @evaluator.processed_count
71
+ end
72
+
73
+ # Here's some examples from prospector's tests that have been simplified a bit.
74
+ def test_advanced
75
+ assert !sample("MlsStatus Eq false And PropertyType Eq true And (City Eq true Or City Eq false)")
76
+ end
77
+
78
+ def sample filter
79
+ @parser = Parser.new
80
+ @expressions = @parser.parse(filter)
81
+ @evaluator = Evaluator.new(BooleanOrBustExpressionResolver.new())
82
+ @evaluator.evaluate(@expressions)
83
+ end
84
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sparkql
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wade McEwen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-06 00:00:00.000000000 Z
11
+ date: 2016-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: georuby
@@ -111,6 +111,8 @@ files:
111
111
  - VERSION
112
112
  - lib/sparkql.rb
113
113
  - lib/sparkql/errors.rb
114
+ - lib/sparkql/evaluator.rb
115
+ - lib/sparkql/expression_resolver.rb
114
116
  - lib/sparkql/expression_state.rb
115
117
  - lib/sparkql/function_resolver.rb
116
118
  - lib/sparkql/geo.rb
@@ -127,8 +129,10 @@ files:
127
129
  - script/markdownify.rb
128
130
  - script/release
129
131
  - sparkql.gemspec
132
+ - test/support/boolean_or_bust_expression_resolver.rb
130
133
  - test/test_helper.rb
131
134
  - test/unit/errors_test.rb
135
+ - test/unit/evaluator_test.rb
132
136
  - test/unit/expression_state_test.rb
133
137
  - test/unit/function_resolver_test.rb
134
138
  - test/unit/geo/record_circle_test.rb
@@ -160,8 +164,10 @@ signing_key:
160
164
  specification_version: 4
161
165
  summary: API Parser engine for filter searching
162
166
  test_files:
167
+ - test/support/boolean_or_bust_expression_resolver.rb
163
168
  - test/test_helper.rb
164
169
  - test/unit/errors_test.rb
170
+ - test/unit/evaluator_test.rb
165
171
  - test/unit/expression_state_test.rb
166
172
  - test/unit/function_resolver_test.rb
167
173
  - test/unit/geo/record_circle_test.rb