sparkql 0.1.8 → 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- 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,9 +1,18 @@
|
|
1
1
|
require 'test_helper'
|
2
|
+
require 'sparkql/geo'
|
2
3
|
|
3
4
|
class ParserTest < Test::Unit::TestCase
|
4
5
|
include Sparkql
|
5
6
|
|
6
|
-
|
7
|
+
test "function parameters and name preserved" do
|
8
|
+
f = FunctionResolver.new('radius', [{:type => :character,
|
9
|
+
:value => "35.12 -68.33"},{:type => :decimal, :value => 1.0}])
|
10
|
+
value = f.call
|
11
|
+
assert_equal 'radius', value[:function_name]
|
12
|
+
assert_equal(["35.12 -68.33", 1.0], value[:function_parameters])
|
13
|
+
end
|
14
|
+
|
15
|
+
test "now()" do
|
7
16
|
start = Time.now
|
8
17
|
f = FunctionResolver.new('now', [])
|
9
18
|
f.validate
|
@@ -14,8 +23,9 @@ class ParserTest < Test::Unit::TestCase
|
|
14
23
|
assert (-5 < test_time - start && 5 > test_time - start), "Time range off by more than five seconds #{test_time - start} '#{test_time} - #{start}'"
|
15
24
|
end
|
16
25
|
|
17
|
-
|
18
|
-
d = Date.
|
26
|
+
test "days()" do
|
27
|
+
d = Date.new(2012,10,20)
|
28
|
+
Date.expects(:today).returns(d)
|
19
29
|
dt = DateTime.new(d.year, d.month,d.day, 0,0,0, DateTime.now.offset)
|
20
30
|
start = Time.parse(dt.to_s)
|
21
31
|
f = FunctionResolver.new('days', [{:type=>:integer, :value =>7}])
|
@@ -24,10 +34,99 @@ class ParserTest < Test::Unit::TestCase
|
|
24
34
|
value = f.call
|
25
35
|
assert_equal :date, value[:type]
|
26
36
|
test_time = Time.parse(value[:value])
|
27
|
-
assert (
|
37
|
+
assert (615000 > test_time - start && 600000 < test_time - start), "Time range off by more than five seconds #{test_time - start} '#{test_time} - #{start}'"
|
38
|
+
end
|
39
|
+
|
40
|
+
test "months()" do
|
41
|
+
dt = DateTime.new(2014, 1, 6, 0, 0, 0, 0)
|
42
|
+
DateTime.expects(:now).once.returns(dt)
|
43
|
+
|
44
|
+
f = FunctionResolver.new('months', [{:type=>:integer, :value =>3}])
|
45
|
+
f.validate
|
46
|
+
assert !f.errors?, "Errors resolving months(): #{f.errors.inspect}"
|
47
|
+
value = f.call
|
48
|
+
assert_equal :date, value[:type]
|
49
|
+
|
50
|
+
assert_equal "2014-04-06", value[:value]
|
51
|
+
end
|
52
|
+
|
53
|
+
test "years()" do
|
54
|
+
dt = DateTime.new(2014, 1, 6, 0, 0, 0, 0)
|
55
|
+
DateTime.expects(:now).once.returns(dt)
|
56
|
+
f = FunctionResolver.new('years', [{:type=>:integer, :value =>-4}])
|
57
|
+
f.validate
|
58
|
+
assert !f.errors?, "Errors resolving years(): #{f.errors.inspect}"
|
59
|
+
value = f.call
|
60
|
+
assert_equal :date, value[:type]
|
61
|
+
assert_equal '2010-01-06', value[:value], "negative values should go back in time"
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
|
66
|
+
# Polygon searches
|
67
|
+
|
68
|
+
test "radius()" do
|
69
|
+
f = FunctionResolver.new('radius', [{:type => :character, :value => "35.12 -68.33"},{:type => :decimal, :value => 1.0}])
|
70
|
+
f.validate
|
71
|
+
assert !f.errors?, "Errors #{f.errors.inspect}"
|
72
|
+
value = f.call
|
73
|
+
assert_equal :shape, value[:type]
|
74
|
+
assert_equal GeoRuby::SimpleFeatures::Circle, value[:value].class
|
75
|
+
assert_equal [-68.33, 35.12], value[:value].center.to_coordinates, "#{value[:value].inspect} "
|
76
|
+
assert_equal 1.0, value[:value].radius, "#{value[:value].inspect} "
|
77
|
+
end
|
78
|
+
|
79
|
+
test "radius() can be overloaded with a ListingKey" do
|
80
|
+
f = FunctionResolver.new('radius', [{:type => :character, :value => "20100000000000000000000000"},
|
81
|
+
{:type => :decimal, :value => 1.0}])
|
82
|
+
f.validate
|
83
|
+
assert !f.errors?, "Errors #{f.errors.inspect}"
|
84
|
+
value = f.call
|
85
|
+
assert_equal :shape, value[:type]
|
86
|
+
assert_equal Sparkql::Geo::RecordRadius, value[:value].class
|
87
|
+
assert_equal "20100000000000000000000000", value[:value].record_id, "#{value[:value].inspect} "
|
88
|
+
assert_equal 1.0, value[:value].radius, "#{value[:value].inspect} "
|
89
|
+
end
|
90
|
+
|
91
|
+
test "radius() fails if not given coords or a flex ID" do
|
92
|
+
f = FunctionResolver.new('radius', [{:type => :character, :value => "35.12,-68.33"},
|
93
|
+
{:type => :decimal, :value => 1.0}])
|
94
|
+
f.validate
|
95
|
+
value = f.call
|
96
|
+
assert f.errors?
|
97
|
+
end
|
98
|
+
|
99
|
+
test "polygon()" do
|
100
|
+
f = FunctionResolver.new('polygon', [{:type => :character, :value => "35.12 -68.33,35.12 -68.32, 35.13 -68.32,35.13 -68.33"}])
|
101
|
+
f.validate
|
102
|
+
assert !f.errors?, "Errors #{f.errors.inspect}"
|
103
|
+
value = f.call
|
104
|
+
assert_equal :shape, value[:type]
|
105
|
+
assert_equal GeoRuby::SimpleFeatures::Polygon, value[:value].class
|
106
|
+
assert_equal [[-68.33, 35.12], [-68.32, 35.12], [-68.32, 35.13], [-68.33, 35.13], [-68.33, 35.12]], value[:value].to_coordinates.first, "#{value[:value].inspect} "
|
107
|
+
end
|
108
|
+
|
109
|
+
test "linestring()" do
|
110
|
+
f = FunctionResolver.new('linestring', [{:type => :character, :value => "35.12 -68.33,35.12 -68.32"}])
|
111
|
+
f.validate
|
112
|
+
assert !f.errors?, "Errors #{f.errors.inspect}"
|
113
|
+
value = f.call
|
114
|
+
assert_equal :shape, value[:type]
|
115
|
+
assert_equal GeoRuby::SimpleFeatures::LineString, value[:value].class
|
116
|
+
assert_equal [[-68.33, 35.12], [-68.32, 35.12]], value[:value].to_coordinates, "#{value[:value].inspect} "
|
117
|
+
end
|
118
|
+
|
119
|
+
test "rectangle()" do
|
120
|
+
f = FunctionResolver.new('rectangle', [{:type => :character, :value => "35.12 -68.33, 35.13 -68.32"}])
|
121
|
+
f.validate
|
122
|
+
assert !f.errors?, "Errors #{f.errors.inspect}"
|
123
|
+
value = f.call
|
124
|
+
assert_equal :shape, value[:type]
|
125
|
+
assert_equal GeoRuby::SimpleFeatures::Polygon, value[:value].class
|
126
|
+
assert_equal [[-68.33,35.12], [-68.32,35.12], [-68.32,35.13], [-68.33,35.13], [-68.33,35.12]], value[:value].to_coordinates.first, "#{value[:value].inspect} "
|
28
127
|
end
|
29
128
|
|
30
|
-
|
129
|
+
test "invalid params" do
|
31
130
|
f = FunctionResolver.new('now', [{:type => :character, :value=>'bad value'}])
|
32
131
|
f.validate
|
33
132
|
assert f.errors?, "'now' function does not support parameters"
|
@@ -40,8 +139,15 @@ class ParserTest < Test::Unit::TestCase
|
|
40
139
|
f.validate
|
41
140
|
assert f.errors?, "'days' function needs integer parameter"
|
42
141
|
end
|
142
|
+
|
143
|
+
test "assert nil returned when function called with errors" do
|
144
|
+
f = FunctionResolver.new('radius', [{:type => :character,
|
145
|
+
:value => "35.12 -68.33, 35.13 -68.34"},{:type => :decimal,
|
146
|
+
:value => 1.0}])
|
147
|
+
assert_nil f.call
|
148
|
+
end
|
43
149
|
|
44
|
-
|
150
|
+
test "invalid function" do
|
45
151
|
f = FunctionResolver.new('then', [])
|
46
152
|
f.validate
|
47
153
|
assert f.errors?, "'then' is not a function"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Sparkql::Geo::RecordRadiusTest < Test::Unit::TestCase
|
4
|
+
def test_valid_record_id
|
5
|
+
assert Sparkql::Geo::RecordRadius.valid_record_id?("20100000000000000000000000")
|
6
|
+
|
7
|
+
["2010000000000000000000000",
|
8
|
+
"201000000000000000000000000",
|
9
|
+
"test",
|
10
|
+
"12.45,-96.5"].each do |bad_record_id|
|
11
|
+
assert !Sparkql::Geo::RecordRadius.valid_record_id?(bad_record_id),
|
12
|
+
"'#{bad_record_id}' should not be a valid record_id"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/test/unit/lexer_test.rb
CHANGED
@@ -10,12 +10,34 @@ class LexerTest < Test::Unit::TestCase
|
|
10
10
|
assert_equal :STANDARD_FIELD, token.first, standard_field
|
11
11
|
end
|
12
12
|
end
|
13
|
+
|
14
|
+
def test_standard_field_formats
|
15
|
+
["City", "PostalCodePlus4", "Inb4ParserError"].each do |standard_field|
|
16
|
+
@lexer = Lexer.new("#{standard_field} Eq true")
|
17
|
+
token = @lexer.shift
|
18
|
+
assert_equal :STANDARD_FIELD, token.first, standard_field
|
19
|
+
assert_equal token[1], standard_field
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_bad_standard_field_formats
|
24
|
+
@lexer = Lexer.new("4PostalCodePlus4 Eq true")
|
25
|
+
token = @lexer.shift
|
26
|
+
assert_equal :INTEGER, token.first
|
27
|
+
assert_equal token[1][:value], "4"
|
28
|
+
end
|
29
|
+
|
13
30
|
def test_check_reserved_words_conjunctions
|
14
31
|
['And Derp', 'Or 123'].each do |conjunction|
|
15
32
|
@lexer = Lexer.new(conjunction)
|
16
33
|
token = @lexer.shift
|
17
34
|
assert_equal :CONJUNCTION, token.first, conjunction
|
18
35
|
end
|
36
|
+
['Not Lol'].each do |conjunction|
|
37
|
+
@lexer = Lexer.new(conjunction)
|
38
|
+
token = @lexer.shift
|
39
|
+
assert_equal :UNARY_CONJUNCTION, token.first, conjunction
|
40
|
+
end
|
19
41
|
end
|
20
42
|
|
21
43
|
def test_check_reserved_words_operators
|
@@ -24,6 +46,27 @@ class LexerTest < Test::Unit::TestCase
|
|
24
46
|
token = @lexer.shift
|
25
47
|
assert_equal :OPERATOR, token.first, op
|
26
48
|
end
|
49
|
+
|
50
|
+
['Bt 1234','Bt 1234,12345'].each do |op|
|
51
|
+
@lexer = Lexer.new(op)
|
52
|
+
token = @lexer.shift
|
53
|
+
assert_equal :RANGE_OPERATOR, token.first, op
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_datetimes_matches
|
58
|
+
['2013-07-26T10:22:15.422804', '2013-07-26T10:22:15'].each do |op|
|
59
|
+
@lexer = Lexer.new(op)
|
60
|
+
token = @lexer.shift
|
61
|
+
assert_equal :DATETIME, token.first, op
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_utc_offsets
|
66
|
+
['2013-07-26T10:22:15.422804-0300', '2013-07-26T10:22:15+0400'].each do |op|
|
67
|
+
@lexer = Lexer.new(op)
|
68
|
+
token = @lexer.shift
|
69
|
+
assert_equal :DATETIME, token.first, op
|
70
|
+
end
|
27
71
|
end
|
28
|
-
|
29
72
|
end
|
@@ -72,6 +72,11 @@ class ParserCompatabilityTest < Test::Unit::TestCase
|
|
72
72
|
:type => :decimal,
|
73
73
|
:operator => "In"
|
74
74
|
},
|
75
|
+
{
|
76
|
+
:string => "FloatField Eq 100.1,2,3.4",
|
77
|
+
:type => :decimal,
|
78
|
+
:operator => "In"
|
79
|
+
},
|
75
80
|
{
|
76
81
|
:string => "DateField Eq 2010-10-10",
|
77
82
|
:type => :date,
|
@@ -155,7 +160,7 @@ class ParserCompatabilityTest < Test::Unit::TestCase
|
|
155
160
|
@test_filters.each do |elem|
|
156
161
|
parser = Parser.new
|
157
162
|
expressions = parser.tokenize( elem[:string] )
|
158
|
-
assert !parser.errors?, "Query: #{elem.inspect}"
|
163
|
+
assert !parser.errors?, "Query: #{elem.inspect} #{parser.errors.inspect}"
|
159
164
|
assert_equal elem[:operator], expressions.first[:operator]
|
160
165
|
end
|
161
166
|
end
|
@@ -286,31 +291,50 @@ class ParserCompatabilityTest < Test::Unit::TestCase
|
|
286
291
|
assert_nil ex
|
287
292
|
end
|
288
293
|
end
|
294
|
+
|
295
|
+
test "mulitples shouldn't restrict based on string size(OMG LOL THAT WAS FUNNYWTF)" do
|
296
|
+
parser = Parser.new
|
297
|
+
ex = parser.tokenize("ListAgentId Eq '20110000000000000000000000'")
|
298
|
+
assert !parser.errors?, parser.inspect
|
299
|
+
end
|
289
300
|
|
290
301
|
test "max out values" do
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
302
|
+
parser = Parser.new
|
303
|
+
to_the_max = []
|
304
|
+
210.times do |x|
|
305
|
+
to_the_max << x
|
306
|
+
end
|
307
|
+
ex = parser.tokenize("City Eq #{to_the_max.join(',')}")
|
308
|
+
vals = ex.first[:value]
|
309
|
+
assert_equal 200, vals.size
|
310
|
+
assert parser.errors?
|
299
311
|
end
|
300
312
|
|
301
313
|
test "max out expressions" do
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
314
|
+
parser = Parser.new
|
315
|
+
to_the_max = []
|
316
|
+
60.times do |x|
|
317
|
+
to_the_max << "City Eq 'Fargo'"
|
318
|
+
end
|
319
|
+
vals = parser.tokenize(to_the_max.join(" And "))
|
320
|
+
assert_equal 50, vals.size
|
321
|
+
assert parser.errors?
|
322
|
+
end
|
323
|
+
|
324
|
+
test "max out function args" do
|
325
|
+
parser = Parser.new
|
326
|
+
to_the_max = []
|
327
|
+
201.times do |x|
|
328
|
+
to_the_max << "1"
|
329
|
+
end
|
330
|
+
vals = parser.tokenize("Args Eq myfunc(#{to_the_max.join(",")})")
|
331
|
+
assert parser.errors?
|
332
|
+
assert parser.errors.first.constraint?
|
309
333
|
end
|
310
334
|
|
311
335
|
test "API-107 And/Or in string spiel" do
|
312
336
|
search_strings = ['Tom And Jerry', 'Tom Or Jerry', 'And Or Eq', 'City Eq \\\'Fargo\\\'',
|
313
|
-
' And Eq Or ', 'Or And
|
337
|
+
' And Eq Or ', 'Or And Not']
|
314
338
|
search_strings.each do |s|
|
315
339
|
parser = Parser.new
|
316
340
|
parser.tokenize("City Eq '#{s}' And PropertyType Eq 'A'")
|
@@ -433,5 +457,52 @@ class ParserCompatabilityTest < Test::Unit::TestCase
|
|
433
457
|
expressions = parser.tokenize( "BooleanField Eq true" )
|
434
458
|
assert_equal true, parser.escape_value(expressions.first)
|
435
459
|
end
|
460
|
+
|
461
|
+
test "Between" do
|
462
|
+
["BathsFull Bt 10,20", "DateField Bt 2012-12-31,2013-01-31"].each do |f|
|
463
|
+
parser = Parser.new
|
464
|
+
expressions = parser.tokenize f
|
465
|
+
assert !parser.errors?, "should successfully parse proper between values, but #{parser.errors.first}"
|
466
|
+
end
|
467
|
+
|
468
|
+
# truckload of fail
|
469
|
+
["BathsFull Bt 2012-12-31,1", "DateField Bt 10,2012-12-31"].each do |f|
|
470
|
+
parser = Parser.new
|
471
|
+
expressions = parser.tokenize f
|
472
|
+
assert parser.errors?, "should have a type mismatch: #{parser.errors.first}"
|
473
|
+
assert_match /Type mismatch/, parser.errors.first.message
|
474
|
+
end
|
475
|
+
|
476
|
+
end
|
477
|
+
|
478
|
+
test "integer type coercion" do
|
479
|
+
parser = Parser.new
|
480
|
+
expression = parser.tokenize( "DecimalField Eq 100").first
|
481
|
+
assert parser.send(:check_type!, expression, :decimal)
|
482
|
+
assert_equal 100.0, parser.escape_value(expression)
|
483
|
+
end
|
484
|
+
|
485
|
+
test "datetime->date type coercion" do
|
486
|
+
t = Time.now
|
487
|
+
parser = Parser.new
|
488
|
+
expression = parser.tokenize( "DateField Eq now()").first
|
489
|
+
assert !parser.errors?
|
490
|
+
assert parser.send(:check_type!, expression, :date)
|
491
|
+
assert_equal t.strftime(Sparkql::FunctionResolver::STRFTIME_FORMAT),
|
492
|
+
parser.escape_value(expression).strftime(Sparkql::FunctionResolver::STRFTIME_FORMAT)
|
493
|
+
end
|
494
|
+
|
495
|
+
test "datetime->date type coercion array" do
|
496
|
+
today = Time.now
|
497
|
+
parser = Parser.new
|
498
|
+
expression = parser.tokenize('"Custom"."DateField" Bt days(-1),now()').first
|
499
|
+
assert !parser.errors?
|
500
|
+
assert parser.send(:check_type!, expression, :date)
|
501
|
+
yesterday = today - 3600 * 24
|
502
|
+
assert_equal [ yesterday.strftime(Sparkql::FunctionResolver::STRFTIME_FORMAT),
|
503
|
+
today.strftime(Sparkql::FunctionResolver::STRFTIME_FORMAT)],
|
504
|
+
parser.escape_value(expression).map { |i| i.strftime(Sparkql::FunctionResolver::STRFTIME_FORMAT)}
|
505
|
+
end
|
506
|
+
|
436
507
|
|
437
508
|
end
|