sparkql 1.2.6 → 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 +4 -0
- data/Gemfile +1 -2
- data/VERSION +1 -1
- data/lib/sparkql/function_resolver.rb +765 -714
- data/lib/sparkql/parser_compatibility.rb +1 -1
- data/sparkql.gemspec +19 -17
- data/test/unit/function_resolver_test.rb +375 -280
- data/test/unit/parser_test.rb +47 -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
|
-
ZmE5ZTQwZDc1YzI0YTJmZTRjYzMxMGY3ZDZmYmFmNzkxMWRmNTBiMg==
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 216e474f5077d8b33ede2b8bc61118bed6eab33d530eae71a642306d53257e6a
|
4
|
+
data.tar.gz: da6486cedc2ed93ac8db963572afbeccead88643535430442e59d8b941dd0fe6
|
7
5
|
SHA512:
|
8
|
-
metadata.gz:
|
9
|
-
|
10
|
-
NTc1ZTMwYTVmNmM5Nzk2Mjk5OTk2Nzc5ZmYyNTA3Njc2ZmU0NjkzM2I2ZmQx
|
11
|
-
YTQxMzIzZjBjNDU0NTc1Y2VmYzMyNGRmZDNmNWFlNTFmYzFiZjg=
|
12
|
-
data.tar.gz: !binary |-
|
13
|
-
MzczODc4ZTAyOTRkNDlhNzZiYTQxYjUyM2FlMTQ0ZmU3NmMyZmY2Y2Q2YmFj
|
14
|
-
MTk0ODFmMTFmZmI4MTQ2MzkxOTFiYjkwYmM1NGQ1NzdmZjNiMjZmYjQ4MGJi
|
15
|
-
ODY1NmM3NWJhNTFmMzgxY2EwZGU5NWY1ZGVmYTVkZTBjMGZlNmU=
|
6
|
+
metadata.gz: 87f6fa0829bb7d99e49bb1675f4d8090ce9271d0dea9ae8da15522b57f86f03f87df21f1187cd4cb6f5812f880da74f690dfae12f99fa52d41957fa09062d82f
|
7
|
+
data.tar.gz: 8575083d2bf0a94bb299109617ab2f41641b01636427883ace5733b5500859e3419a6d977f30a79c15602c7b9fe1ec7315c7d4508cab4f1829f19a365c7547f5
|
data/CHANGELOG.md
CHANGED
data/Gemfile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.2.
|
1
|
+
1.2.7
|
@@ -1,804 +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
|
-
|
231
|
-
if @args.size < required_args.size || @args.size > total_args.size
|
232
|
-
@errors << Sparkql::ParserError.new(:token => @name,
|
233
|
-
:message => "Function call '#{@name}' requires #{required_args.size} arguments",
|
234
|
-
:status => :fatal )
|
235
|
-
return
|
236
|
-
end
|
237
|
-
|
238
|
-
count = 0
|
239
|
-
@args.each do |arg|
|
240
|
-
type = arg[:type] == :function ? arg[:return_type] : arg[:type]
|
241
|
-
unless Array(total_args[count]).include?(type)
|
242
|
-
@errors << Sparkql::ParserError.new(:token => @name,
|
243
|
-
:message => "Function call '#{@name}' has an invalid argument at #{arg[:value]}",
|
244
|
-
:status => :fatal )
|
245
|
-
end
|
246
|
-
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 = []
|
247
232
|
end
|
248
233
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
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)
|
255
252
|
return
|
256
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?
|
257
277
|
end
|
258
278
|
|
259
|
-
|
260
|
-
|
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
|
261
287
|
end
|
262
|
-
end
|
263
|
-
|
264
|
-
def return_type
|
265
|
-
name = @name.to_sym
|
266
288
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
289
|
+
attr_reader :errors
|
290
|
+
|
291
|
+
def errors?
|
292
|
+
@errors.size.positive?
|
271
293
|
end
|
272
|
-
end
|
273
|
-
|
274
|
-
def errors
|
275
|
-
@errors
|
276
|
-
end
|
277
|
-
|
278
|
-
def errors?
|
279
|
-
@errors.size > 0
|
280
|
-
end
|
281
|
-
|
282
|
-
def support
|
283
|
-
SUPPORTED_FUNCTIONS
|
284
|
-
end
|
285
|
-
|
286
|
-
# Execute the function
|
287
|
-
def call()
|
288
|
-
real_vals = @args.map { |i| i[:value]}
|
289
|
-
name = @name.to_sym
|
290
294
|
|
291
|
-
|
292
|
-
|
295
|
+
def support
|
296
|
+
SUPPORTED_FUNCTIONS
|
293
297
|
end
|
294
298
|
|
295
|
-
|
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)
|
296
314
|
|
297
|
-
|
298
|
-
|
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
|
299
340
|
|
300
|
-
|
341
|
+
v.merge!(args: @args,
|
342
|
+
field: field)
|
301
343
|
|
302
|
-
|
303
|
-
real_vals << default
|
344
|
+
v
|
304
345
|
end
|
305
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
|
306
358
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
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
|
312
366
|
end
|
313
|
-
|
314
|
-
|
367
|
+
|
368
|
+
{
|
369
|
+
type: :character,
|
370
|
+
value: regular_expression
|
371
|
+
}
|
372
|
+
end
|
373
|
+
|
374
|
+
def trim_character(arg)
|
315
375
|
{
|
316
|
-
:
|
317
|
-
:
|
318
|
-
:value => "#{name}",
|
376
|
+
type: :character,
|
377
|
+
value: arg.strip
|
319
378
|
}
|
320
379
|
end
|
321
380
|
|
322
|
-
|
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
|
323
387
|
|
324
|
-
|
325
|
-
|
326
|
-
|
388
|
+
new_string = character[first_index..second_index].to_s
|
389
|
+
|
390
|
+
{
|
391
|
+
type: :character,
|
392
|
+
value: new_string
|
393
|
+
}
|
327
394
|
end
|
328
395
|
|
329
|
-
|
330
|
-
|
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
|
331
405
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
406
|
+
def tolower(_args)
|
407
|
+
{
|
408
|
+
type: :character,
|
409
|
+
value: 'tolower'
|
410
|
+
}
|
411
|
+
end
|
338
412
|
|
339
|
-
|
413
|
+
def tolower_character(string)
|
414
|
+
{
|
415
|
+
type: :character,
|
416
|
+
value: "'#{string.to_s.downcase}'"
|
417
|
+
}
|
418
|
+
end
|
340
419
|
|
341
|
-
|
342
|
-
|
343
|
-
:
|
344
|
-
:
|
345
|
-
|
420
|
+
def toupper_character(string)
|
421
|
+
{
|
422
|
+
type: :character,
|
423
|
+
value: "'#{string.to_s.upcase}'"
|
424
|
+
}
|
346
425
|
end
|
347
426
|
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
:status => :fatal)
|
354
|
-
return
|
427
|
+
def length_character(string)
|
428
|
+
{
|
429
|
+
type: :integer,
|
430
|
+
value: string.size.to_s
|
431
|
+
}
|
355
432
|
end
|
356
433
|
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
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}"
|
362
444
|
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
445
|
+
{
|
446
|
+
function_name: 'regex',
|
447
|
+
function_parameters: [new_value, ''],
|
448
|
+
type: :character,
|
449
|
+
value: new_value
|
450
|
+
}
|
451
|
+
end
|
369
452
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
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
|
+
}
|
375
470
|
end
|
376
471
|
|
377
|
-
|
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
|
378
482
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
483
|
+
{
|
484
|
+
function_name: 'regex',
|
485
|
+
function_parameters: [new_value, ''],
|
486
|
+
type: :character,
|
487
|
+
value: new_value
|
488
|
+
}
|
489
|
+
end
|
384
490
|
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
:
|
390
|
-
|
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
|
+
}
|
391
498
|
end
|
392
|
-
false
|
393
|
-
end
|
394
499
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
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
|
401
508
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
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
|
408
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
|
409
528
|
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
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
|
416
546
|
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
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
|
423
557
|
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
#
|
430
|
-
# The string passed in will merely be "far", rather than
|
431
|
-
# the string literal "'far'".
|
432
|
-
string = Regexp.escape(string)
|
433
|
-
new_value = "^#{string}"
|
434
|
-
|
435
|
-
{
|
436
|
-
:function_name => "regex",
|
437
|
-
:function_parameters => [new_value, ''],
|
438
|
-
:type => :character,
|
439
|
-
:value => new_value
|
440
|
-
}
|
441
|
-
end
|
558
|
+
{
|
559
|
+
type: :date,
|
560
|
+
value: today.strftime(STRFTIME_DATE_FORMAT)
|
561
|
+
}
|
562
|
+
end
|
442
563
|
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
# the string literal "'far'".
|
451
|
-
string = Regexp.escape(string)
|
452
|
-
new_value = "#{string}$"
|
453
|
-
|
454
|
-
{
|
455
|
-
:function_name => "regex",
|
456
|
-
:function_parameters => [new_value, ''],
|
457
|
-
:type => :character,
|
458
|
-
:value => new_value
|
459
|
-
}
|
460
|
-
end
|
564
|
+
# The current timestamp
|
565
|
+
def now
|
566
|
+
{
|
567
|
+
type: :datetime,
|
568
|
+
value: current_time.iso8601
|
569
|
+
}
|
570
|
+
end
|
461
571
|
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
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
|
-
}
|
479
|
-
end
|
572
|
+
def maxdatetime
|
573
|
+
{
|
574
|
+
type: :datetime,
|
575
|
+
value: MAX_DATE_TIME
|
576
|
+
}
|
577
|
+
end
|
480
578
|
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
}
|
488
|
-
end
|
579
|
+
def mindatetime
|
580
|
+
{
|
581
|
+
type: :datetime,
|
582
|
+
value: MIN_DATE_TIME
|
583
|
+
}
|
584
|
+
end
|
489
585
|
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
}
|
497
|
-
end
|
586
|
+
def floor_decimal(arg)
|
587
|
+
{
|
588
|
+
type: :integer,
|
589
|
+
value: arg.floor.to_s
|
590
|
+
}
|
591
|
+
end
|
498
592
|
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
}
|
506
|
-
end
|
593
|
+
def ceiling_decimal(arg)
|
594
|
+
{
|
595
|
+
type: :integer,
|
596
|
+
value: arg.ceil.to_s
|
597
|
+
}
|
598
|
+
end
|
507
599
|
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
:type => :date,
|
515
|
-
:value => d.strftime(STRFTIME_DATE_FORMAT)
|
516
|
-
}
|
517
|
-
end
|
518
|
-
|
519
|
-
# The current timestamp
|
520
|
-
def now()
|
521
|
-
{
|
522
|
-
:type => :datetime,
|
523
|
-
:value => Time.now.iso8601
|
524
|
-
}
|
525
|
-
end
|
600
|
+
def round_decimal(arg)
|
601
|
+
{
|
602
|
+
type: :integer,
|
603
|
+
value: arg.round.to_s
|
604
|
+
}
|
605
|
+
end
|
526
606
|
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
607
|
+
def indexof(arg1, arg2)
|
608
|
+
{
|
609
|
+
value: 'indexof',
|
610
|
+
args: [arg1, arg2]
|
611
|
+
}
|
612
|
+
end
|
533
613
|
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
614
|
+
def concat_character(arg1, arg2)
|
615
|
+
{
|
616
|
+
type: :character,
|
617
|
+
value: "'#{arg1}#{arg2}'"
|
618
|
+
}
|
619
|
+
end
|
540
620
|
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
621
|
+
def date_datetime(datetime)
|
622
|
+
{
|
623
|
+
type: :date,
|
624
|
+
value: datetime.strftime(STRFTIME_DATE_FORMAT)
|
625
|
+
}
|
626
|
+
end
|
547
627
|
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
552
|
-
|
553
|
-
|
628
|
+
def time_datetime(datetime)
|
629
|
+
{
|
630
|
+
type: :time,
|
631
|
+
value: datetime.strftime(STRFTIME_TIME_FORMAT)
|
632
|
+
}
|
633
|
+
end
|
554
634
|
|
555
|
-
|
556
|
-
|
557
|
-
|
558
|
-
|
559
|
-
|
560
|
-
|
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
|
561
642
|
|
562
|
-
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
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
|
568
650
|
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
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
|
575
659
|
|
576
|
-
|
577
|
-
|
578
|
-
:type => :date,
|
579
|
-
:value => dt.strftime(STRFTIME_DATE_FORMAT)
|
580
|
-
}
|
581
|
-
end
|
582
|
-
|
583
|
-
def time_datetime(dt)
|
584
|
-
{
|
585
|
-
:type => :time,
|
586
|
-
:value => dt.strftime(STRFTIME_TIME_FORMAT)
|
587
|
-
}
|
588
|
-
end
|
660
|
+
# auto close the polygon if it's open
|
661
|
+
new_coords << new_coords.first.clone unless new_coords.first == new_coords.last
|
589
662
|
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
end
|
663
|
+
shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([new_coords])
|
664
|
+
{
|
665
|
+
type: :shape,
|
666
|
+
value: shape
|
667
|
+
}
|
668
|
+
end
|
597
669
|
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
def polygon(coords)
|
607
|
-
new_coords = parse_coordinates(coords)
|
608
|
-
unless new_coords.size > 2
|
609
|
-
@errors << Sparkql::ParserError.new(:token => coords,
|
610
|
-
:message => "Function call 'polygon' requires at least three coordinates",
|
611
|
-
:status => :fatal )
|
612
|
-
return
|
613
|
-
end
|
614
|
-
|
615
|
-
# auto close the polygon if it's open
|
616
|
-
unless new_coords.first == new_coords.last
|
617
|
-
new_coords << new_coords.first.clone
|
618
|
-
end
|
619
|
-
|
620
|
-
shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([new_coords])
|
621
|
-
{
|
622
|
-
:type => :shape,
|
623
|
-
:value => shape
|
624
|
-
}
|
625
|
-
end
|
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
|
626
678
|
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
:status => :fatal )
|
633
|
-
return
|
679
|
+
shape = GeoRuby::SimpleFeatures::LineString.from_coordinates(new_coords)
|
680
|
+
{
|
681
|
+
type: :shape,
|
682
|
+
value: shape
|
683
|
+
}
|
634
684
|
end
|
635
685
|
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
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
|
697
|
+
end
|
642
698
|
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
:
|
661
|
-
:
|
662
|
-
|
663
|
-
end
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
|
675
|
-
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
680
|
-
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
# The radius() function is overloaded to allow an identifier
|
688
|
-
# to be specified over lat/lon. This identifier should specify a
|
689
|
-
# record that, in turn, references a lat/lon. Naturally, this won't be
|
690
|
-
# validated here.
|
691
|
-
shape_error = false
|
692
|
-
shape = if is_coords?(coords)
|
693
|
-
new_coords = parse_coordinates(coords)
|
694
|
-
if new_coords.size != 1
|
695
|
-
shape_error = true
|
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
|
720
|
+
|
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
|
728
|
+
|
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)
|
696
743
|
else
|
697
|
-
|
744
|
+
shape_error = true
|
698
745
|
end
|
699
|
-
elsif Sparkql::Geo::RecordRadius.valid_record_id?(coords)
|
700
|
-
Sparkql::Geo::RecordRadius.new(coords, length)
|
701
|
-
else
|
702
|
-
shape_error = true
|
703
|
-
end
|
704
746
|
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
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
|
+
}
|
710
758
|
end
|
711
759
|
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
718
|
-
def range(start_str, end_str)
|
719
|
-
{
|
720
|
-
:type => :character,
|
721
|
-
:value => [start_str.to_s, end_str.to_s]
|
722
|
-
}
|
723
|
-
end
|
760
|
+
def range(start_str, end_str)
|
761
|
+
{
|
762
|
+
type: :character,
|
763
|
+
value: [start_str.to_s, end_str.to_s]
|
764
|
+
}
|
765
|
+
end
|
724
766
|
|
725
|
-
|
726
|
-
|
727
|
-
value = nil
|
728
|
-
end
|
729
|
-
|
730
|
-
new_type = type.to_sym
|
731
|
-
{
|
732
|
-
type: new_type,
|
733
|
-
value: cast_literal(value, new_type)
|
734
|
-
}
|
735
|
-
rescue
|
736
|
-
{
|
737
|
-
type: :null,
|
738
|
-
value: 'NULL'
|
739
|
-
}
|
740
|
-
end
|
767
|
+
def cast(value, type)
|
768
|
+
value = nil if value == 'NULL'
|
741
769
|
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
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
|
+
}
|
750
780
|
end
|
751
|
-
end
|
752
781
|
|
753
|
-
|
754
|
-
|
755
|
-
|
782
|
+
def valid_cast_type?(type)
|
783
|
+
if VALID_CAST_TYPES.key?(type.to_sym)
|
784
|
+
true
|
785
|
+
else
|
786
|
+
@errors << Sparkql::ParserError.new(token: coords,
|
787
|
+
message: "Function call 'cast' requires a castable type.",
|
788
|
+
status: :fatal)
|
789
|
+
false
|
790
|
+
end
|
791
|
+
end
|
756
792
|
|
757
|
-
|
758
|
-
|
759
|
-
|
793
|
+
def cast_null(value, type)
|
794
|
+
cast(value, type)
|
795
|
+
end
|
760
796
|
|
761
|
-
|
762
|
-
|
763
|
-
|
797
|
+
def cast_decimal(value, type)
|
798
|
+
cast(value, type)
|
799
|
+
end
|
764
800
|
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
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'
|
780
823
|
end
|
781
|
-
when :null
|
782
|
-
'NULL'
|
783
824
|
end
|
784
|
-
end
|
785
825
|
|
786
|
-
|
826
|
+
def current_date
|
827
|
+
current_timestamp.to_date
|
828
|
+
end
|
787
829
|
|
788
|
-
|
789
|
-
|
790
|
-
|
830
|
+
def current_time
|
831
|
+
current_timestamp.to_time
|
832
|
+
end
|
791
833
|
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
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)
|
796
853
|
end
|
797
|
-
coords
|
798
|
-
rescue
|
799
|
-
@errors << Sparkql::ParserError.new(:token => coord_string,
|
800
|
-
:message => "Unable to parse coordinate string.",
|
801
|
-
:status => :fatal )
|
802
854
|
end
|
803
|
-
|
804
855
|
end
|