sparkql 0.1.8 → 0.3.2

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.
@@ -1,22 +1,22 @@
1
1
  # Required interface for existing parser implementations
2
2
  module Sparkql::ParserCompatibility
3
-
4
- MAXIMUM_MULTIPLE_VALUES = 25
3
+
4
+ MAXIMUM_MULTIPLE_VALUES = 200
5
5
  MAXIMUM_EXPRESSIONS = 50
6
6
  MAXIMUM_LEVEL_DEPTH = 2
7
-
7
+
8
8
  # TODO I Really don't think this is required anymore
9
9
  # Ordered by precedence.
10
10
  FILTER_VALUES = [
11
11
  {
12
12
  :type => :datetime,
13
13
  :regex => /^[0-9]{4}\-[0-9]{2}\-[0-9]{2}T[0-9]{2}\:[0-9]{2}\:[0-9]{2}\.[0-9]{6}$/,
14
- :operators => Sparkql::Token::OPERATORS
14
+ :operators => Sparkql::Token::OPERATORS + [Sparkql::Token::RANGE_OPERATOR]
15
15
  },
16
16
  {
17
17
  :type => :date,
18
18
  :regex => /^[0-9]{4}\-[0-9]{2}\-[0-9]{2}$/,
19
- :operators => Sparkql::Token::OPERATORS
19
+ :operators => Sparkql::Token::OPERATORS + [Sparkql::Token::RANGE_OPERATOR]
20
20
  },
21
21
  {
22
22
  :type => :character,
@@ -28,13 +28,18 @@ module Sparkql::ParserCompatibility
28
28
  :type => :integer,
29
29
  :regex => /^\-?[0-9]+$/,
30
30
  :multiple => /^\-?[0-9]+/,
31
- :operators => Sparkql::Token::OPERATORS
31
+ :operators => Sparkql::Token::OPERATORS + [Sparkql::Token::RANGE_OPERATOR]
32
32
  },
33
33
  {
34
34
  :type => :decimal,
35
35
  :regex => /^\-?[0-9]+\.[0-9]+$/,
36
36
  :multiple => /^\-?[0-9]+\.[0-9]+/,
37
- :operators => Sparkql::Token::OPERATORS
37
+ :operators => Sparkql::Token::OPERATORS + [Sparkql::Token::RANGE_OPERATOR]
38
+ },
39
+ {
40
+ :type => :shape,
41
+ # This type is not parseable, so no regex
42
+ :operators => Sparkql::Token::EQUALITY_OPERATORS
38
43
  },
39
44
  {
40
45
  :type => :boolean,
@@ -47,9 +52,9 @@ module Sparkql::ParserCompatibility
47
52
  :operators => Sparkql::Token::EQUALITY_OPERATORS
48
53
  }
49
54
  ]
50
-
55
+
51
56
  OPERATORS_SUPPORTING_MULTIPLES = ["Eq","Ne"]
52
-
57
+
53
58
  # To be implemented by child class.
54
59
  # Shall return a valid query string for the respective database,
55
60
  # or nil if the source could not be processed. It may be possible to return a valid
@@ -58,7 +63,7 @@ module Sparkql::ParserCompatibility
58
63
  def compile( source, mapper )
59
64
  raise NotImplementedError
60
65
  end
61
-
66
+
62
67
  # Returns a list of expressions tokenized in the following format:
63
68
  # [{ :field => IdentifierName, :operator => "Eq", :value => "'Fargo'", :type => :character, :conjunction => "And" }]
64
69
  # This step will set errors if source is not syntactically correct.
@@ -67,22 +72,22 @@ module Sparkql::ParserCompatibility
67
72
 
68
73
  # Reset the parser error stack
69
74
  @errors = []
70
-
75
+
71
76
  expressions = self.parse(source)
72
77
  expressions
73
78
  end
74
-
79
+
75
80
  # Returns an array of errors. This is an array of ParserError objects
76
81
  def errors
77
82
  @errors = [] unless defined?(@errors)
78
83
  @errors
79
84
  end
80
-
85
+
81
86
  # Delegator for methods to process the error list.
82
87
  def process_errors
83
88
  Sparkql::ErrorsProcessor.new(@errors)
84
89
  end
85
-
90
+
86
91
  # delegate :errors?, :fatal_errors?, :dropped_errors?, :recovered_errors?, :to => :process_errors
