Class: Sparkql::FunctionResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/sparkql/function_resolver.rb

Overview

Binding class to all supported function calls in the parser. Current support requires that the resolution of function calls to happen on the fly at parsing time at which point a value and value type is required, just as literals would be returned to the expression tokenization level.

Name and argument requirements for the function should match the function declaration in SUPPORTED_FUNCTIONS which will run validation on the function syntax prior to execution.

Constant Summary collapse

SECONDS_IN_MINUTE =
60
SECONDS_IN_HOUR =
SECONDS_IN_MINUTE * 60
SECONDS_IN_DAY =
SECONDS_IN_HOUR * 24
STRFTIME_DATE_FORMAT =
'%Y-%m-%d'
STRFTIME_TIME_FORMAT =
'%H:%M:%S.%N'
VALID_REGEX_FLAGS =
['', 'i'].freeze
MIN_DATE_TIME =
Time.new(1970, 1, 1, 0, 0, 0, '+00:00').iso8601
MAX_DATE_TIME =
Time.new(9999, 12, 31, 23, 59, 59, '+00:00').iso8601
MAX_DAYS =

1000 years ought to cover most cases

(365 * 1000).freeze
VALID_CAST_TYPES =
%i[field character decimal integer].freeze
SUPPORTED_FUNCTIONS =
{
  all: {
    args: [:field],
    return_type: :all
  },
  polygon: {
    args: [:character],
    return_type: :shape
  },
  rectangle: {
    args: [:character],
    return_type: :shape
  },
  radius: {
    args: [:character, %i[decimal integer]],
    return_type: :shape
  },
  regex: {
    args: [:character],
    opt_args: [{
                 type: :character,
                 default: ''
               }],
    return_type: :character
  },
  substring: {
    args: [%i[field character], :integer],
    opt_args: [{
                 type: :integer
               }],
    resolve_for_type: true,
    return_type: :character
  },
  trim: {
    args: [%i[field character]],
    resolve_for_type: true,
    return_type: :character
  },
  tolower: {
    args: [%i[field character]],
    resolve_for_type: true,
    return_type: :character
  },
  toupper: {
    args: [%i[field character]],
    resolve_for_type: true,
    return_type: :character
  },
  length: {
    args: [%i[field character]],
    resolve_for_type: true,
    return_type: :integer
  },
  indexof: {
    args: [%i[field character], :character],
    return_type: :integer
  },
  concat: {
    args: [%i[field character], :character],
    resolve_for_type: true,
    return_type: :character
  },
  cast: {
    args: [%i[field character decimal integer null], :character],
    resolve_for_type: true
  },
  round: {
    args: [%i[field decimal]],
    resolve_for_type: true,
    return_type: :integer
  },
  ceiling: {
    args: [%i[field decimal]],
    resolve_for_type: true,
    return_type: :integer
  },
  floor: {
    args: [%i[field decimal]],
    resolve_for_type: true,
    return_type: :integer
  },
  startswith: {
    args: [:character],
    return_type: :startswith
  },
  endswith: {
    args: [:character],
    return_type: :endswith
  },
  contains: {
    args: [:character],
    return_type: :contains
  },
  linestring: {
    args: [:character],
    return_type: :shape
  },
  seconds: {
    args: [:integer],
    return_type: :datetime
  },
  minutes: {
    args: [:integer],
    return_type: :datetime
  },
  hours: {
    args: [:integer],
    return_type: :datetime
  },
  days: {
    args: [:integer],
    return_type: :datetime
  },
  weekdays: {
    args: [:integer],
    return_type: :datetime
  },
  months: {
    args: [:integer],
    return_type: :datetime
  },
  years: {
    args: [:integer],
    return_type: :datetime
  },
  now: {
    args: [],
    return_type: :datetime
  },
  maxdatetime: {
    args: [],
    return_type: :datetime
  },
  mindatetime: {
    args: [],
    return_type: :datetime
  },
  date: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :date
  },
  time: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :time
  },
  year: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :integer
  },
  dayofyear: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :integer
  },
  month: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :integer
  },
  day: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :integer
  },
  dayofweek: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :integer
  },
  hour: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :integer
  },
  minute: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :integer
  },
  second: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :integer
  },
  fractionalseconds: {
    args: [%i[field datetime date]],
    resolve_for_type: true,
    return_type: :decimal
  },
  range: {
    args: %i[character character],
    return_type: :character
  },
  wkt: {
    args: [:character],
    return_type: :shape
  }
}.freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, args, options = {}) ⇒ FunctionResolver

