Skip to content
Open

PG #4

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions lib/active_record/connection_adapters/postgresql/procedures.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# frozen_string_literal: true

require "active_support"

module ActiveRecord
module PostgreSQLProcedures
module ClassMethods
def set_create_method(&block)
self.custom_create_method = block
end

def set_update_method(&block)
self.custom_update_method = block
end

def set_delete_method(&block)
self.custom_delete_method = block
end
end

def self.included(base)
base.class_eval do
extend ClassMethods
class_attribute :custom_create_method
class_attribute :custom_update_method
class_attribute :custom_delete_method
end
end

def destroy
if self.class.custom_delete_method
with_transaction_returning_status do
run_callbacks(:destroy) { destroy_using_custom_method }
end
else
super
end
end

private
def _create_record
if self.class.custom_create_method
run_callbacks(:create) do
if self.record_timestamps
current_time = current_time_from_proper_timezone

all_timestamp_attributes_in_model.each do |column|
if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
write_attribute(column.to_s, current_time)
end
end
end
create_using_custom_method
end
else
super
end
end

def create_using_custom_method
log_custom_method("custom create method", "#{self.class.name} Create") do
self.id = instance_eval(&self.class.custom_create_method)
end
@new_record = false
@persisted = true
id
end

def _update_record(attribute_names = @attributes.keys)
if self.class.custom_update_method
run_callbacks(:update) do
if should_record_timestamps?
current_time = current_time_from_proper_timezone

timestamp_attributes_for_update_in_model.each do |column|
column = column.to_s
next if will_save_change_to_attribute?(column)
write_attribute(column, current_time)
end
end
if partial_updates?
update_using_custom_method(changed | (attributes.keys & self.class.columns.select { |column| column.is_a?(Type::Serialized) }))
else
update_using_custom_method(attributes.keys)
end
end
else
super
end
end

def update_using_custom_method(attribute_names)
return 0 if attribute_names.empty?
log_custom_method("custom update method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Update") do
instance_eval(&self.class.custom_update_method)
end
1
end

def destroy_using_custom_method
unless new_record? || @destroyed
log_custom_method("custom delete method with #{self.class.primary_key}=#{self.id}", "#{self.class.name} Destroy") do
instance_eval(&self.class.custom_delete_method)
end
end

@destroyed = true
freeze
end

def log_custom_method(*args, &block)
self.class.connection.send(:log, *args, &block)
end

alias_method :update_record, :_update_record if private_method_defined?(:_update_record)
alias_method :create_record, :_create_record if private_method_defined?(:_create_record)
end
end
2 changes: 1 addition & 1 deletion lib/active_record/oracle_enhanced_adapter_patch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ def parse_function_name(name)
protected

def translate_exception(exception, message) #:nodoc:
case @connection.error_code(exception)
case @raw_connection.error_code(exception)
when 1
RecordNotUnique.new(message, exception)
when 2291
Expand Down
7 changes: 7 additions & 0 deletions lib/active_record/plpgsql/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module ActiveRecord::PLSQL
class Base < ActiveRecord::Base
self.abstract_class = true
include Pipelined
include ProcedureMethods
end
end
158 changes: 158 additions & 0 deletions lib/active_record/plpgsql/function_methods.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
require 'active_support/concern'
require 'active_record/connection_adapters/postgresql/procedures'

module ActiveRecord::PLPGSQL
module FunctionMethods
extend ActiveSupport::Concern

class CannotFetchId < StandardError; end

included do
include ActiveRecord::PostgreSQLProcedures

class_attribute :function_schema, :function_method_cache, instance_writer: false
self.function_schema = nil
self.function_method_cache = Hash.new do |cache, klass|
cache[klass] = Hash.new do |methods, method|
# Inherits procedure methods from base class
if klass.superclass.respond_to?(:function_methods)
methods[method] = klass.superclass.function_methods[method]
else
nil
end
end
end
end

module ClassMethods
def set_create_function(function, options = {}, &reload_block)
block ||= proc do |record, result|
case result
when Hash
record.id = result.values.first
when Numeric
record.id = result
else
raise CannotFetchId, "Couldn't fetch primary key from create procedure (%s) result: %s" %
[procedure, result.inspect]
end

reload_block ? reload_block.call(record) : record.reload

record.instance_variable_set(:@new_record, true)
record.id
end

function_method(:create, function, options, &block)
set_create_method {call_function_method(:create)}
end

def set_update_function(function, options = {})
function_method(:update, function, options) do |record|
record.reload
record.id
end
set_update_method {call_function_method(:update)}
end

def set_destroy_function(function, options = {})
function_method(:destroy, function, options)
set_delete_method {call_function_method(:destroy)}
end

def function_methods
function_method_cache[self]
end

def function_method(method, function_name = method, options = {}, &block)
function = if ::PLPGSQL::Routine === function_name
function_name
else
find_function(function_name)
end

# Raise error if procedure not found
raise ArgumentError, "Function (%s) not found for method (%s)" % [function_name, method] unless function

function_methods[method] = {routine: function, options: options, block: block}

unless (instance_methods + private_instance_methods).find {|m| m == method}
@generated_attribute_methods.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
def #{method}(arguments = {}, options = {})
call_function_method(:#{method}, arguments, options)
end
RUBY
end
end

def functions_arguments
@functions_arguments ||= Hash.new do |cache, function|
# Always select arguments of first function (overloading not supported)
cache[function] = Hash[ function.arguments[0].sort_by {|arg| arg[1][:position]} ]
end
end

private

def find_function(function_name)
case function_name.to_s.split('.').compact
in [package, function]
plpgsql.send(package.to_sym)[function.to_sym]
in [function]
if function_schema
function_schema[function] || ::PLPGSQL::Routine.find(
plpgsql,
schema_name: function_schema.name,
routine_name: function
)
else
raise ArgumentError, "Function (%s) not found" % function_name
end
end
end
end

delegate :functions_arguments, :function_methods, to: 'self.class'

private

def call_function_method(method, arguments = {}, opts = {})
function, options, block = function_methods[method].values_at(:routine, :options, :block)
options = options.merge(opts)

if options[:arguments]
if arguments.is_a?(Hash)
arguments = arguments.merge(instance_exec(&options[:arguments]))
else
arguments += options[:arguments]
end
end

options[:arguments] = arguments
call_function(function, options, &block)
end

def call_function(function, options = {})
result = function.(*get_function_arguments(function, options))
if block_given?
yield(self, result)
else
result
end
end

def get_function_arguments(function, options)
arguments = options[:arguments]
arguments = arguments.dup if arguments.duplicable?

if Hash === arguments
arguments.symbolize_keys!
arguments_metadata = procedures_arguments[function]
# throw away unnecessary arguments
[arguments.select {|k,_| arguments_metadata[k]}]
else
arguments
end
end
end
end
Loading