sparkql 1.2.2 → 1.2.7
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 +5 -13
- data/CHANGELOG.md +21 -0
- data/GRAMMAR.md +5 -4
- data/Gemfile +1 -2
- data/VERSION +1 -1
- data/lib/sparkql/function_resolver.rb +768 -676
- data/lib/sparkql/parser.rb +198 -178
- data/lib/sparkql/parser.y +4 -2
- data/lib/sparkql/parser_compatibility.rb +36 -17
- data/lib/sparkql/parser_tools.rb +67 -23
- data/sparkql.gemspec +19 -17
- data/test/unit/function_resolver_test.rb +455 -191
- data/test/unit/parser_compatability_test.rb +15 -0
- data/test/unit/parser_test.rb +148 -13
- metadata +34 -35
- data/.ruby-version +0 -1
checksums.yaml
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
|
|
5
|
-
data.tar.gz: !binary |-
|
|
6
|
-
ZjhiZmI1Mjc4ZDM0YmE5M2Q4YzU5NTMwMmRhODY3MDY5YWE3ODQ5YQ==
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 216e474f5077d8b33ede2b8bc61118bed6eab33d530eae71a642306d53257e6a
|
|
4
|
+
data.tar.gz: da6486cedc2ed93ac8db963572afbeccead88643535430442e59d8b941dd0fe6
|
|
7
5
|
SHA512:
|
|
8
|
-
metadata.gz:
|
|
9
|
-
|
|
10
|
-
YTg5NjJhNmFlMDVlNzkxNWM2MDgyYTQwZjI5ZTc1OGZjODI1MzIxMTE2ZGQ5
|
|
11
|
-
ZjBmYmQ2ZjdiNzNlODIwOWUzMjI5ODEzMThiMTcyZTJkYWUwOTk=
|
|
12
|
-
data.tar.gz: !binary |-
|
|
13
|
-
YmUyZjU5ZWI3NWNhOGVhYmU5MDQxZDNhZjhhZWEwNzJmMzNmZjkzNTUxODQ5
|
|
14
|
-
YzIwODQwMjgzMThlODc4ZWJiYjdkMzM0NDU4ZjBiZjlhMGMwODQ3YzdkNDRh
|
|
15
|
-
MTRiYzZkMmVmMTM0NGNhZWY1ZWJkZDcwYjEzZDIyMDU0ZjY4YmM=
|
|
6
|
+
metadata.gz: 87f6fa0829bb7d99e49bb1675f4d8090ce9271d0dea9ae8da15522b57f86f03f87df21f1187cd4cb6f5812f880da74f690dfae12f99fa52d41957fa09062d82f
|
|
7
|
+
data.tar.gz: 8575083d2bf0a94bb299109617ab2f41641b01636427883ace5733b5500859e3419a6d977f30a79c15602c7b9fe1ec7315c7d4508cab4f1829f19a365c7547f5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
v1.2.7, 2021-05-06
|
|
2
|
+
-------------------
|
|
3
|
+
* [IMPROVEMENT] dayofweek(), dayofyear(), and weekdays() functions
|
|
4
|
+
|
|
5
|
+
v1.2.6, 2019-04-01
|
|
6
|
+
-------------------
|
|
7
|
+
* [IMPROVEMENT] hours(), minutes(), and seconds() functions
|
|
8
|
+
|
|
9
|
+
v1.2.5, 2018-12-19
|
|
10
|
+
-------------------
|
|
11
|
+
* [BUGFIX] Correctly handle arithmetic grouping
|
|
12
|
+
|
|
13
|
+
v1.2.4, 2018-12-13
|
|
14
|
+
-------------------
|
|
15
|
+
* [IMPROVEMENT] Support decimal arithmetic
|
|
16
|
+
* [BUGFIX] Correctly handle type checking with arithmetic
|
|
17
|
+
|
|
18
|
+
v1.2.3, 2018-12-05
|
|
19
|
+
-------------------
|
|
20
|
+
* [IMPROVEMENT] Support Arithmetic Grouping and Negation
|
|
21
|
+
|
|
1
22
|
v1.2.2, 2018-11-28
|
|
2
23
|
-------------------
|
|
3
24
|
* [IMPROVEMENT] Support Arithmetic: Add, Sub, Mul, Div, Mod
|
data/GRAMMAR.md
CHANGED
|
@@ -46,8 +46,7 @@ and criteria for comparing the value of the field to the value(s) of the
|
|
|
46
46
|
condition. The result of evaluating the expression on a resource is a true of
|
|
47
47
|
false for matching the criteria. We are separating functions and arithmetic
|
|
48
48
|
based on if we are acting on the field side or the literal side. This is to
|
|
49
|
-
allow literal folding on the literal side
|
|
50
|
-
to see if a field is in the expression.
|
|
49
|
+
allow literal folding on the literal side.
|
|
51
50
|
|
|
52
51
|
|
|
53
52
|
```
|
|
@@ -96,6 +95,8 @@ One or more expressions encased in parenthesis. There are limitations on nesting
|
|
|
96
95
|
| field_arithmetic_expression MUL field_arithmetic_expression
|
|
97
96
|
| field_arithmetic_expression DIV field_arithmetic_expression
|
|
98
97
|
| field_arithmetic_expression MOD field_arithmetic_expression
|
|
98
|
+
| LPAREN field_arithmetic_expression RPAREN
|
|
99
|
+
| UMINUS field_arithmetic_expression
|
|
99
100
|
| literals
|
|
100
101
|
| field_function_expression
|
|
101
102
|
;
|
|
@@ -117,6 +118,8 @@ on filtering values
|
|
|
117
118
|
: arithmetic_condition
|
|
118
119
|
| literal_list
|
|
119
120
|
| literal
|
|
121
|
+
| LPAREN condition RPAREN
|
|
122
|
+
| UMINUS condition
|
|
120
123
|
;
|
|
121
124
|
arithmetic_condition
|
|
122
125
|
: condition ADD condition
|
|
@@ -203,8 +206,6 @@ Literals that support multiple values in a list for a condition
|
|
|
203
206
|
: INTEGER
|
|
204
207
|
| DECIMAL
|
|
205
208
|
| CHARACTER
|
|
206
|
-
| LPAREN literals RPAREN
|
|
207
|
-
| UMINUS literals
|
|
208
209
|
;
|
|
209
210
|
```
|
|
210
211
|
|
data/Gemfile
CHANGED
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.2.
|
|
1
|
+
1.2.7
|
|
@@ -1,763 +1,855 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
2
4
|
require 'geo_ruby'
|
|
3
5
|
require 'geo_ruby/ewk'
|
|
4
6
|
require 'sparkql/geo'
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
:
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
:
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
:
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
:
|
|
210
|
-
:
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
:status => :fatal )
|
|
231
|
-
end
|
|
232
|
-
count +=1
|
|
8
|
+
module Sparkql
|
|
9
|
+
# Binding class to all supported function calls in the parser. Current support requires that the
|
|
10
|
+
# resolution of function calls to happen on the fly at parsing time at which point a value and
|
|
11
|
+
# value type is required, just as literals would be returned to the expression tokenization level.
|
|
12
|
+
#
|
|
13
|
+
# Name and argument requirements for the function should match the function declaration in
|
|
14
|
+
# SUPPORTED_FUNCTIONS which will run validation on the function syntax prior to execution.
|
|
15
|
+
class FunctionResolver
|
|
16
|
+
SECONDS_IN_MINUTE = 60
|
|
17
|
+
SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60
|
|
18
|
+
SECONDS_IN_DAY = SECONDS_IN_HOUR * 24
|
|
19
|
+
STRFTIME_DATE_FORMAT = '%Y-%m-%d'
|
|
20
|
+
STRFTIME_TIME_FORMAT = '%H:%M:%S.%N'
|
|
21
|
+
VALID_REGEX_FLAGS = ['', 'i'].freeze
|
|
22
|
+
MIN_DATE_TIME = Time.new(1970, 1, 1, 0, 0, 0, '+00:00').iso8601
|
|
23
|
+
MAX_DATE_TIME = Time.new(9999, 12, 31, 23, 59, 59, '+00:00').iso8601
|
|
24
|
+
VALID_CAST_TYPES = %i[field character decimal integer].freeze
|
|
25
|
+
|
|
26
|
+
SUPPORTED_FUNCTIONS = {
|
|
27
|
+
polygon: {
|
|
28
|
+
args: [:character],
|
|
29
|
+
return_type: :shape
|
|
30
|
+
},
|
|
31
|
+
rectangle: {
|
|
32
|
+
args: [:character],
|
|
33
|
+
return_type: :shape
|
|
34
|
+
},
|
|
35
|
+
radius: {
|
|
36
|
+
args: [:character, %i[decimal integer]],
|
|
37
|
+
return_type: :shape
|
|
38
|
+
},
|
|
39
|
+
regex: {
|
|
40
|
+
args: [:character],
|
|
41
|
+
opt_args: [{
|
|
42
|
+
type: :character,
|
|
43
|
+
default: ''
|
|
44
|
+
}],
|
|
45
|
+
return_type: :character
|
|
46
|
+
},
|
|
47
|
+
substring: {
|
|
48
|
+
args: [%i[field character], :integer],
|
|
49
|
+
opt_args: [{
|
|
50
|
+
type: :integer
|
|
51
|
+
}],
|
|
52
|
+
resolve_for_type: true,
|
|
53
|
+
return_type: :character
|
|
54
|
+
},
|
|
55
|
+
trim: {
|
|
56
|
+
args: [%i[field character]],
|
|
57
|
+
resolve_for_type: true,
|
|
58
|
+
return_type: :character
|
|
59
|
+
},
|
|
60
|
+
tolower: {
|
|
61
|
+
args: [%i[field character]],
|
|
62
|
+
resolve_for_type: true,
|
|
63
|
+
return_type: :character
|
|
64
|
+
},
|
|
65
|
+
toupper: {
|
|
66
|
+
args: [%i[field character]],
|
|
67
|
+
resolve_for_type: true,
|
|
68
|
+
return_type: :character
|
|
69
|
+
},
|
|
70
|
+
length: {
|
|
71
|
+
args: [%i[field character]],
|
|
72
|
+
resolve_for_type: true,
|
|
73
|
+
return_type: :integer
|
|
74
|
+
},
|
|
75
|
+
indexof: {
|
|
76
|
+
args: [%i[field character], :character],
|
|
77
|
+
return_type: :integer
|
|
78
|
+
},
|
|
79
|
+
concat: {
|
|
80
|
+
args: [%i[field character], :character],
|
|
81
|
+
resolve_for_type: true,
|
|
82
|
+
return_type: :character
|
|
83
|
+
},
|
|
84
|
+
cast: {
|
|
85
|
+
args: [%i[field character decimal integer null], :character],
|
|
86
|
+
resolve_for_type: true
|
|
87
|
+
},
|
|
88
|
+
round: {
|
|
89
|
+
args: [%i[field decimal]],
|
|
90
|
+
resolve_for_type: true,
|
|
91
|
+
return_type: :integer
|
|
92
|
+
},
|
|
93
|
+
ceiling: {
|
|
94
|
+
args: [%i[field decimal]],
|
|
95
|
+
resolve_for_type: true,
|
|
96
|
+
return_type: :integer
|
|
97
|
+
},
|
|
98
|
+
floor: {
|
|
99
|
+
args: [%i[field decimal]],
|
|
100
|
+
resolve_for_type: true,
|
|
101
|
+
return_type: :integer
|
|
102
|
+
},
|
|
103
|
+
startswith: {
|
|
104
|
+
args: [:character],
|
|
105
|
+
return_type: :startswith
|
|
106
|
+
},
|
|
107
|
+
endswith: {
|
|
108
|
+
args: [:character],
|
|
109
|
+
return_type: :endswith
|
|
110
|
+
},
|
|
111
|
+
contains: {
|
|
112
|
+
args: [:character],
|
|
113
|
+
return_type: :contains
|
|
114
|
+
},
|
|
115
|
+
linestring: {
|
|
116
|
+
args: [:character],
|
|
117
|
+
return_type: :shape
|
|
118
|
+
},
|
|
119
|
+
seconds: {
|
|
120
|
+
args: [:integer],
|
|
121
|
+
return_type: :datetime
|
|
122
|
+
},
|
|
123
|
+
minutes: {
|
|
124
|
+
args: [:integer],
|
|
125
|
+
return_type: :datetime
|
|
126
|
+
},
|
|
127
|
+
hours: {
|
|
128
|
+
args: [:integer],
|
|
129
|
+
return_type: :datetime
|
|
130
|
+
},
|
|
131
|
+
days: {
|
|
132
|
+
args: [:integer],
|
|
133
|
+
return_type: :datetime
|
|
134
|
+
},
|
|
135
|
+
weekdays: {
|
|
136
|
+
args: [:integer],
|
|
137
|
+
return_type: :datetime
|
|
138
|
+
},
|
|
139
|
+
months: {
|
|
140
|
+
args: [:integer],
|
|
141
|
+
return_type: :datetime
|
|
142
|
+
},
|
|
143
|
+
years: {
|
|
144
|
+
args: [:integer],
|
|
145
|
+
return_type: :datetime
|
|
146
|
+
},
|
|
147
|
+
now: {
|
|
148
|
+
args: [],
|
|
149
|
+
return_type: :datetime
|
|
150
|
+
},
|
|
151
|
+
maxdatetime: {
|
|
152
|
+
args: [],
|
|
153
|
+
return_type: :datetime
|
|
154
|
+
},
|
|
155
|
+
mindatetime: {
|
|
156
|
+
args: [],
|
|
157
|
+
return_type: :datetime
|
|
158
|
+
},
|
|
159
|
+
date: {
|
|
160
|
+
args: [%i[field datetime date]],
|
|
161
|
+
resolve_for_type: true,
|
|
162
|
+
return_type: :date
|
|
163
|
+
},
|
|
164
|
+
time: {
|
|
165
|
+
args: [%i[field datetime date]],
|
|
166
|
+
resolve_for_type: true,
|
|
167
|
+
return_type: :time
|
|
168
|
+
},
|
|
169
|
+
year: {
|
|
170
|
+
args: [%i[field datetime date]],
|
|
171
|
+
resolve_for_type: true,
|
|
172
|
+
return_type: :integer
|
|
173
|
+
},
|
|
174
|
+
dayofyear: {
|
|
175
|
+
args: [%i[field datetime date]],
|
|
176
|
+
resolve_for_type: true,
|
|
177
|
+
return_type: :integer
|
|
178
|
+
},
|
|
179
|
+
month: {
|
|
180
|
+
args: [%i[field datetime date]],
|
|
181
|
+
resolve_for_type: true,
|
|
182
|
+
return_type: :integer
|
|
183
|
+
},
|
|
184
|
+
day: {
|
|
185
|
+
args: [%i[field datetime date]],
|
|
186
|
+
resolve_for_type: true,
|
|
187
|
+
return_type: :integer
|
|
188
|
+
},
|
|
189
|
+
dayofweek: {
|
|
190
|
+
args: [%i[field datetime date]],
|
|
191
|
+
resolve_for_type: true,
|
|
192
|
+
return_type: :integer
|
|
193
|
+
},
|
|
194
|
+
hour: {
|
|
195
|
+
args: [%i[field datetime date]],
|
|
196
|
+
resolve_for_type: true,
|
|
197
|
+
return_type: :integer
|
|
198
|
+
},
|
|
199
|
+
minute: {
|
|
200
|
+
args: [%i[field datetime date]],
|
|
201
|
+
resolve_for_type: true,
|
|
202
|
+
return_type: :integer
|
|
203
|
+
},
|
|
204
|
+
second: {
|
|
205
|
+
args: [%i[field datetime date]],
|
|
206
|
+
resolve_for_type: true,
|
|
207
|
+
return_type: :integer
|
|
208
|
+
},
|
|
209
|
+
fractionalseconds: {
|
|
210
|
+
args: [%i[field datetime date]],
|
|
211
|
+
resolve_for_type: true,
|
|
212
|
+
return_type: :decimal
|
|
213
|
+
},
|
|
214
|
+
range: {
|
|
215
|
+
args: %i[character character],
|
|
216
|
+
return_type: :character
|
|
217
|
+
},
|
|
218
|
+
wkt: {
|
|
219
|
+
args: [:character],
|
|
220
|
+
return_type: :shape
|
|
221
|
+
}
|
|
222
|
+
}.freeze
|
|
223
|
+
|
|
224
|
+
# Construct a resolver instance for a function
|
|
225
|
+
# name: function name (String)
|
|
226
|
+
# args: array of literal hashes of the format {:type=><literal_type>, :value=><escaped_literal_value>}.
|
|
227
|
+
# Empty arry for functions that have no arguments.
|
|
228
|
+
def initialize(name, args)
|
|
229
|
+
@name = name
|
|
230
|
+
@args = args
|
|
231
|
+
@errors = []
|
|
233
232
|
end
|
|
234
233
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
234
|
+
# Validate the function instance prior to calling it. All validation failures will show up in the
|
|
235
|
+
# errors array.
|
|
236
|
+
def validate
|
|
237
|
+
name = @name.to_sym
|
|
238
|
+
unless support.key?(name)
|
|
239
|
+
@errors << Sparkql::ParserError.new(token: @name,
|
|
240
|
+
message: "Unsupported function call '#{@name}' for expression",
|
|
241
|
+
status: :fatal)
|
|
242
|
+
return
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
required_args = support[name][:args]
|
|
246
|
+
total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:type] }
|
|
247
|
+
|
|
248
|
+
if @args.size < required_args.size || @args.size > total_args.size
|
|
249
|
+
@errors << Sparkql::ParserError.new(token: @name,
|
|
250
|
+
message: "Function call '#{@name}' requires #{required_args.size} arguments",
|
|
251
|
+
status: :fatal)
|
|
241
252
|
return
|
|
242
253
|
end
|
|
254
|
+
|
|
255
|
+
count = 0
|
|
256
|
+
@args.each do |arg|
|
|
257
|
+
type = arg[:type] == :function ? arg[:return_type] : arg[:type]
|
|
258
|
+
unless Array(total_args[count]).include?(type)
|
|
259
|
+
@errors << Sparkql::ParserError.new(token: @name,
|
|
260
|
+
message: "Function call '#{@name}' has an invalid argument at #{arg[:value]}",
|
|
261
|
+
status: :fatal)
|
|
262
|
+
end
|
|
263
|
+
count += 1
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
if name == :cast
|
|
267
|
+
type = @args.last[:value]
|
|
268
|
+
unless VALID_CAST_TYPES.include?(type.to_sym)
|
|
269
|
+
@errors << Sparkql::ParserError.new(token: @name,
|
|
270
|
+
message: "Function call '#{@name}' requires a castable type.",
|
|
271
|
+
status: :fatal)
|
|
272
|
+
return
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
substring_index_error?(@args[2][:value]) if name == :substring && !@args[2].nil?
|
|
243
277
|
end
|
|
244
278
|
|
|
245
|
-
|
|
246
|
-
|
|
279
|
+
def return_type
|
|
280
|
+
name = @name.to_sym
|
|
281
|
+
|
|
282
|
+
if name == :cast
|
|
283
|
+
@args.last[:value].to_sym
|
|
284
|
+
else
|
|
285
|
+
support[@name.to_sym][:return_type]
|
|
286
|
+
end
|
|
247
287
|
end
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def return_type
|
|
251
|
-
name = @name.to_sym
|
|
252
288
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
289
|
+
attr_reader :errors
|
|
290
|
+
|
|
291
|
+
def errors?
|
|
292
|
+
@errors.size.positive?
|
|
257
293
|
end
|
|
258
|
-
end
|
|
259
|
-
|
|
260
|
-
def errors
|
|
261
|
-
@errors
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def errors?
|
|
265
|
-
@errors.size > 0
|
|
266
|
-
end
|
|
267
|
-
|
|
268
|
-
def support
|
|
269
|
-
SUPPORTED_FUNCTIONS
|
|
270
|
-
end
|
|
271
|
-
|
|
272
|
-
# Execute the function
|
|
273
|
-
def call()
|
|
274
|
-
real_vals = @args.map { |i| i[:value]}
|
|
275
|
-
name = @name.to_sym
|
|
276
294
|
|
|
277
|
-
|
|
278
|
-
|
|
295
|
+
def support
|
|
296
|
+
SUPPORTED_FUNCTIONS
|
|
279
297
|
end
|
|
280
298
|
|
|
281
|
-
|
|
299
|
+
# Execute the function
|
|
300
|
+
def call
|
|
301
|
+
real_vals = @args.map { |i| i[:value] }
|
|
302
|
+
name = @name.to_sym
|
|
303
|
+
|
|
304
|
+
field = @args.find do |i|
|
|
305
|
+
i[:type] == :field || i.key?(:field)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
field = field[:type] == :function ? field[:field] : field[:value] unless field.nil?
|
|
309
|
+
|
|
310
|
+
required_args = support[name][:args]
|
|
311
|
+
total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:default] }
|
|
312
|
+
|
|
313
|
+
fill_in_optional_args = total_args.drop(real_vals.length)
|
|
282
314
|
|
|
283
|
-
|
|
284
|
-
|
|
315
|
+
fill_in_optional_args.each do |default|
|
|
316
|
+
real_vals << default
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
v = if field.nil?
|
|
320
|
+
method = name
|
|
321
|
+
if support[name][:resolve_for_type]
|
|
322
|
+
method_type = @args.first[:type]
|
|
323
|
+
method = "#{method}_#{method_type}"
|
|
324
|
+
end
|
|
325
|
+
send(method, *real_vals)
|
|
326
|
+
else
|
|
327
|
+
{
|
|
328
|
+
type: :function,
|
|
329
|
+
return_type: return_type,
|
|
330
|
+
value: name.to_s
|
|
331
|
+
}
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
return if v.nil?
|
|
335
|
+
|
|
336
|
+
unless v.key?(:function_name)
|
|
337
|
+
v.merge!(function_parameters: real_vals,
|
|
338
|
+
function_name: @name)
|
|
339
|
+
end
|
|
285
340
|
|
|
286
|
-
|
|
341
|
+
v.merge!(args: @args,
|
|
342
|
+
field: field)
|
|
287
343
|
|
|
288
|
-
|
|
289
|
-
real_vals << default
|
|
344
|
+
v
|
|
290
345
|
end
|
|
291
346
|
|
|
347
|
+
protected
|
|
348
|
+
|
|
349
|
+
# Supported function calls
|
|
350
|
+
|
|
351
|
+
def regex(regular_expression, flags)
|
|
352
|
+
unless (flags.chars.to_a - VALID_REGEX_FLAGS).empty?
|
|
353
|
+
@errors << Sparkql::ParserError.new(token: regular_expression,
|
|
354
|
+
message: 'Invalid Regexp',
|
|
355
|
+
status: :fatal)
|
|
356
|
+
return
|
|
357
|
+
end
|
|
292
358
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
359
|
+
begin
|
|
360
|
+
Regexp.new(regular_expression)
|
|
361
|
+
rescue StandardError
|
|
362
|
+
@errors << Sparkql::ParserError.new(token: regular_expression,
|
|
363
|
+
message: 'Invalid Regexp',
|
|
364
|
+
status: :fatal)
|
|
365
|
+
return
|
|
298
366
|
end
|
|
299
|
-
|
|
300
|
-
|
|
367
|
+
|
|
368
|
+
{
|
|
369
|
+
type: :character,
|
|
370
|
+
value: regular_expression
|
|
371
|
+
}
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def trim_character(arg)
|
|
301
375
|
{
|
|
302
|
-
:
|
|
303
|
-
:
|
|
304
|
-
:value => "#{name}",
|
|
376
|
+
type: :character,
|
|
377
|
+
value: arg.strip
|
|
305
378
|
}
|
|
306
379
|
end
|
|
307
380
|
|
|
308
|
-
|
|
381
|
+
def substring_character(character, first_index, number_chars)
|
|
382
|
+
second_index = if number_chars.nil?
|
|
383
|
+
-1
|
|
384
|
+
else
|
|
385
|
+
number_chars + first_index - 1
|
|
386
|
+
end
|
|
309
387
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
388
|
+
new_string = character[first_index..second_index].to_s
|
|
389
|
+
|
|
390
|
+
{
|
|
391
|
+
type: :character,
|
|
392
|
+
value: new_string
|
|
393
|
+
}
|
|
313
394
|
end
|
|
314
395
|
|
|
315
|
-
|
|
316
|
-
|
|
396
|
+
def substring_index_error?(second_index)
|
|
397
|
+
if second_index.to_i.negative?
|
|
398
|
+
@errors << Sparkql::ParserError.new(token: second_index,
|
|
399
|
+
message: "Function call 'substring' may not have a negative integer for its second parameter",
|
|
400
|
+
status: :fatal)
|
|
401
|
+
true
|
|
402
|
+
end
|
|
403
|
+
false
|
|
404
|
+
end
|
|
317
405
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
406
|
+
def tolower(_args)
|
|
407
|
+
{
|
|
408
|
+
type: :character,
|
|
409
|
+
value: 'tolower'
|
|
410
|
+
}
|
|
411
|
+
end
|
|
324
412
|
|
|
325
|
-
|
|
413
|
+
def tolower_character(string)
|
|
414
|
+
{
|
|
415
|
+
type: :character,
|
|
416
|
+
value: "'#{string.to_s.downcase}'"
|
|
417
|
+
}
|
|
418
|
+
end
|
|
326
419
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
:
|
|
330
|
-
:
|
|
331
|
-
|
|
420
|
+
def toupper_character(string)
|
|
421
|
+
{
|
|
422
|
+
type: :character,
|
|
423
|
+
value: "'#{string.to_s.upcase}'"
|
|
424
|
+
}
|
|
332
425
|
end
|
|
333
426
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
:status => :fatal)
|
|
340
|
-
return
|
|
427
|
+
def length_character(string)
|
|
428
|
+
{
|
|
429
|
+
type: :integer,
|
|
430
|
+
value: string.size.to_s
|
|
431
|
+
}
|
|
341
432
|
end
|
|
342
433
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
434
|
+
def startswith(string)
|
|
435
|
+
# Wrap this string in quotes, as we effectively translate
|
|
436
|
+
# City Eq startswith('far')
|
|
437
|
+
# ...to...
|
|
438
|
+
# City Eq '^far'
|
|
439
|
+
#
|
|
440
|
+
# The string passed in will merely be "far", rather than
|
|
441
|
+
# the string literal "'far'".
|
|
442
|
+
string = Regexp.escape(string)
|
|
443
|
+
new_value = "^#{string}"
|
|
348
444
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
445
|
+
{
|
|
446
|
+
function_name: 'regex',
|
|
447
|
+
function_parameters: [new_value, ''],
|
|
448
|
+
type: :character,
|
|
449
|
+
value: new_value
|
|
450
|
+
}
|
|
451
|
+
end
|
|
355
452
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
453
|
+
def endswith(string)
|
|
454
|
+
# Wrap this string in quotes, as we effectively translate
|
|
455
|
+
# City Eq endswith('far')
|
|
456
|
+
# ...to...
|
|
457
|
+
# City Eq regex('far$')
|
|
458
|
+
#
|
|
459
|
+
# The string passed in will merely be "far", rather than
|
|
460
|
+
# the string literal "'far'".
|
|
461
|
+
string = Regexp.escape(string)
|
|
462
|
+
new_value = "#{string}$"
|
|
463
|
+
|
|
464
|
+
{
|
|
465
|
+
function_name: 'regex',
|
|
466
|
+
function_parameters: [new_value, ''],
|
|
467
|
+
type: :character,
|
|
468
|
+
value: new_value
|
|
469
|
+
}
|
|
361
470
|
end
|
|
362
471
|
|
|
363
|
-
|
|
472
|
+
def contains(string)
|
|
473
|
+
# Wrap this string in quotes, as we effectively translate
|
|
474
|
+
# City Eq contains('far')
|
|
475
|
+
# ...to...
|
|
476
|
+
# City Eq regex('far')
|
|
477
|
+
#
|
|
478
|
+
# The string passed in will merely be "far", rather than
|
|
479
|
+
# the string literal "'far'".
|
|
480
|
+
string = Regexp.escape(string)
|
|
481
|
+
new_value = string.to_s
|
|
364
482
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
483
|
+
{
|
|
484
|
+
function_name: 'regex',
|
|
485
|
+
function_parameters: [new_value, ''],
|
|
486
|
+
type: :character,
|
|
487
|
+
value: new_value
|
|
488
|
+
}
|
|
489
|
+
end
|
|
370
490
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
:
|
|
376
|
-
|
|
491
|
+
# Offset the current timestamp by a number of seconds
|
|
492
|
+
def seconds(num)
|
|
493
|
+
t = current_time + num
|
|
494
|
+
{
|
|
495
|
+
type: :datetime,
|
|
496
|
+
value: t.iso8601
|
|
497
|
+
}
|
|
377
498
|
end
|
|
378
|
-
false
|
|
379
|
-
end
|
|
380
499
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
500
|
+
# Offset the current timestamp by a number of minutes
|
|
501
|
+
def minutes(num)
|
|
502
|
+
t = current_time + num * SECONDS_IN_MINUTE
|
|
503
|
+
{
|
|
504
|
+
type: :datetime,
|
|
505
|
+
value: t.iso8601
|
|
506
|
+
}
|
|
507
|
+
end
|
|
387
508
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
509
|
+
# Offset the current timestamp by a number of hours
|
|
510
|
+
def hours(num)
|
|
511
|
+
t = current_time + num * SECONDS_IN_HOUR
|
|
512
|
+
{
|
|
513
|
+
type: :datetime,
|
|
514
|
+
value: t.iso8601
|
|
515
|
+
}
|
|
516
|
+
end
|
|
394
517
|
|
|
518
|
+
# Offset the current timestamp by a number of days
|
|
519
|
+
def days(number_of_days)
|
|
520
|
+
# date calculated as the offset from midnight tommorrow. Zero will provide values for all times
|
|
521
|
+
# today.
|
|
522
|
+
d = current_date + number_of_days
|
|
523
|
+
{
|
|
524
|
+
type: :date,
|
|
525
|
+
value: d.strftime(STRFTIME_DATE_FORMAT)
|
|
526
|
+
}
|
|
527
|
+
end
|
|
395
528
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
529
|
+
def weekdays(number_of_days)
|
|
530
|
+
today = current_date
|
|
531
|
+
weekend_start = today.saturday? || today.sunday?
|
|
532
|
+
direction = number_of_days.positive? ? 1 : -1
|
|
533
|
+
weeks = (number_of_days / 5.0).to_i
|
|
534
|
+
remaining = number_of_days.abs % 5
|
|
535
|
+
|
|
536
|
+
# Jump ahead the number of weeks represented in the input
|
|
537
|
+
today += weeks * 7
|
|
538
|
+
|
|
539
|
+
# Now iterate on the remaining weekdays
|
|
540
|
+
remaining.times do |i|
|
|
541
|
+
today += direction
|
|
542
|
+
while today.saturday? || today.sunday?
|
|
543
|
+
today += direction
|
|
544
|
+
end
|
|
545
|
+
end
|
|
402
546
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
547
|
+
# If we end on the weekend, bump accordingly
|
|
548
|
+
while today.saturday? || today.sunday?
|
|
549
|
+
# If we start and end on the weekend, wind things back to the next
|
|
550
|
+
# appropriate weekday.
|
|
551
|
+
if weekend_start && remaining == 0
|
|
552
|
+
today -= direction
|
|
553
|
+
else
|
|
554
|
+
today += direction
|
|
555
|
+
end
|
|
556
|
+
end
|
|
409
557
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
#
|
|
416
|
-
# The string passed in will merely be "far", rather than
|
|
417
|
-
# the string literal "'far'".
|
|
418
|
-
string = Regexp.escape(string)
|
|
419
|
-
new_value = "^#{string}"
|
|
420
|
-
|
|
421
|
-
{
|
|
422
|
-
:function_name => "regex",
|
|
423
|
-
:function_parameters => [new_value, ''],
|
|
424
|
-
:type => :character,
|
|
425
|
-
:value => new_value
|
|
426
|
-
}
|
|
427
|
-
end
|
|
558
|
+
{
|
|
559
|
+
type: :date,
|
|
560
|
+
value: today.strftime(STRFTIME_DATE_FORMAT)
|
|
561
|
+
}
|
|
562
|
+
end
|
|
428
563
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
# the string literal "'far'".
|
|
437
|
-
string = Regexp.escape(string)
|
|
438
|
-
new_value = "#{string}$"
|
|
439
|
-
|
|
440
|
-
{
|
|
441
|
-
:function_name => "regex",
|
|
442
|
-
:function_parameters => [new_value, ''],
|
|
443
|
-
:type => :character,
|
|
444
|
-
:value => new_value
|
|
445
|
-
}
|
|
446
|
-
end
|
|
564
|
+
# The current timestamp
|
|
565
|
+
def now
|
|
566
|
+
{
|
|
567
|
+
type: :datetime,
|
|
568
|
+
value: current_time.iso8601
|
|
569
|
+
}
|
|
570
|
+
end
|
|
447
571
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
# The string passed in will merely be "far", rather than
|
|
455
|
-
# the string literal "'far'".
|
|
456
|
-
string = Regexp.escape(string)
|
|
457
|
-
new_value = "#{string}"
|
|
458
|
-
|
|
459
|
-
{
|
|
460
|
-
:function_name => "regex",
|
|
461
|
-
:function_parameters => [new_value, ''],
|
|
462
|
-
:type => :character,
|
|
463
|
-
:value => new_value
|
|
464
|
-
}
|
|
465
|
-
end
|
|
572
|
+
def maxdatetime
|
|
573
|
+
{
|
|
574
|
+
type: :datetime,
|
|
575
|
+
value: MAX_DATE_TIME
|
|
576
|
+
}
|
|
577
|
+
end
|
|
466
578
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
:type => :date,
|
|
474
|
-
:value => d.strftime(STRFTIME_DATE_FORMAT)
|
|
475
|
-
}
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
# The current timestamp
|
|
479
|
-
def now()
|
|
480
|
-
{
|
|
481
|
-
:type => :datetime,
|
|
482
|
-
:value => Time.now.iso8601
|
|
483
|
-
}
|
|
484
|
-
end
|
|
579
|
+
def mindatetime
|
|
580
|
+
{
|
|
581
|
+
type: :datetime,
|
|
582
|
+
value: MIN_DATE_TIME
|
|
583
|
+
}
|
|
584
|
+
end
|
|
485
585
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
586
|
+
def floor_decimal(arg)
|
|
587
|
+
{
|
|
588
|
+
type: :integer,
|
|
589
|
+
value: arg.floor.to_s
|
|
590
|
+
}
|
|
591
|
+
end
|
|
492
592
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
593
|
+
def ceiling_decimal(arg)
|
|
594
|
+
{
|
|
595
|
+
type: :integer,
|
|
596
|
+
value: arg.ceil.to_s
|
|
597
|
+
}
|
|
598
|
+
end
|
|
499
599
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
600
|
+
def round_decimal(arg)
|
|
601
|
+
{
|
|
602
|
+
type: :integer,
|
|
603
|
+
value: arg.round.to_s
|
|
604
|
+
}
|
|
605
|
+
end
|
|
506
606
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
607
|
+
def indexof(arg1, arg2)
|
|
608
|
+
{
|
|
609
|
+
value: 'indexof',
|
|
610
|
+
args: [arg1, arg2]
|
|
611
|
+
}
|
|
612
|
+
end
|
|
513
613
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
614
|
+
def concat_character(arg1, arg2)
|
|
615
|
+
{
|
|
616
|
+
type: :character,
|
|
617
|
+
value: "'#{arg1}#{arg2}'"
|
|
618
|
+
}
|
|
619
|
+
end
|
|
520
620
|
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
621
|
+
def date_datetime(datetime)
|
|
622
|
+
{
|
|
623
|
+
type: :date,
|
|
624
|
+
value: datetime.strftime(STRFTIME_DATE_FORMAT)
|
|
625
|
+
}
|
|
626
|
+
end
|
|
527
627
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
628
|
+
def time_datetime(datetime)
|
|
629
|
+
{
|
|
630
|
+
type: :time,
|
|
631
|
+
value: datetime.strftime(STRFTIME_TIME_FORMAT)
|
|
632
|
+
}
|
|
633
|
+
end
|
|
534
634
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
def time_datetime(dt)
|
|
543
|
-
{
|
|
544
|
-
:type => :time,
|
|
545
|
-
:value => dt.strftime(STRFTIME_TIME_FORMAT)
|
|
546
|
-
}
|
|
547
|
-
end
|
|
635
|
+
def months(num_months)
|
|
636
|
+
d = current_timestamp >> num_months
|
|
637
|
+
{
|
|
638
|
+
type: :date,
|
|
639
|
+
value: d.strftime(STRFTIME_DATE_FORMAT)
|
|
640
|
+
}
|
|
641
|
+
end
|
|
548
642
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
643
|
+
def years(num_years)
|
|
644
|
+
d = current_timestamp >> (num_years * 12)
|
|
645
|
+
{
|
|
646
|
+
type: :date,
|
|
647
|
+
value: d.strftime(STRFTIME_DATE_FORMAT)
|
|
648
|
+
}
|
|
649
|
+
end
|
|
556
650
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
@errors << Sparkql::ParserError.new(:token => coords,
|
|
569
|
-
:message => "Function call 'polygon' requires at least three coordinates",
|
|
570
|
-
:status => :fatal )
|
|
571
|
-
return
|
|
572
|
-
end
|
|
573
|
-
|
|
574
|
-
# auto close the polygon if it's open
|
|
575
|
-
unless new_coords.first == new_coords.last
|
|
576
|
-
new_coords << new_coords.first.clone
|
|
577
|
-
end
|
|
578
|
-
|
|
579
|
-
shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([new_coords])
|
|
580
|
-
{
|
|
581
|
-
:type => :shape,
|
|
582
|
-
:value => shape
|
|
583
|
-
}
|
|
584
|
-
end
|
|
651
|
+
def polygon(coords)
|
|
652
|
+
new_coords = parse_coordinates(coords)
|
|
653
|
+
unless new_coords.size > 2
|
|
654
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
|
655
|
+
message: "Function call 'polygon' requires at least three coordinates",
|
|
656
|
+
status: :fatal)
|
|
657
|
+
return
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# auto close the polygon if it's open
|
|
661
|
+
new_coords << new_coords.first.clone unless new_coords.first == new_coords.last
|
|
585
662
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
:status => :fatal )
|
|
592
|
-
return
|
|
663
|
+
shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([new_coords])
|
|
664
|
+
{
|
|
665
|
+
type: :shape,
|
|
666
|
+
value: shape
|
|
667
|
+
}
|
|
593
668
|
end
|
|
594
669
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
670
|
+
def linestring(coords)
|
|
671
|
+
new_coords = parse_coordinates(coords)
|
|
672
|
+
unless new_coords.size > 1
|
|
673
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
|
674
|
+
message: "Function call 'linestring' requires at least two coordinates",
|
|
675
|
+
status: :fatal)
|
|
676
|
+
return
|
|
677
|
+
end
|
|
601
678
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
rescue GeoRuby::SimpleFeatures::EWKTFormatError
|
|
609
|
-
@errors << Sparkql::ParserError.new(:token => wkt_string,
|
|
610
|
-
:message => "Function call 'wkt' requires a valid wkt string",
|
|
611
|
-
:status => :fatal )
|
|
612
|
-
return
|
|
613
|
-
end
|
|
614
|
-
|
|
615
|
-
def rectangle(coords)
|
|
616
|
-
bounding_box = parse_coordinates(coords)
|
|
617
|
-
unless bounding_box.size == 2
|
|
618
|
-
@errors << Sparkql::ParserError.new(:token => coords,
|
|
619
|
-
:message => "Function call 'rectangle' requires two coordinates for the bounding box",
|
|
620
|
-
:status => :fatal )
|
|
621
|
-
return
|
|
622
|
-
end
|
|
623
|
-
poly_coords = [
|
|
624
|
-
bounding_box.first,
|
|
625
|
-
[bounding_box.last.first, bounding_box.first.last],
|
|
626
|
-
bounding_box.last,
|
|
627
|
-
[bounding_box.first.first, bounding_box.last.last],
|
|
628
|
-
bounding_box.first.clone,
|
|
629
|
-
]
|
|
630
|
-
shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([poly_coords])
|
|
631
|
-
{
|
|
632
|
-
:type => :shape,
|
|
633
|
-
:value => shape
|
|
634
|
-
}
|
|
635
|
-
end
|
|
636
|
-
|
|
637
|
-
def radius(coords, length)
|
|
638
|
-
|
|
639
|
-
unless length > 0
|
|
640
|
-
@errors << Sparkql::ParserError.new(:token => length,
|
|
641
|
-
:message => "Function call 'radius' length must be positive",
|
|
642
|
-
:status => :fatal )
|
|
643
|
-
return
|
|
644
|
-
end
|
|
645
|
-
|
|
646
|
-
# The radius() function is overloaded to allow an identifier
|
|
647
|
-
# to be specified over lat/lon. This identifier should specify a
|
|
648
|
-
# record that, in turn, references a lat/lon. Naturally, this won't be
|
|
649
|
-
# validated here.
|
|
650
|
-
shape_error = false
|
|
651
|
-
shape = if is_coords?(coords)
|
|
652
|
-
new_coords = parse_coordinates(coords)
|
|
653
|
-
if new_coords.size != 1
|
|
654
|
-
shape_error = true
|
|
655
|
-
else
|
|
656
|
-
GeoRuby::SimpleFeatures::Circle.from_coordinates(new_coords.first, length);
|
|
657
|
-
end
|
|
658
|
-
elsif Sparkql::Geo::RecordRadius.valid_record_id?(coords)
|
|
659
|
-
Sparkql::Geo::RecordRadius.new(coords, length)
|
|
660
|
-
else
|
|
661
|
-
shape_error = true
|
|
662
|
-
end
|
|
679
|
+
shape = GeoRuby::SimpleFeatures::LineString.from_coordinates(new_coords)
|
|
680
|
+
{
|
|
681
|
+
type: :shape,
|
|
682
|
+
value: shape
|
|
683
|
+
}
|
|
684
|
+
end
|
|
663
685
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
:
|
|
668
|
-
|
|
686
|
+
def wkt(wkt_string)
|
|
687
|
+
shape = GeoRuby::SimpleFeatures::Geometry.from_ewkt(wkt_string)
|
|
688
|
+
{
|
|
689
|
+
type: :shape,
|
|
690
|
+
value: shape
|
|
691
|
+
}
|
|
692
|
+
rescue GeoRuby::SimpleFeatures::EWKTFormatError
|
|
693
|
+
@errors << Sparkql::ParserError.new(token: wkt_string,
|
|
694
|
+
message: "Function call 'wkt' requires a valid wkt string",
|
|
695
|
+
status: :fatal)
|
|
696
|
+
nil
|
|
669
697
|
end
|
|
670
698
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
699
|
+
def rectangle(coords)
|
|
700
|
+
bounding_box = parse_coordinates(coords)
|
|
701
|
+
unless bounding_box.size == 2
|
|
702
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
|
703
|
+
message: "Function call 'rectangle' requires two coordinates for the bounding box",
|
|
704
|
+
status: :fatal)
|
|
705
|
+
return
|
|
706
|
+
end
|
|
707
|
+
poly_coords = [
|
|
708
|
+
bounding_box.first,
|
|
709
|
+
[bounding_box.last.first, bounding_box.first.last],
|
|
710
|
+
bounding_box.last,
|
|
711
|
+
[bounding_box.first.first, bounding_box.last.last],
|
|
712
|
+
bounding_box.first.clone
|
|
713
|
+
]
|
|
714
|
+
shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([poly_coords])
|
|
715
|
+
{
|
|
716
|
+
type: :shape,
|
|
717
|
+
value: shape
|
|
718
|
+
}
|
|
719
|
+
end
|
|
683
720
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
type: new_type,
|
|
692
|
-
value: cast_literal(value, new_type)
|
|
693
|
-
}
|
|
694
|
-
rescue
|
|
695
|
-
{
|
|
696
|
-
type: :null,
|
|
697
|
-
value: 'NULL'
|
|
698
|
-
}
|
|
699
|
-
end
|
|
721
|
+
def radius(coords, length)
|
|
722
|
+
unless length.positive?
|
|
723
|
+
@errors << Sparkql::ParserError.new(token: length,
|
|
724
|
+
message: "Function call 'radius' length must be positive",
|
|
725
|
+
status: :fatal)
|
|
726
|
+
return
|
|
727
|
+
end
|
|
700
728
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
729
|
+
# The radius() function is overloaded to allow an identifier
|
|
730
|
+
# to be specified over lat/lon. This identifier should specify a
|
|
731
|
+
# record that, in turn, references a lat/lon. Naturally, this won't be
|
|
732
|
+
# validated here.
|
|
733
|
+
shape_error = false
|
|
734
|
+
shape = if coords?(coords)
|
|
735
|
+
new_coords = parse_coordinates(coords)
|
|
736
|
+
if new_coords.size != 1
|
|
737
|
+
shape_error = true
|
|
738
|
+
else
|
|
739
|
+
GeoRuby::SimpleFeatures::Circle.from_coordinates(new_coords.first, length)
|
|
740
|
+
end
|
|
741
|
+
elsif Sparkql::Geo::RecordRadius.valid_record_id?(coords)
|
|
742
|
+
Sparkql::Geo::RecordRadius.new(coords, length)
|
|
743
|
+
else
|
|
744
|
+
shape_error = true
|
|
745
|
+
end
|
|
746
|
+
|
|
747
|
+
if shape_error
|
|
748
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
|
749
|
+
message: "Function call 'radius' requires one coordinate for the center",
|
|
750
|
+
status: :fatal)
|
|
751
|
+
return
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
{
|
|
755
|
+
type: :shape,
|
|
756
|
+
value: shape
|
|
757
|
+
}
|
|
709
758
|
end
|
|
710
|
-
end
|
|
711
759
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
760
|
+
def range(start_str, end_str)
|
|
761
|
+
{
|
|
762
|
+
type: :character,
|
|
763
|
+
value: [start_str.to_s, end_str.to_s]
|
|
764
|
+
}
|
|
765
|
+
end
|
|
715
766
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
end
|
|
767
|
+
def cast(value, type)
|
|
768
|
+
value = nil if value == 'NULL'
|
|
719
769
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
770
|
+
new_type = type.to_sym
|
|
771
|
+
{
|
|
772
|
+
type: new_type,
|
|
773
|
+
value: cast_literal(value, new_type)
|
|
774
|
+
}
|
|
775
|
+
rescue StandardError
|
|
776
|
+
{
|
|
777
|
+
type: :null,
|
|
778
|
+
value: 'NULL'
|
|
779
|
+
}
|
|
780
|
+
end
|
|
723
781
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
"'#{value.to_s}'"
|
|
728
|
-
when :integer
|
|
729
|
-
if value.nil?
|
|
730
|
-
'0'
|
|
782
|
+
def valid_cast_type?(type)
|
|
783
|
+
if VALID_CAST_TYPES.key?(type.to_sym)
|
|
784
|
+
true
|
|
731
785
|
else
|
|
732
|
-
|
|
786
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
|
787
|
+
message: "Function call 'cast' requires a castable type.",
|
|
788
|
+
status: :fatal)
|
|
789
|
+
false
|
|
733
790
|
end
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def cast_null(value, type)
|
|
794
|
+
cast(value, type)
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
def cast_decimal(value, type)
|
|
798
|
+
cast(value, type)
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
def cast_character(value, type)
|
|
802
|
+
cast(value, type)
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
def cast_literal(value, type)
|
|
806
|
+
case type
|
|
807
|
+
when :character
|
|
808
|
+
"'#{value}'"
|
|
809
|
+
when :integer
|
|
810
|
+
if value.nil?
|
|
811
|
+
'0'
|
|
812
|
+
else
|
|
813
|
+
Integer(Float(value)).to_s
|
|
814
|
+
end
|
|
815
|
+
when :decimal
|
|
816
|
+
if value.nil?
|
|
817
|
+
'0.0'
|
|
818
|
+
else
|
|
819
|
+
Float(value).to_s
|
|
820
|
+
end
|
|
821
|
+
when :null
|
|
822
|
+
'NULL'
|
|
739
823
|
end
|
|
740
|
-
when :null
|
|
741
|
-
'NULL'
|
|
742
824
|
end
|
|
743
|
-
end
|
|
744
825
|
|
|
745
|
-
|
|
826
|
+
def current_date
|
|
827
|
+
current_timestamp.to_date
|
|
828
|
+
end
|
|
746
829
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
830
|
+
def current_time
|
|
831
|
+
current_timestamp.to_time
|
|
832
|
+
end
|
|
750
833
|
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
834
|
+
def current_timestamp
|
|
835
|
+
@current_timestamp ||= DateTime.now
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
private
|
|
839
|
+
|
|
840
|
+
def coords?(coord_string)
|
|
841
|
+
coord_string.split(' ').size > 1
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
def parse_coordinates(coord_string)
|
|
845
|
+
terms = coord_string.strip.split(',')
|
|
846
|
+
terms.map do |term|
|
|
847
|
+
term.strip.split(/\s+/).reverse.map(&:to_f)
|
|
848
|
+
end
|
|
849
|
+
rescue StandardError
|
|
850
|
+
@errors << Sparkql::ParserError.new(token: coord_string,
|
|
851
|
+
message: 'Unable to parse coordinate string.',
|
|
852
|
+
status: :fatal)
|
|
755
853
|
end
|
|
756
|
-
coords
|
|
757
|
-
rescue
|
|
758
|
-
@errors << Sparkql::ParserError.new(:token => coord_string,
|
|
759
|
-
:message => "Unable to parse coordinate string.",
|
|
760
|
-
:status => :fatal )
|
|
761
854
|
end
|
|
762
|
-
|
|
763
855
|
end
|