Class: PgFuncall

Inherits:
Object
  • Object
show all
Extended by:
HelperMethods
Defined in:
lib/pg_funcall.rb,
lib/pg_funcall/version.rb,
lib/pg_funcall/type_map.rb,
lib/pg_funcall/type_info.rb

Defined Under Namespace

Modules: HelperMethods, PGReadable, PGTyped, PGWritable Classes: AR40TypeMap, AR41TypeMap, AR42TypeMap, FunctionSig, Literal, PGTime, PGTimeInterval, PGUUID, PgType, TypeInfo, TypeMap, Typed, TypedArray

Constant Summary collapse

VERSION =
"0.1.1"

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection) ⇒ PgFuncall

Returns a new instance of PgFuncall.

Raises:

  • (ArgumentError)


30
31
32
33
34
35
36
37
# File 'lib/pg_funcall.rb', line 30

def initialize(connection)
  raise ArgumentError, "Requires ActiveRecord PG connection" unless
      connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)

  @ar_connection = connection

  clear_cache
end

Class Method Details

._assign_pg_type_map_to_res(res, conn) ⇒ Object



245
246
247
248
249
250
251
252
253
254
# File 'lib/pg_funcall.rb', line 245

def self._assign_pg_type_map_to_res(res, conn)
  return res

  ## this appears to fail to roundtrip on bytea and date types
  ##
  #if res.respond_to?(:type_map=)
  #  res.type_map = PG::BasicTypeMapForResults.new(conn)
  #end
  #res
end

.default_instanceObject



22
23
24
# File 'lib/pg_funcall.rb', line 22

def self.default_instance
  @default_instance ||= PgFuncall.new(ActiveRecord::Base.connection)
end

.default_instance=(instance) ⇒ Object



26
27
28
# File 'lib/pg_funcall.rb', line 26

def self.default_instance=(instance)
  @default_instance = instance
end

.literal(arg) ⇒ Object

wrap a value so that it is inserted into the query as-is



119
120
121
# File 'lib/pg_funcall.rb', line 119

def self.literal(arg)
  Literal.new(arg)
end

.tag_pg_type(value, tagtype, pgvalue = nil) ⇒ Object



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/pg_funcall.rb', line 98

def self.tag_pg_type(value, tagtype, pgvalue = nil)
  pgvalue ||= value

  # XXX: this is going to blow the method cache every time it runs
  value.class_eval do
    include PGTyped

    define_method(:__pg_value, lambda do
      pgvalue
    end)

    define_method(:__pg_type, lambda do
      tagtype
    end)
  end
  value
end

Instance Method Details

#_ar_connObject



371
372
373
# File 'lib/pg_funcall.rb', line 371

def _ar_conn
  @ar_connection
end

#_cast_pgresult(res) ⇒ Object

Take a PGResult and cast the first column of each tuple to the Ruby equivalent of the PG type as described in the PGResult.



260
261
262
263
264
265
266
# File 'lib/pg_funcall.rb', line 260

def _cast_pgresult(res)
  PgFuncall._assign_pg_type_map_to_res(res, _pg_conn)
  res.column_values(0).map do |val|
    type_map.type_cast_from_database(val,
                                     type_for_typeid(res.ftype(0)))
  end
end

#_format_param_for_descriptor(param, type = nil) ⇒ Object

Represent a Ruby object in a string form to be passed as a parameter within a descriptor hash, rather than substituted into a string-form query.



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
# File 'lib/pg_funcall.rb', line 161

def _format_param_for_descriptor(param, type=nil)
  return param.value if param.is_a?(Literal)

  case param
    when TypedArray
      _format_param_for_descriptor(param.value, param.type + "[]")
    when Typed
      _format_param_for_descriptor(param.value, param.type)
    when PGTyped
      param.respond_to?(:__pg_value) ?
          param.__pg_value :
          _format_param_for_descriptor(param, type)
    when TrueClass
      'true'
    when FalseClass
      'false'
    when String
      if type == 'bytea' || param.encoding == Encoding::BINARY
        '\x' + param.unpack('C*').map {|x| sprintf("%02X", x)}.join("")
      else
        param
      end
    when Array
      "{" + param.map {|p| _format_param_for_descriptor(p)}.join(",") + "}"
    when IPAddr
      param.to_cidr_string
    when Range
      last_char = param.exclude_end? ? ')' : ']'
      case type
        when 'tsrange', 'tstzrange'
          "[#{param.first.utc},#{param.last.utc}#{last_char}"
        else
          "[#{param.first},#{param.last}#{last_char}"
      end
    when Set
      _format_param_for_descriptor(param.to_a)
    when Hash
      param.map do |k,v|
        "#{k} => #{v}"
      end.join(',')
    else
      ActiveRecord::Base.connection.quote(param)
  end
end

#_pg_connObject



375
376
377
# File 'lib/pg_funcall.rb', line 375

def _pg_conn
  _ar_conn.raw_connection
end

#_pg_param_descriptors(params) ⇒ Object



281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/pg_funcall.rb', line 281

def _pg_param_descriptors(params)
  params.map do |p|
    pgtype = _pgtype_for_value(p)
    typeinfo = type_map.resolve(pgtype)
    {
        # value: typeinfo.cast_to_database(p),
        value: _format_param_for_descriptor(p, pgtype),
        # if we can't find a type, let PG guess
        type:  (typeinfo && typeinfo.oid) || 0,
        format: 0
    }
  end
end

#_pgtype_for_value(value) ⇒ Object



303
304
305
306
307
308
309
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
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/pg_funcall.rb', line 303