87
92
  # Since I don't have rails delegate...
88
93
  def errors?
@@ -97,7 +102,7 @@ module Sparkql::ParserCompatibility
97
102
  def recovered_errors?
98
103
  process_errors.recovered_errors?
99
104
  end
100
-
105
+
101
106
  def escape_value_list( expression )
102
107
  final_list = []
103
108
  expression[:value].each do | value |
@@ -109,7 +114,7 @@ module Sparkql::ParserCompatibility
109
114
  end
110
115
  expression[:value] = final_list
111
116
  end
112
-
117
+
113
118
  def escape_value( expression )
114
119
  if expression[:value].is_a? Array
115
120
  return escape_value_list( expression )
@@ -158,7 +163,7 @@ module Sparkql::ParserCompatibility
158
163
  def boolean_escape(string)
159
164
  "true" == string
160
165
  end
161
-
166
+
162
167
  # Returns the rule hash for a given type
163
168
  def rules_for_type( type )
164
169
  FILTER_VALUES.each do |rule|
@@ -166,49 +171,72 @@ module Sparkql::ParserCompatibility
166
171
  end
167
172
  nil
168
173
  end
169
-
174
+
170
175
  # true if a given type supports multiple values
171
176
  def supports_multiple?( type )
172
177
  rules_for_type(type).include?( :multiple )
173
178
  end
174
-
179
+
175
180
  # Maximum supported nesting level for the parser filters
176
181
  def max_level_depth
177
182
  MAXIMUM_LEVEL_DEPTH
178
183
  end
179
-
184
+
185
+ def max_expressions
186
+ MAXIMUM_EXPRESSIONS
187
+ end
188
+
189
+ def max_values
190
+ MAXIMUM_MULTIPLE_VALUES
191
+ end
192
+
180
193
  private
181
-
194
+
182
195
  def tokenizer_error( error_hash )
183
196
  self.errors << Sparkql::ParserError.new( error_hash )
184
197
  end
185
198
  alias :compile_error :tokenizer_error
186
-
199
+
187
200
  # Checks the type of an expression with what is expected.
188
201
  def check_type!(expression, expected, supports_nulls = true)
189
202
  if expected == expression[:type] || (supports_nulls && expression[:type] == :null)
190
203
  return true
191
- elsif expected == :datetime && expression[:type] == :date
204
+ elsif expected == :datetime && expression[:type] == :date
192
205
  expression[:type] = :datetime
193
206
  expression[:cast] = :date
194
207
  return true
208
+ elsif expected == :date && expression[:type] == :datetime
209
+ expression[:type] = :date
210
+ expression[:cast] = :datetime
211
+ if multiple_values?(expression[:value])
212
+ expression[:value].map!{ |val| coerce_datetime val }
213
+ else
214
+ expression[:value] = coerce_datetime expression[:value]
215
+ end
216
+ return true
217
+ elsif expected == :decimal && expression[:type] == :integer
218
+ expression[:type] = :decimal
219
+ expression[:cast] = :integer
220
+ return true
195
221
  end
196
222
  type_error(expression, expected)
197
223
  false
198
224
  end
199
-
225
+
200
226
  def type_error( expression, expected )
201
227
  compile_error(:token => expression[:field], :expression => expression,
202
228
  :message => "expected #{expected} but found #{expression[:type]}",
203
229
  :status => :fatal )
204
230
  end
205
-
231
+
206
232
  # Builds the correct operator based on the type and the value.
207
233
  # default should be the operator provided in the actual filter string
208
234
  def get_operator(expression, default )
209
235
  f = rules_for_type(expression[:type])
210
236
  if f[:operators].include?(default)
211
- if f[:multiple] && multiple_values?( expression[:value])
237
+ if f[:multiple] && range?(expression[:value]) && default == 'Bt'
238
+ return "Bt"
239
+ elsif f[:multiple] && multiple_values?(expression[:value])
212
240
  return nil unless operator_supports_multiples?(default)
213
241
  return default == "Ne" ? "Not In" : "In"
214
242
  elsif default == "Ne"
@@ -219,13 +247,25 @@ module Sparkql::ParserCompatibility
219
247
  return nil
220
248
  end
221
249
  end
222
-
250
+
223
251
  def multiple_values?(value)
224
252
  Array(value).size > 1
225
253
  end
