sparkql 1.2.5 → 1.3.0
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/.rubocop.yml +111 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +16 -0
- data/Gemfile +1 -2
- data/Rakefile +2 -3
- data/VERSION +1 -1
- data/lib/sparkql/errors.rb +68 -71
- data/lib/sparkql/evaluator.rb +13 -9
- data/lib/sparkql/expression_resolver.rb +2 -3
- data/lib/sparkql/expression_state.rb +7 -9
- data/lib/sparkql/function_resolver.rb +777 -676
- data/lib/sparkql/geo/record_circle.rb +1 -1
- data/lib/sparkql/lexer.rb +54 -56
- data/lib/sparkql/parser.rb +35 -35
- data/lib/sparkql/parser_compatibility.rb +98 -77
- data/lib/sparkql/parser_tools.rb +159 -139
- data/lib/sparkql/token.rb +25 -25
- data/lib/sparkql/version.rb +1 -1
- data/sparkql.gemspec +20 -18
- data/test/unit/errors_test.rb +4 -5
- data/test/unit/evaluator_test.rb +15 -16
- data/test/unit/expression_state_test.rb +14 -15
- data/test/unit/function_resolver_test.rb +445 -203
- data/test/unit/geo/record_circle_test.rb +2 -2
- data/test/unit/lexer_test.rb +15 -16
- data/test/unit/parser_compatability_test.rb +177 -151
- data/test/unit/parser_test.rb +133 -99
- metadata +36 -35
@@ -1,763 +1,864 @@
|
|
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
|
+
def self.lookup(function_name)
|
229
|
+
SUPPORTED_FUNCTIONS[function_name.to_sym]
|
230
|
+
end
|
231
|
+
|
232
|
+
# Construct a resolver instance for a function
|
233
|
+
# name: function name (String)
|
234
|
+
# args: array of literal hashes of the format {:type=><literal_type>, :value=><escaped_literal_value>}.
|
235
|
+
# Empty arry for functions that have no arguments.
|
236
|
+
def initialize(name, args, options = {})
|
237
|
+
@name = name
|
238
|
+
@args = args
|
239
|
+
@errors = []
|
240
|
+
@current_timestamp = options[:current_timestamp]
|
233
241
|
end
|
234
242
|
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
243
|
+
# Validate the function instance prior to calling it. All validation failures will show up in the
|
244
|
+
# errors array.
|
245
|
+
def validate
|
246
|
+
name = @name.to_sym
|
247
|
+
unless support.key?(name)
|
248
|
+
@errors << Sparkql::ParserError.new(token: @name,
|
249
|
+
message: "Unsupported function call '#{@name}' for expression",
|
250
|
+
status: :fatal)
|
241
251
|
return
|
242
252
|
end
|
253
|
+
|
254
|
+
required_args = support[name][:args]
|
255
|
+
total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:type] }
|
256
|
+
|
257
|
+
if @args.size < required_args.size || @args.size > total_args.size
|
258
|
+
@errors << Sparkql::ParserError.new(token: @name,
|
259
|
+
message: "Function call '#{@name}' requires #{required_args.size} arguments",
|
260
|
+
status: :fatal)
|
261
|
+
return
|
262
|
+
end
|
263
|
+
|
264
|
+
count = 0
|
265
|
+
@args.each do |arg|
|
266
|
+
type = arg[:type] == :function ? arg[:return_type] : arg[:type]
|
267
|
+
unless Array(total_args[count]).include?(type)
|
268
|
+
@errors << Sparkql::ParserError.new(token: @name,
|
269
|
+
message: "Function call '#{@name}' has an invalid argument at #{arg[:value]}",
|
270
|
+
status: :fatal)
|
271
|
+
end
|
272
|
+
count += 1
|
273
|
+
end
|
274
|
+
|
275
|
+
if name == :cast
|
276
|
+
type = @args.last[:value]
|
277
|
+
unless VALID_CAST_TYPES.include?(type.to_sym)
|
278
|
+
@errors << Sparkql::ParserError.new(token: @name,
|
279
|
+
message: "Function call '#{@name}' requires a castable type.",
|
280
|
+
status: :fatal)
|
281
|
+
return
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
substring_index_error?(@args[2][:value]) if name == :substring && !@args[2].nil?
|
243
286
|
end
|
244
287
|
|
245
|
-
|
246
|
-
|
288
|
+
def return_type
|
289
|
+
name = @name.to_sym
|
290
|
+
|
291
|
+
if name == :cast
|
292
|
+
@args.last[:value].to_sym
|
293
|
+
else
|
294
|
+
support[@name.to_sym][:return_type]
|
295
|
+
end
|
247
296
|
end
|
248
|
-
end
|
249
|
-
|
250
|
-
def return_type
|
251
|
-
name = @name.to_sym
|
252
297
|
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
298
|
+
attr_reader :errors
|
299
|
+
|
300
|
+
def errors?
|
301
|
+
@errors.size.positive?
|
257
302
|
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
303
|
|
277
|
-
|
278
|
-
|
304
|
+
def support
|
305
|
+
SUPPORTED_FUNCTIONS
|
279
306
|
end
|
280
307
|
|
281
|
-
|
308
|
+
# Execute the function
|
309
|
+
def call
|
310
|
+
real_vals = @args.map { |i| i[:value] }
|
311
|
+
name = @name.to_sym
|
312
|
+
|
313
|
+
field = @args.find do |i|
|
314
|
+
i[:type] == :field || i.key?(:field)
|
315
|
+
end
|
316
|
+
|
317
|
+
field = field[:type] == :function ? field[:field] : field[:value] unless field.nil?
|
282
318
|
|
283
|
-
|
284
|
-
|
319
|
+
required_args = support[name][:args]
|
320
|
+
total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:default] }
|
285
321
|
|
286
|
-
|
322
|
+
fill_in_optional_args = total_args.drop(real_vals.length)
|
287
323
|
|
288
|
-
|
289
|
-
|
324
|
+
fill_in_optional_args.each do |default|
|
325
|
+
real_vals << default
|
326
|
+
end
|
327
|
+
|
328
|
+
v = if field.nil?
|
329
|
+
method = name
|
330
|
+
if support[name][:resolve_for_type]
|
331
|
+
method_type = @args.first[:type]
|
332
|
+
method = "#{method}_#{method_type}"
|
333
|
+
end
|
334
|
+
send(method, *real_vals)
|
335
|
+
else
|
336
|
+
{
|
337
|
+
type: :function,
|
338
|
+
return_type: return_type,
|
339
|
+
value: name.to_s
|
340
|
+
}
|
341
|
+
end
|
342
|
+
|
343
|
+
return if v.nil?
|
344
|
+
|
345
|
+
unless v.key?(:function_name)
|
346
|
+
v.merge!(function_parameters: real_vals,
|
347
|
+
function_name: @name)
|
348
|
+
end
|
349
|
+
|
350
|
+
v.merge!(args: @args,
|
351
|
+
field: field)
|
352
|
+
|
353
|
+
v
|
290
354
|
end
|
291
355
|
|
356
|
+
protected
|
357
|
+
|
358
|
+
# Supported function calls
|
292
359
|
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
360
|
+
def regex(regular_expression, flags)
|
361
|
+
unless (flags.chars.to_a - VALID_REGEX_FLAGS).empty?
|
362
|
+
@errors << Sparkql::ParserError.new(token: regular_expression,
|
363
|
+
message: 'Invalid Regexp',
|
364
|
+
status: :fatal)
|
365
|
+
return
|
366
|
+
end
|
367
|
+
|
368
|
+
begin
|
369
|
+
Regexp.new(regular_expression)
|
370
|
+
rescue StandardError
|
371
|
+
@errors << Sparkql::ParserError.new(token: regular_expression,
|
372
|
+
message: 'Invalid Regexp',
|
373
|
+
status: :fatal)
|
374
|
+
return
|
298
375
|
end
|
299
|
-
|
300
|
-
else
|
376
|
+
|
301
377
|
{
|
302
|
-
:
|
303
|
-
:
|
304
|
-
:value => "#{name}",
|
378
|
+
type: :character,
|
379
|
+
value: regular_expression
|
305
380
|
}
|
306
381
|
end
|
307
382
|
|
308
|
-
|
383
|
+
def trim_character(arg)
|
384
|
+
{
|
385
|
+
type: :character,
|
386
|
+
value: arg.strip
|
387
|
+
}
|
388
|
+
end
|
389
|
+
|
390
|
+
def substring_character(character, first_index, number_chars)
|
391
|
+
second_index = if number_chars.nil?
|
392
|
+
-1
|
393
|
+
else
|
394
|
+
number_chars + first_index - 1
|
395
|
+
end
|
309
396
|
|
310
|
-
|
311
|
-
|
312
|
-
|
397
|
+
new_string = character[first_index..second_index].to_s
|
398
|
+
|
399
|
+
{
|
400
|
+
type: :character,
|
401
|
+
value: new_string
|
402
|
+
}
|
313
403
|
end
|
314
404
|
|
315
|
-
|
316
|
-
|
405
|
+
def substring_index_error?(second_index)
|
406
|
+
if second_index.to_i.negative?
|
407
|
+
@errors << Sparkql::ParserError.new(token: second_index,
|
408
|
+
message: "Function call 'substring' may not have a negative integer for its second parameter",
|
409
|
+
status: :fatal)
|
410
|
+
true
|
411
|
+
end
|
412
|
+
false
|
413
|
+
end
|
317
414
|
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
415
|
+
def tolower(_args)
|
416
|
+
{
|
417
|
+
type: :character,
|
418
|
+
value: 'tolower'
|
419
|
+
}
|
420
|
+
end
|
324
421
|
|
325
|
-
|
422
|
+
def tolower_character(string)
|
423
|
+
{
|
424
|
+
type: :character,
|
425
|
+
value: "'#{string.to_s.downcase}'"
|
426
|
+
}
|
427
|
+
end
|
326
428
|
|
327
|
-
|
328
|
-
|
329
|
-
:
|
330
|
-
:
|
331
|
-
|
429
|
+
def toupper_character(string)
|
430
|
+
{
|
431
|
+
type: :character,
|
432
|
+
value: "'#{string.to_s.upcase}'"
|
433
|
+
}
|
332
434
|
end
|
333
435
|
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
:status => :fatal)
|
340
|
-
return
|
436
|
+
def length_character(string)
|
437
|
+
{
|
438
|
+
type: :integer,
|
439
|
+
value: string.size.to_s
|
440
|
+
}
|
341
441
|
end
|
342
442
|
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
443
|
+
def startswith(string)
|
444
|
+
# Wrap this string in quotes, as we effectively translate
|
445
|
+
# City Eq startswith('far')
|
446
|
+
# ...to...
|
447
|
+
# City Eq '^far'
|
448
|
+
#
|
449
|
+
# The string passed in will merely be "far", rather than
|
450
|
+
# the string literal "'far'".
|
451
|
+
string = Regexp.escape(string)
|
452
|
+
new_value = "^#{string}"
|
348
453
|
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
454
|
+
{
|
455
|
+
function_name: 'regex',
|
456
|
+
function_parameters: [new_value, ''],
|
457
|
+
type: :character,
|
458
|
+
value: new_value
|
459
|
+
}
|
460
|
+
end
|
355
461
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
462
|
+
def endswith(string)
|
463
|
+
# Wrap this string in quotes, as we effectively translate
|
464
|
+
# City Eq endswith('far')
|
465
|
+
# ...to...
|
466
|
+
# City Eq regex('far$')
|
467
|
+
#
|
468
|
+
# The string passed in will merely be "far", rather than
|
469
|
+
# the string literal "'far'".
|
470
|
+
string = Regexp.escape(string)
|
471
|
+
new_value = "#{string}$"
|
472
|
+
|
473
|
+
{
|
474
|
+
function_name: 'regex',
|
475
|
+
function_parameters: [new_value, ''],
|
476
|
+
type: :character,
|
477
|
+
value: new_value
|
478
|
+
}
|
361
479
|
end
|
362
480
|
|
363
|
-
|
481
|
+
def contains(string)
|
482
|
+
# Wrap this string in quotes, as we effectively translate
|
483
|
+
# City Eq contains('far')
|
484
|
+
# ...to...
|
485
|
+
# City Eq regex('far')
|
486
|
+
#
|
487
|
+
# The string passed in will merely be "far", rather than
|
488
|
+
# the string literal "'far'".
|
489
|
+
string = Regexp.escape(string)
|
490
|
+
new_value = string.to_s
|
364
491
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
492
|
+
{
|
493
|
+
function_name: 'regex',
|
494
|
+
function_parameters: [new_value, ''],
|
495
|
+
type: :character,
|
496
|
+
value: new_value
|
497
|
+
}
|
498
|
+
end
|
370
499
|
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
:
|
376
|
-
|
500
|
+
# Offset the current timestamp by a number of seconds
|
501
|
+
def seconds(num)
|
502
|
+
t = current_time + num
|
503
|
+
{
|
504
|
+
type: :datetime,
|
505
|
+
value: t.iso8601
|
506
|
+
}
|
377
507
|
end
|
378
|
-
false
|
379
|
-
end
|
380
508
|
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
509
|
+
# Offset the current timestamp by a number of minutes
|
510
|
+
def minutes(num)
|
511
|
+
t = current_time + num * SECONDS_IN_MINUTE
|
512
|
+
{
|
513
|
+
type: :datetime,
|
514
|
+
value: t.iso8601
|
515
|
+
}
|
516
|
+
end
|
387
517
|
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
518
|
+
# Offset the current timestamp by a number of hours
|
519
|
+
def hours(num)
|
520
|
+
t = current_time + num * SECONDS_IN_HOUR
|
521
|
+
{
|
522
|
+
type: :datetime,
|
523
|
+
value: t.iso8601
|
524
|
+
}
|
525
|
+
end
|
394
526
|
|
527
|
+
# Offset the current timestamp by a number of days
|
528
|
+
def days(number_of_days)
|
529
|
+
# date calculated as the offset from midnight tommorrow. Zero will provide values for all times
|
530
|
+
# today.
|
531
|
+
d = current_date + number_of_days
|
532
|
+
{
|
533
|
+
type: :date,
|
534
|
+
value: d.strftime(STRFTIME_DATE_FORMAT)
|
535
|
+
}
|
536
|
+
end
|
395
537
|
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
538
|
+
def weekdays(number_of_days)
|
539
|
+
today = current_date
|
540
|
+
weekend_start = today.saturday? || today.sunday?
|
541
|
+
direction = number_of_days.positive? ? 1 : -1
|
542
|
+
weeks = (number_of_days / 5.0).to_i
|
543
|
+
remaining = number_of_days.abs % 5
|
402
544
|
|
403
|
-
|
404
|
-
|
405
|
-
:type => :integer,
|
406
|
-
:value => "#{string.size}"
|
407
|
-
}
|
408
|
-
end
|
545
|
+
# Jump ahead the number of weeks represented in the input
|
546
|
+
today += weeks * 7
|
409
547
|
|
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
|
548
|
+
# Now iterate on the remaining weekdays
|
549
|
+
remaining.times do |_i|
|
550
|
+
today += direction
|
551
|
+
today += direction while today.saturday? || today.sunday?
|
552
|
+
end
|
428
553
|
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
{
|
441
|
-
:function_name => "regex",
|
442
|
-
:function_parameters => [new_value, ''],
|
443
|
-
:type => :character,
|
444
|
-
:value => new_value
|
445
|
-
}
|
446
|
-
end
|
554
|
+
# If we end on the weekend, bump accordingly
|
555
|
+
while today.saturday? || today.sunday?
|
556
|
+
# If we start and end on the weekend, wind things back to the next
|
557
|
+
# appropriate weekday.
|
558
|
+
if weekend_start && remaining.zero?
|
559
|
+
today -= direction
|
560
|
+
else
|
561
|
+
today += direction
|
562
|
+
end
|
563
|
+
end
|
447
564
|
|
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
|
565
|
+
{
|
566
|
+
type: :date,
|
567
|
+
value: today.strftime(STRFTIME_DATE_FORMAT)
|
568
|
+
}
|
569
|
+
end
|
466
570
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
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
|
571
|
+
# The current timestamp
|
572
|
+
def now
|
573
|
+
{
|
574
|
+
type: :datetime,
|
575
|
+
value: current_time.iso8601
|
576
|
+
}
|
577
|
+
end
|
485
578
|
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
579
|
+
def maxdatetime
|
580
|
+
{
|
581
|
+
type: :datetime,
|
582
|
+
value: MAX_DATE_TIME
|
583
|
+
}
|
584
|
+
end
|
492
585
|
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
586
|
+
def mindatetime
|
587
|
+
{
|
588
|
+
type: :datetime,
|
589
|
+
value: MIN_DATE_TIME
|
590
|
+
}
|
591
|
+
end
|
499
592
|
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
593
|
+
def floor_decimal(arg)
|
594
|
+
{
|
595
|
+
type: :integer,
|
596
|
+
value: arg.floor.to_s
|
597
|
+
}
|
598
|
+
end
|
506
599
|
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
600
|
+
def ceiling_decimal(arg)
|
601
|
+
{
|
602
|
+
type: :integer,
|
603
|
+
value: arg.ceil.to_s
|
604
|
+
}
|
605
|
+
end
|
513
606
|
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
607
|
+
def round_decimal(arg)
|
608
|
+
{
|
609
|
+
type: :integer,
|
610
|
+
value: arg.round.to_s
|
611
|
+
}
|
612
|
+
end
|
520
613
|
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
|
614
|
+
def indexof(arg1, arg2)
|
615
|
+
{
|
616
|
+
value: 'indexof',
|
617
|
+
args: [arg1, arg2]
|
618
|
+
}
|
619
|
+
end
|
527
620
|
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
621
|
+
def concat_character(arg1, arg2)
|
622
|
+
{
|
623
|
+
type: :character,
|
624
|
+
value: "'#{arg1}#{arg2}'"
|
625
|
+
}
|
626
|
+
end
|
534
627
|
|
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
|
628
|
+
def date_datetime(datetime)
|
629
|
+
{
|
630
|
+
type: :date,
|
631
|
+
value: datetime.strftime(STRFTIME_DATE_FORMAT)
|
632
|
+
}
|
633
|
+
end
|
548
634
|
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
554
|
-
|
555
|
-
end
|
635
|
+
def time_datetime(datetime)
|
636
|
+
{
|
637
|
+
type: :time,
|
638
|
+
value: datetime.strftime(STRFTIME_TIME_FORMAT)
|
639
|
+
}
|
640
|
+
end
|
556
641
|
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
def polygon(coords)
|
566
|
-
new_coords = parse_coordinates(coords)
|
567
|
-
unless new_coords.size > 2
|
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
|
642
|
+
def months(num_months)
|
643
|
+
# DateTime usage. There's a better means to do this with Time via rails
|
644
|
+
d = (current_timestamp.to_datetime >> num_months).to_time
|
645
|
+
{
|
646
|
+
type: :date,
|
647
|
+
value: d.strftime(STRFTIME_DATE_FORMAT)
|
648
|
+
}
|
649
|
+
end
|
585
650
|
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
:
|
591
|
-
:
|
592
|
-
|
651
|
+
def years(num_years)
|
652
|
+
# DateTime usage. There's a better means to do this with Time via rails
|
653
|
+
d = (current_timestamp.to_datetime >> (num_years * 12)).to_time
|
654
|
+
{
|
655
|
+
type: :date,
|
656
|
+
value: d.strftime(STRFTIME_DATE_FORMAT)
|
657
|
+
}
|
593
658
|
end
|
594
659
|
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
660
|
+
def polygon(coords)
|
661
|
+
new_coords = parse_coordinates(coords)
|
662
|
+
unless new_coords.size > 2
|
663
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
664
|
+
message: "Function call 'polygon' requires at least three coordinates",
|
665
|
+
status: :fatal)
|
666
|
+
return
|
667
|
+
end
|
601
668
|
|
602
|
-
|
603
|
-
|
604
|
-
{
|
605
|
-
:type => :shape,
|
606
|
-
:value => shape
|
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
|
669
|
+
# auto close the polygon if it's open
|
670
|
+
new_coords << new_coords.first.clone unless new_coords.first == new_coords.last
|
663
671
|
|
664
|
-
|
665
|
-
|
666
|
-
:
|
667
|
-
:
|
668
|
-
|
672
|
+
shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([new_coords])
|
673
|
+
{
|
674
|
+
type: :shape,
|
675
|
+
value: shape
|
676
|
+
}
|
669
677
|
end
|
670
678
|
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
:type => :character,
|
680
|
-
:value => [start_str.to_s, end_str.to_s]
|
681
|
-
}
|
682
|
-
end
|
679
|
+
def linestring(coords)
|
680
|
+
new_coords = parse_coordinates(coords)
|
681
|
+
unless new_coords.size > 1
|
682
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
683
|
+
message: "Function call 'linestring' requires at least two coordinates",
|
684
|
+
status: :fatal)
|
685
|
+
return
|
686
|
+
end
|
683
687
|
|
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
|
688
|
+
shape = GeoRuby::SimpleFeatures::LineString.from_coordinates(new_coords)
|
689
|
+
{
|
690
|
+
type: :shape,
|
691
|
+
value: shape
|
692
|
+
}
|
693
|
+
end
|
700
694
|
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
695
|
+
def wkt(wkt_string)
|
696
|
+
shape = GeoRuby::SimpleFeatures::Geometry.from_ewkt(wkt_string)
|
697
|
+
{
|
698
|
+
type: :shape,
|
699
|
+
value: shape
|
700
|
+
}
|
701
|
+
rescue GeoRuby::SimpleFeatures::EWKTFormatError
|
702
|
+
@errors << Sparkql::ParserError.new(token: wkt_string,
|
703
|
+
message: "Function call 'wkt' requires a valid wkt string",
|
704
|
+
status: :fatal)
|
705
|
+
nil
|
709
706
|
end
|
710
|
-
end
|
711
707
|
|
712
|
-
|
713
|
-
|
714
|
-
|
708
|
+
def rectangle(coords)
|
709
|
+
bounding_box = parse_coordinates(coords)
|
710
|
+
unless bounding_box.size == 2
|
711
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
712
|
+
message: "Function call 'rectangle' requires two coordinates for the bounding box",
|
713
|
+
status: :fatal)
|
714
|
+
return
|
715
|
+
end
|
716
|
+
poly_coords = [
|
717
|
+
bounding_box.first,
|
718
|
+
[bounding_box.last.first, bounding_box.first.last],
|
719
|
+
bounding_box.last,
|
720
|
+
[bounding_box.first.first, bounding_box.last.last],
|
721
|
+
bounding_box.first.clone
|
722
|
+
]
|
723
|
+
shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([poly_coords])
|
724
|
+
{
|
725
|
+
type: :shape,
|
726
|
+
value: shape
|
727
|
+
}
|
728
|
+
end
|
715
729
|
|
716
|
-
|
717
|
-
|
718
|
-
|
730
|
+
def radius(coords, length)
|
731
|
+
unless length.positive?
|
732
|
+
@errors << Sparkql::ParserError.new(token: length,
|
733
|
+
message: "Function call 'radius' length must be positive",
|
734
|
+
status: :fatal)
|
735
|
+
return
|
736
|
+
end
|
719
737
|
|
720
|
-
|
721
|
-
|
722
|
-
|
738
|
+
# The radius() function is overloaded to allow an identifier
|
739
|
+
# to be specified over lat/lon. This identifier should specify a
|
740
|
+
# record that, in turn, references a lat/lon. Naturally, this won't be
|
741
|
+
# validated here.
|
742
|
+
shape_error = false
|
743
|
+
shape = if coords?(coords)
|
744
|
+
new_coords = parse_coordinates(coords)
|
745
|
+
if new_coords.size != 1
|
746
|
+
shape_error = true
|
747
|
+
else
|
748
|
+
GeoRuby::SimpleFeatures::Circle.from_coordinates(new_coords.first, length)
|
749
|
+
end
|
750
|
+
elsif Sparkql::Geo::RecordRadius.valid_record_id?(coords)
|
751
|
+
Sparkql::Geo::RecordRadius.new(coords, length)
|
752
|
+
else
|
753
|
+
shape_error = true
|
754
|
+
end
|
723
755
|
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
if value.nil?
|
730
|
-
'0'
|
731
|
-
else
|
732
|
-
Integer(Float(value)).to_s
|
756
|
+
if shape_error
|
757
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
758
|
+
message: "Function call 'radius' requires one coordinate for the center",
|
759
|
+
status: :fatal)
|
760
|
+
return
|
733
761
|
end
|
734
|
-
|
735
|
-
|
736
|
-
|
762
|
+
|
763
|
+
{
|
764
|
+
type: :shape,
|
765
|
+
value: shape
|
766
|
+
}
|
767
|
+
end
|
768
|
+
|
769
|
+
def range(start_str, end_str)
|
770
|
+
{
|
771
|
+
type: :character,
|
772
|
+
value: [start_str.to_s, end_str.to_s]
|
773
|
+
}
|
774
|
+
end
|
775
|
+
|
776
|
+
def cast(value, type)
|
777
|
+
value = nil if value == 'NULL'
|
778
|
+
|
779
|
+
new_type = type.to_sym
|
780
|
+
{
|
781
|
+
type: new_type,
|
782
|
+
value: cast_literal(value, new_type)
|
783
|
+
}
|
784
|
+
rescue StandardError
|
785
|
+
{
|
786
|
+
type: :null,
|
787
|
+
value: 'NULL'
|
788
|
+
}
|
789
|
+
end
|
790
|
+
|
791
|
+
def valid_cast_type?(type)
|
792
|
+
if VALID_CAST_TYPES.key?(type.to_sym)
|
793
|
+
true
|
737
794
|
else
|
738
|
-
|
795
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
796
|
+
message: "Function call 'cast' requires a castable type.",
|
797
|
+
status: :fatal)
|
798
|
+
false
|
739
799
|
end
|
740
|
-
when :null
|
741
|
-
'NULL'
|
742
800
|
end
|
743
|
-
end
|
744
801
|
|
745
|
-
|
802
|
+
def cast_null(value, type)
|
803
|
+
cast(value, type)
|
804
|
+
end
|
746
805
|
|
747
|
-
|
748
|
-
|
749
|
-
|
806
|
+
def cast_decimal(value, type)
|
807
|
+
cast(value, type)
|
808
|
+
end
|
809
|
+
|
810
|
+
def cast_character(value, type)
|
811
|
+
cast(value, type)
|
812
|
+
end
|
750
813
|
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
814
|
+
def cast_literal(value, type)
|
815
|
+
case type
|
816
|
+
when :character
|
817
|
+
"'#{value}'"
|
818
|
+
when :integer
|
819
|
+
if value.nil?
|
820
|
+
'0'
|
821
|
+
else
|
822
|
+
Integer(Float(value)).to_s
|
823
|
+
end
|
824
|
+
when :decimal
|
825
|
+
if value.nil?
|
826
|
+
'0.0'
|
827
|
+
else
|
828
|
+
Float(value).to_s
|
829
|
+
end
|
830
|
+
when :null
|
831
|
+
'NULL'
|
832
|
+
end
|
833
|
+
end
|
834
|
+
|
835
|
+
def current_date
|
836
|
+
current_timestamp.to_date
|
837
|
+
end
|
838
|
+
|
839
|
+
def current_time
|
840
|
+
current_timestamp
|
841
|
+
end
|
842
|
+
|
843
|
+
def current_timestamp
|
844
|
+
@current_timestamp ||= Time.now
|
845
|
+
end
|
846
|
+
|
847
|
+
private
|
848
|
+
|
849
|
+
def coords?(coord_string)
|
850
|
+
coord_string.split(' ').size > 1
|
851
|
+
end
|
852
|
+
|
853
|
+
def parse_coordinates(coord_string)
|
854
|
+
terms = coord_string.strip.split(',')
|
855
|
+
terms.map do |term|
|
856
|
+
term.strip.split(/\s+/).reverse.map(&:to_f)
|
857
|
+
end
|
858
|
+
rescue StandardError
|
859
|
+
@errors << Sparkql::ParserError.new(token: coord_string,
|
860
|
+
message: 'Unable to parse coordinate string.',
|
861
|
+
status: :fatal)
|
755
862
|
end
|
756
|
-
coords
|
757
|
-
rescue
|
758
|
-
@errors << Sparkql::ParserError.new(:token => coord_string,
|
759
|
-
:message => "Unable to parse coordinate string.",
|
760
|
-
:status => :fatal )
|
761
863
|
end
|
762
|
-
|
763
864
|
end
|