def _pgtype_for_value(value)
  case value
    # type-forcing wrapper for arrays
    when TypedArray
      value.type + '[]'

    # type-forcing wrapper
    when Typed
      value.type

    # marker ancestor
    when PGTyped
      value.__pg_type

    when String
      if value.encoding == Encoding::BINARY
        'bytea'
      else
        'text'
      end
    when Fixnum, Bignum
      'int4'
    when Float
      'float4'
    when TrueClass, FalseClass
      'bool'
    when BigDecimal
      'numeric'
    when Hash
      'hstore'
    when UUID
      'uuid'
    when Time, DateTime
      'timestamp'
    when Date
      'date'
    when IPAddr
      if value.host?
        'inet'
      else
        'cidr'
      end
    when Range
      case value.last
        when Fixnum
          if value.last > (2**31)-1
            'int8range'
          else
            'int4range'
          end
        when Bignum then 'int8range'
        when DateTime, Time then 'tsrange'
        when Date then 'daterange'
        when Float, BigDecimal, Numeric then 'numrange'
        else
          raise "Unknown range type: #{value.first.type}"
      end
    when Array, Set
      first = value.flatten.first
      raise "Empty untyped array" if first.nil?
      _pgtype_for_value(first) + '[]'
    else
      'text'
  end
end

#_quote_param(param, type = nil) ⇒ Object

“Quote”, which means to format and quote, a parameter for inclusion into a SQL query as a string.



139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/pg_funcall.rb', line 139

def _quote_param(param, type=nil)
  return param.value if param.is_a?(Literal)

  case param
    when Array
      "ARRAY[" + param.map {|p| _quote_param(p)}.join(",") + "]"
    when Set
      _quote_param(param.to_a)
    when Hash
      '$$' + param.map do |k,v|
        "#{k} => #{v}"
      end.join(',') + '$$::hstore'
    else
      ActiveRecord::Base.connection.quote(param)
  end
end

#call_cast(fn, *args) ⇒ Object Also known as: call



268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/pg_funcall.rb', line 268

def call_cast(fn, *args)
  fn_sig = type_map.function_types(fn)

  ## TODO: finish this with the new type info class
  # unwrap = fn_sig && type_map.is_scalar_type?(type_map.lookup_by_oid(fn_sig.ret_type))

  call_raw_pg(fn, *args) do |res|
    results = _cast_pgresult(res)
    # unwrap && results.ntuples < 2 ? results.first : results
    results.first
  end
end

#call_raw_inline(fn, *args) ⇒ Object Also known as: call_raw



206
207
208
209
210
211
212
# File 'lib/pg_funcall.rb', line 206

def call_raw_inline(fn, *args)
  query = "SELECT #{fn}(" +
       args.map {|arg| _quote_param(arg) }.join(", ") + ") as res;"

  ActiveRecord::Base.connection.exec_query(query,
                                           "calling for DB function #{fn}")
end

#call_raw_pg(fn, *args, &blk) ⇒ Object



214
215
216
217
218
219
220
221
# File 'lib/pg_funcall.rb', line 214

def call_raw_pg(fn, *args, &blk)
  query = "SELECT #{fn}(" +
       args.map {|arg| _quote_param(arg) }.join(", ") + ") as res;"

  _pg_conn.query(query, &blk).tap do |res|
    PgFuncall._assign_pg_type_map_to_res(res, _pg_conn)
  end
end

#call_returning_array(fn, *args) ⇒ Object



131
132
133
# File 'lib/pg_funcall.rb', line 131

def call_returning_array(fn, *args)
  call_raw(fn, *args).rows
end

#call_returning_type(fn, ret_type, *args) ⇒ Object

Force a typecast of the return value



240
241
242
243
# File 'lib/pg_funcall.rb', line 240

def call_returning_type(fn, ret_type, *args)
  type_map.type_cast_from_database(call(fn, *args),
                                   type_for_name(ret_type))
end

#call_uncast(fn, *args) ⇒ Object Also known as: call_scalar

Calls Database function with a given set of arguments. Returns result as a string.



126
127
128
# File 'lib/pg_funcall.rb', line 126

def call_uncast(fn, *args)
  call_raw(fn, *args).rows.first.first
end

#casting_query(query, params) ⇒ Object



295
296
297
298
299
300
# File 'lib/pg_funcall.rb', line 295

def casting_query(query, params)
  # puts "param descriptors = #{_pg_param_descriptors(params)}.inspect"
  _pg_conn.exec_params(query, _pg_param_descriptors(params)) do |res|
    _cast_pgresult(res)
  end
end

#clear_cacheObject



39
40
41
42
43
# File 'lib/pg_funcall.rb', line 39

def clear_cache
  (@ftype_cache ||= {}).clear
  @type_map = nil
  true
end

#search_pathObject

Return an array of schema names for the current session’s search path



382
383
384
385
386
# File 'lib/pg_funcall.rb', line 382

def search_path
  _pg_conn.query("SHOW search_path;") do |res|
    res.column_values(0).first.split(/, ?/)
  end
end

#type_for_name(name) ⇒ Object



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

def type_for_name(name)
  type_map.resolve(name)
end

#type_for_typeid(typeid) ⇒ Object



225
226
227
# File 'lib/pg_funcall.rb', line 225

def type_for_typeid(typeid)
  type_map.resolve(typeid.to_i)
end

#type_mapObject



233
234
235
# File 'lib/pg_funcall.rb', line 233

def type_map
  @type_map ||= TypeMap.fetch(@ar_connection, search_path: search_path)
end