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.
- checksums.yaml +15 -0
- data/.gitignore +2 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +11 -0
- data/GRAMMAR.md +208 -0
- data/Gemfile +1 -1
- data/README.md +2 -2
- data/Rakefile +17 -4
- data/VERSION +1 -1
- data/lib/sparkql/errors.rb +10 -8
- data/lib/sparkql/expression_state.rb +7 -4
- data/lib/sparkql/function_resolver.rb +172 -3
- data/lib/sparkql/geo/record_circle.rb +18 -0
- data/lib/sparkql/geo.rb +1 -0
- data/lib/sparkql/lexer.rb +10 -14
- data/lib/sparkql/parser.rb +171 -97
- data/lib/sparkql/parser.y +111 -8
- data/lib/sparkql/parser_compatibility.rb +68 -28
- data/lib/sparkql/parser_tools.rb +104 -20
- data/lib/sparkql/token.rb +7 -5
- data/script/bootstrap +7 -0
- data/script/ci_build +7 -0
- data/script/markdownify.rb +63 -0
- data/script/release +6 -0
- data/sparkql.gemspec +7 -2
- data/test/test_helper.rb +2 -1
- data/test/unit/errors_test.rb +30 -0
- data/test/unit/expression_state_test.rb +38 -0
- data/test/unit/function_resolver_test.rb +112 -6
- data/test/unit/geo/record_circle_test.rb +15 -0
- data/test/unit/lexer_test.rb +44 -1
- data/test/unit/parser_compatability_test.rb +88 -17
- data/test/unit/parser_test.rb +259 -0
- metadata +127 -126
- data/.rvmrc +0 -2
@@ -1,22 +1,22 @@
|
|
1
1
|
# Required interface for existing parser implementations
|
2
2
|
module Sparkql::ParserCompatibility
|
3
|
-
|
4
|
-
MAXIMUM_MULTIPLE_VALUES =
|
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 &&
|
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] &&
|
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
|
data/lib/sparkql/parser_tools.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
58
|
-
|
59
|
-
end
|
75
|
+
condition = lit1[:condition] || lit1[:value]
|
76
|
+
array << lit2[:value]
|
60
77
|
{
|
61
|
-
:type =>
|
78
|
+
:type => final_type ,
|
62
79
|
:value => array,
|
63
|
-
:multiple => "true"
|
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
|
-
|
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
|
-
|
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-
|
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]{
|
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 = ['
|
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
data/script/ci_build
ADDED
@@ -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
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
@@ -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|
|