sparkql 0.1.8 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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|