226
-
254
+
255
+ def range?(value)
256
+ Array(value).size == 2
257
+ end
258
+
227
259
  def operator_supports_multiples?(operator)
228
260
  OPERATORS_SUPPORTING_MULTIPLES.include?(operator)
229
261
  end
230
262
 
263
+ def coerce_datetime datetime
264
+ if datestr = datetime.match(/^(\d{4}-\d{2}-\d{2})/)
265
+ datestr[0]
266
+ else
267
+ datetime
268
+ end
269
+ end
270
+
231
271
  end
@@ -1,12 +1,16 @@
1
1
  # This is the guts of the parser internals and is mixed into the parser for organization.
2
2
  module Sparkql::ParserTools
3
+
4
+ # Coercible types from highest precision to lowest
5
+ DATE_TYPES = [:datetime, :date]
6
+ NUMBER_TYPES = [:decimal, :integer]
3
7
 
4
8
  def parse(str)
5
9
  @lexer = Sparkql::Lexer.new(str)
6
10
  results = do_parse
7
- max = Sparkql::ParserCompatibility::MAXIMUM_EXPRESSIONS
8
11
  return if results.nil?
9
- results.size > max ? results[0,max] : results
12
+ validate_expressions results
13
+ results
10
14
  end
11
15
 
12
16
  def next_token
@@ -24,59 +28,89 @@ module Sparkql::ParserTools
24
28
  expression = {:field => field, :operator => operator, :conjunction => 'And',
25
29
  :level => @lexer.level, :block_group => block_group, :custom_field => custom_field}
26
30
  expression = val.merge(expression) unless val.nil?
27
- if @lexer.level > max_level_depth
28
- compile_error(:token => "(", :expression => expression,
29
- :message => "You have exceeded the maximum nesting level. Please nest no more than #{max_level_depth} levels deep.",
30
- :status => :fatal, :syntax => false )
31
- end
31
+ validate_level_depth expression
32
32
  if operator.nil?
33
33
  tokenizer_error(:token => op, :expression => expression,
34
34
  :message => "Operator not supported for this type and value string", :status => :fatal )
35
35
  end
36
36
  [expression]
37
37
  end
38
-
38
+
39
39
  def tokenize_conjunction(exp1, conj, exp2)
40
40
  exp2.first[:conjunction] = conj
41
+ exp2.first[:conjunction_level] = @lexer.level
41
42
  exp1 + exp2
42
43
  end
43
-
44
+
45
+ def tokenize_unary_conjunction(conj, exp)
46
+ exp.first[:unary] = conj
47
+ exp.first[:unary_level] = @lexer.level
48
+ exp
49
+ end
50
+
44
51
  def tokenize_group(expressions)
45
52
  @lexer.leveldown
46
53
  expressions
47
54
  end
48
55
 
56
+ def tokenize_list(list)
57
+ validate_multiple_values list[:value]
58
+ list[:condition] ||= list[:value]
59
+ list
60
+ end
61
+
49
62
  def tokenize_multiple(lit1, lit2)
63
+ final_type = lit1[:type]
50
64
  if lit1[:type] != lit2[:type]
51
- tokenizer_error(:token => @lexer.last_field,
52
- :message => "Type mismatch in field list.",
53
- :status => :fatal,
54
- :syntax => true)
65
+ final_type = coercible_types(lit1[:type],lit2[:type])
66
+ if final_type.nil?
67
+ final_type = lit1[:type]
68
+ tokenizer_error(:token => @lexer.last_field,
69
+ :message => "Type mismatch in field list.",
70
+ :status => :fatal,
71
+ :syntax => true)
72
+ end
55
73
  end
56
74
  array = Array(lit1[:value])
57
- unless array.size >= Sparkql::ParserCompatibility::MAXIMUM_MULTIPLE_VALUES
58
- array << lit2[:value]
59
- end
75
+ condition = lit1[:condition] || lit1[:value]
76
+ array << lit2[:value]
60
77
  {
61
- :type => lit1[:type],
78
+ :type => final_type ,
62
79
  :value => array,
63
- :multiple => "true" # TODO ?
80
+ :multiple => "true",
81
+ :condition => condition + "," + (lit2[:condition] || lit2[:value])
64
82
  }
65
83
  end
66
84
 
