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