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,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
|