85
+ def tokenize_function_args(lit1, lit2)
86
+ array = lit1.kind_of?(Array) ? lit1 : [lit1]
87
+ array << lit2
88
+ array
89
+ end
90
+
67
91
  def tokenize_function(name, f_args)
92
+ @lexer.leveldown
93
+ @lexer.block_group_identifier -= 1
94
+
68
95
  args = f_args.instance_of?(Array) ? f_args : [f_args]
96
+ validate_multiple_arguments args
97
+ condition_list = []
69
98
  args.each do |arg|
99
+ condition_list << arg[:value] # Needs to be pure string value
70
100
  arg[:value] = escape_value(arg)
71
101
  end
72
102
  resolver = Sparkql::FunctionResolver.new(name, args)
73
103
 
74
104
  resolver.validate
75
105
  if(resolver.errors?)
76
- errors += resolver.errors
106
+ tokenizer_error(:token => @lexer.last_field,
107
+ :message => "Error parsing function #{resolver.errors.join(',')}",
108
+ :status => :fatal,
109
+ :syntax => true)
77
110
  return nil
78
111
  else
79
- return resolver.call()
112
+ result = resolver.call()
113
+ return result.nil? ? result : result.merge(:condition => "#{name}(#{condition_list.join(',')})")
80
114
  end
81
115
  end
82
116
 
@@ -90,4 +124,54 @@ module Sparkql::ParserTools
90
124
  :syntax => true)
91
125
  end
92
126
 
127
+ def validate_level_depth expression
128
+ if @lexer.level > max_level_depth
129
+ compile_error(:token => "(", :expression => expression,
130
+ :message => "You have exceeded the maximum nesting level. Please nest no more than #{max_level_depth} levels deep.",
131
+ :status => :fatal, :syntax => false, :constraint => true )
132
+ end
133
+ end
134
+
135
+ def validate_expressions results
136
+ if results.size > max_expressions
137
+ compile_error(:token => results[max_expressions][:field], :expression => results[max_expressions],
138
+ :message => "You have exceeded the maximum expression count. Please limit to no more than #{max_expressions} expressions in a filter.",
139
+ :status => :fatal, :syntax => false, :constraint => true )
140
+ results.slice!(max_expressions..-1)
141
+ end
142
+ end
143
+
144
+ def validate_multiple_values values
145
+ values = Array(values)
146
+ if values.size > max_values
147
+ compile_error(:token => values[max_values],
148
+ :message => "You have exceeded the maximum value count. Please limit to #{max_values} values in a single expression.",
149
+ :status => :fatal, :syntax => false, :constraint => true )
150
+ values.slice!(max_values..-1)
151
+ end
152
+ end
153
+
154
+ def validate_multiple_arguments args
155
+ args = Array(args)
156
+ if args.size > max_values
157
+ compile_error(:token => args[max_values],
158
+ :message => "You have exceeded the maximum parameter count. Please limit to #{max_values} parameters to a single function.",
159
+ :status => :fatal, :syntax => false, :constraint => true )
160
+ args.slice!(max_values..-1)
161
+ end
162
+ end
163
+
164
+ # If both types support coercion with eachother, always selects the highest
165
+ # precision type to return as a reflection of the two. Any type that doesn't
166
+ # support coercion with the other type returns nil
167
+ def coercible_types type1, type2
168
+ if DATE_TYPES.include?(type1) && DATE_TYPES.include?(type2)
169
+ DATE_TYPES.first
170
+ elsif NUMBER_TYPES.include?(type1) && NUMBER_TYPES.include?(type2)
171
+ NUMBER_TYPES.first
172
+ else
173
+ nil
174
+ end
175
+ end
176
+
93
177
  end
data/lib/sparkql/token.rb CHANGED
@@ -4,18 +4,20 @@ module Sparkql::Token
4
4
  LPAREN = /\(/
5
5
  RPAREN = /\)/
6
6
  KEYWORD = /[A-Za-z]+/