Construct a resolver instance for a function name: function name (String) args: array of literal hashes of the format :value=><escaped_literal_value>.

Empty arry for functions that have no arguments.


237
238
239
240
241
242
# File 'lib/sparkql/function_resolver.rb', line 237

def initialize(name, args, options = {})
  @name = name
  @args = args
  @errors = []
  @current_timestamp = options[:current_timestamp]
end

Instance Attribute Details

#errorsObject (readonly)

Returns the value of attribute errors.



299
300
301
# File 'lib/sparkql/function_resolver.rb', line 299

def errors
  @errors
end

Class Method Details

.lookup(function_name) ⇒ Object



229
230
231
# File 'lib/sparkql/function_resolver.rb', line 229

def self.lookup(function_name)
  SUPPORTED_FUNCTIONS[function_name.to_sym]
end

Instance Method Details

#callObject

Execute the function



310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/sparkql/function_resolver.rb', line 310

def call
  real_vals = @args.map { |i| i[:value] }
  name = @name.to_sym

  field = @args.find do |i|
    i[:type] == :field || i.key?(:field)
  end

  field = field[:type] == :function ? field[:field] : field[:value] unless field.nil?

  required_args = support[name][:args]
  total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:default] }

  fill_in_optional_args = total_args.drop(real_vals.length)

  fill_in_optional_args.each do |default|
    real_vals << default
  end

  v = if field.nil?
        method = name
        if support[name][:resolve_for_type]
          method_type = @args.first[:type]
          method = "#{method}_#{method_type}"
        end
        send(method, *real_vals)
      else
        {
          type: :function,
          return_type: return_type,
          value: name.to_s
        }
      end

  return if v.nil?

  unless v.key?(:function_name)
    v.merge!(function_parameters: real_vals,
             function_name: @name)
  end

  v.merge!(args: @args,
           field: field)

  v
end

#errors?Boolean

Returns:

  • (Boolean)


301
302
303
# File 'lib/sparkql/function_resolver.rb', line 301

def errors?
  @errors.size.positive?
end

#return_typeObject



289
290
291
292
293
294
295
296
297
# File 'lib/sparkql/function_resolver.rb', line 289

def return_type
  name = @name.to_sym

  if name == :cast
    @args.last[:value].to_sym
  else
    support[@name.to_sym][:return_type]
  end
end

#supportObject



305
306
307
# File 'lib/sparkql/function_resolver.rb', line 305

def support
  SUPPORTED_FUNCTIONS
end

#validateObject

Validate the function instance prior to calling it. All validation failures will show up in the errors array.



246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/sparkql/function_resolver.rb', line 246

def validate
  name = @name.to_sym
  unless support.key?(name)
    @errors << Sparkql::ParserError.new(token: @name,
                                        message: "Unsupported function call '#{@name}' for expression",
                                        status: :fatal)
    return
  end

  required_args = support[name][:args]
  total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:type] }

  if @args.size < required_args.size || @args.size > total_args.size
    @errors << Sparkql::ParserError.new(token: @name,
                                        message: "Function call '#{@name}' requires #{required_args.size} arguments",
                                        status: :fatal)
    return
  end

  count = 0
  @args.each do |arg|
    type = arg[:type] == :function ? arg[:return_type] : arg[:type]
    unless Array(total_args[count]).include?(type)
      @errors << Sparkql::ParserError.new(token: @name,
                                          message: "Function call '#{@name}' has an invalid argument at #{arg[:value]}",
                                          status: :fatal)
    end
    count += 1
  end

  if name == :cast
    type = @args.last[:value]
    unless VALID_CAST_TYPES.include?(type.to_sym)
      @errors << Sparkql::ParserError.new(token: @name,
                                          message: "Function call '#{@name}' requires a castable type.",
                                          status: :fatal)
      return
    end
  end

  substring_index_error?(@args[2][:value]) if name == :substring && !@args[2].nil?
end