7
- STANDARD_FIELD = /[A-Z]+[A-Za-z]*/
8
- CUSTOM_FIELD = /^(\"([^$."][^."]+)\".\"([^$."][^."]+)\")/
7
+ STANDARD_FIELD = /[A-Z]+[A-Za-z0-9]*/
8
+ CUSTOM_FIELD = /^(\"([^$."][^."]+)\".\"([^$."][^."]*)\")/
9
9
  INTEGER = /^\-?[0-9]+/
10
10
  DECIMAL = /^\-?[0-9]+\.[0-9]+/
11
11
  CHARACTER = /^'([^'\\]*(\\.[^'\\]*)*)'/
12
12
  DATE = /^[0-9]{4}\-[0-9]{2}\-[0-9]{2}/
13
- DATETIME = /^[0-9]{4}\-[0-9]{2}\-[0-9]{2}T[0-9]{2}\:[0-9]{2}\:[0-9]{2}\.[0-9]{6}/
13
+ DATETIME = /^[0-9]{4}\-[0-9]{2}\-[0-9]{2}T[0-9]{2}\:[0-9]{2}(\:[0-9]{2})?(\.[0-9]{1,50})?(((\+|-)[0-9]{2}\:?[0-9]{2})|Z)?/
14
14
  BOOLEAN = /^true|false/
15
15
  NULL = /NULL|null|Null/
16
16
  # Reserved words
17
+ RANGE_OPERATOR = 'Bt'
17
18
  EQUALITY_OPERATORS = ['Eq','Ne']
18
- OPERATORS = ['Eq','Ne','Gt','Ge','Lt','Le'] + EQUALITY_OPERATORS
19
+ OPERATORS = ['Gt','Ge','Lt','Le'] + EQUALITY_OPERATORS
20
+ UNARY_CONJUNCTIONS = ['Not']
19
21
  CONJUNCTIONS = ['And','Or']
20
22
 
21
- end
23
+ end
data/script/bootstrap ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+
3
+ ruby -v
4
+ echo "==> Installing gems..."
5
+ bundle check --path .bundle 2>&1 > /dev/null || {
6
+ bundle install --quiet --path .bundle
7
+ }
data/script/ci_build ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ script/bootstrap
5
+
6
+ echo "==> Running tests..."
7
+ bundle exec rake ci:setup:testunit test
@@ -0,0 +1,63 @@
1
+ # Parses the grammar into a fancy markdown document.
2
+
3
+ class Markdownify
4
+
5
+ def initialize file
6
+ @file = file
7
+ @line_num = 0
8
+ @markdowning = false
9
+ @codeblock = false
10
+ end
11
+
12
+ def format!
13
+ line_num=0
14
+ markdowning = false
15
+ File.open(@file).each do |line|
16
+ if line =~ /^\#STOP_MARKDOWN/
17
+ @markdowning = false
18
+ end
19
+ if markdowning? && !(line =~ /^\s+$/)
20
+ print format_line(line)
21
+ end
22
+ if line =~ /^\#START_MARKDOWN/
23
+ @markdowning = true
24
+ end
25
+ end
26
+ finish_code_block if @codeblock
27
+ end
28
+
29
+ def markdowning?
30
+ @markdowning
31
+ end
32
+
33
+ def format_line(line)
34
+ if line =~ /\s*\#/
35
+ finish_code_block if @codeblock
36
+ @codeblock = false
37
+ format_doc line
38
+ else
39
+ start_code_block unless @codeblock
40
+ @codeblock = true
41
+ format_bnf line
42
+ end
43
+ end
44
+
45
+ def format_doc line
46
+ line.sub(/\s*\#\s*/, '')
47
+ end
48
+
49
+ def format_bnf line
50
+ bnf = line.gsub(/\{.+\}/, '')
51
+ " #{bnf}"
52
+ end
53
+
54
+ def start_code_block
55
+ print "\n\n```\n"
56
+ end
57
+
58
+ def finish_code_block
59
+ print "```\n\n"
60
+ end
61
+ end
62
+
63
+ Markdownify.new('lib/sparkql/parser.y').format!
data/script/release ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -e
3
+
4
+ rm -rf pkg/*
5
+ bundle exec rake build
6
+ gem push pkg/* $@
data/sparkql.gemspec CHANGED
@@ -12,17 +12,22 @@ Gem::Specification.new do |s|
12
12
  s.description = %q{Specification and base implementation of the Spark API parsing system.}
13
13
 
14
14
  s.rubyforge_project = "sparkql"
15
-
15
+
16
+ s.license = 'Apache 2.0'
17
+
16
18
  s.files = `git ls-files`.split("\n")
17
19
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
20
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
21
  s.require_paths = ["lib"]
20
22
 
23
+ # georuby 2.1.x adds ruby 1.9-only syntax, so that's
24
+ # a no-go for us at the moment
25
+ s.add_dependency 'georuby', '~> 2.0.0'
21
26
  s.add_development_dependency 'racc', '1.4.8'
22
- s.add_development_dependency 'flexmls_gems', '~> 0.2.9'
23
27
  s.add_development_dependency 'rake', '~> 0.9.2'
24
28
  s.add_development_dependency 'test-unit', '~> 2.1.0'
25
29
  s.add_development_dependency 'ci_reporter', '~> 1.6'
30
+ s.add_development_dependency 'mocha', '~> 0.12.0'
26
31
  s.add_development_dependency 'rcov', '~> 0.9.9'
27
32
 
28
33
  end
data/test/test_helper.rb CHANGED
@@ -1,2 +1,3 @@
1
1
  require 'test/unit'
2
- require 'sparkql'
2
+ require 'mocha'
3
+ require 'sparkql'
@@ -0,0 +1,30 @@
1
+ require 'test_helper'
2
+
3
+ class ParserTest < Test::Unit::TestCase
4
+ include Sparkql
5
+
6
+ def test_error_defaults
7
+ errors = ParserError.new
8
+ assert errors.syntax?
9
+ assert !errors.constraint?
10
+ end
11
+
12
+ def test_error_constraint
13
+ errors = ParserError.new(:constraint => true, :syntax => false)
14
+ assert !errors.syntax?
15
+ assert errors.constraint?
16
+ end
17
+
18
+ def test_process_fatal_errors
19
+ p = ErrorsProcessor.new(ParserError.new(:status => :fatal))
20
+ assert p.fatal_errors?
21
+ assert !p.dropped_errors?
22
+ end
23
+
24
+ def test_process_dropped_errors
25
+ p = ErrorsProcessor.new(ParserError.new(:status => :dropped))
26
+ assert p.dropped_errors?
27
+ assert !p.fatal_errors?
28
+ end
29
+
30
+ end
@@ -20,6 +20,12 @@ class ExpressionStateTest < Test::Unit::TestCase
20
20
  assert !@subject.needs_join?, "#{@subject.inspect} Expressions:#{ @expressions.inspect}"
21
21
  end
22
22
 
23
+ def test_not
24
+ filter = '"General Property Description"."Taxes" Lt 500.0 Not "General Property Description"."Taxes2" Eq 1.0'
25
+ process(filter)
26
+ assert @subject.needs_join?
27
+ end
28
+
23
29
  def test_and
24
30
  filter = '"General Property Description"."Taxes" Lt 500.0 And "General Property Description"."Taxes2" Eq 1.0'
25
31
  process(filter)
@@ -46,6 +52,38 @@ class ExpressionStateTest < Test::Unit::TestCase
46
52
  assert @subject.needs_join?
47
53
  end
48
54
 
55
+ # Nesting
56
+ def test_nested_or
57
+ parse '"General Property Description"."Taxes" Lt 5.0 Or ("General Property Description"."Taxes" Gt 4.0)'
58
+ @expressions.each do |ex|
59
+ @subject.push(ex)
60
+ assert @subject.needs_join?, "#{@subject.inspect} Expression:#{ ex.inspect}"
61
+ end
62
+ end
63
+
64
+ def test_nested_ors
65
+ parse '"Tax"."Taxes" Lt 5.0 Or ("Tax"."Taxes" Gt 4.0 Or "Tax"."Taxes" Gt 2.0)'
66
+ @subject.push(@expressions[0])
67
+ assert @subject.needs_join?
68
+ @subject.push(@expressions[1])
69
+ assert @subject.needs_join?
70
+ @subject.push(@expressions[2])
71
+ assert !@subject.needs_join?
72
+ end
73
+
74
+ # Nesting
75
+ def test_nested_and
76
+ parse '"Tax"."Taxes" Lt 5.0 Or ("Tax"."Taxes" Gt 4.0 And "Tax"."Taxes" Gt 2.0)'
77
+ @expressions.each do |ex|
78
+ @subject.push(ex)
79
+ assert @subject.needs_join?, "#{@subject.inspect} Expression:#{ ex.inspect}"
80
+ end
81
+ end
82
+
83
+ def parse(filter)
84
+ @expressions = @parser.parse(filter)
85
+ end
86
+
49
87
  def process(filter)
50
88
  @expressions = @parser.parse(filter)
51
89
  @expressions.each do |ex|