rails-plsql 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZDI1NzUzYjdlZWVkZDMyN2M0N2E1OTg0YzUzMWE3ZjhhODJlNjE0Zg==
5
+ data.tar.gz: !binary |-
6
+ MTgyMzYyZmU5OWJlMzgxZjYzNzY2NzQwODNiNDkwZmUyOTk5YTJkZA==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ ODgwN2M4ZGQyNzU2MjllZWI4NzI5MjkzYWIwMDc3MWQwNDdkMTgxYTNhZjE1
10
+ MGU3ZjAzZDUxMzg4YjY2NzMxODQ1M2NkNThjN2M5ZTI4NmZiZTY3NzkxNjRl
11
+ YmQ5YjQ2NjgwZGQ5MWE0MzI0ZjBlYmZhMzcwNzk0YzU3ZWYyNTQ=
12
+ data.tar.gz: !binary |-
13
+ MzhkZDk3MmY4YzJkOGQ3ZjQwZTEwNGEwNmVjYjg0ZjA3M2ZlNTVjYjU0MTNh
14
+ YmE1ZjU4M2U5MGUzNzExNDg4Njk4YzU1MDlmYTFmOTMyMTcxYWIwNzMzMjMx
15
+ ODhkZTIyMTBlNmUyNjA4NTdlNmVlYmE1NTgwNzY2M2JlYzFjNDc=
data/MIT-LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright © 2012 Nikita Shilnikov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ [![Code Climate](https://codeclimate.com/github/flash-gordon/rails-plsql.png)](https://codeclimate.com/github/flash-gordon/rails-plsql)
2
+ rails-plsql
3
+ ====================================
4
+
5
+ Middleware between ActiveRecord and Oracle Database
6
+
7
+ Description
8
+ -----------
9
+
10
+ This gem is ActiveRecord extension for some Oracle Database specific features such as pipelined functions or PL/SQL procedures. It uses [ruby-plsql](https://github.com/rsim/ruby-plsql) and [oracle enhanced adapter](https://github.com/rsim/oracle-enhanced) gems as dependencies for connection to Oracle and calling PL/SQL procedures and functions. It also adds basic logger to [my fork](https://github.com/flash-gordon/ruby-plsql) of ruby-plsql gem.
11
+
12
+ Installation
13
+ ------------
14
+
15
+ ### Rails 3.2
16
+
17
+ Just put this line into your Gemfile
18
+
19
+ gem 'rails-plsql', '~> 0.1', github: 'flash-gordon/rails-plsql'
20
+
21
+ Gem tested with MRI 1.9.2, 1.9.3, 2.0.0 and JRuby 1.7.4. So if you use ruby-oci8 then add also
22
+
23
+ gem 'ruby-oci8', '~> 2.1.0'
24
+
25
+ And run
26
+
27
+ bundle install
28
+
29
+ to install all gems.
30
+
31
+ Other versions of Rails not tested.
32
+
33
+ Usage
34
+ -----
35
+
36
+ ### Pipelined functions as tables in ActiveRecord models
37
+
38
+ Oracle pipelined functions could be used as data source instead of ordinary tables (or views).
39
+
40
+ If you have such PL/SQL function
41
+
42
+ ```sql
43
+
44
+ CREATE OR REPLACE
45
+ PACKAGE users_pkg IS
46
+
47
+ TYPE users_list IS TABLE OF USERS%ROWTYPE;
48
+
49
+ FUNCTION find_users_by_name(
50
+ p_name USERS.NAME%TYPE)
51
+ RETURN users_list
52
+ PIPELINED;
53
+
54
+ END users_pkg;
55
+ /
56
+
57
+ CREATE OR REPLACE
58
+ PACKAGE BODY users_pkg IS
59
+
60
+ FUNCTION find_users_by_name(
61
+ p_name USERS.NAME%TYPE)
62
+ RETURN users_list
63
+ PIPELINED
64
+ IS
65
+ BEGIN
66
+ FOR l_user IN (
67
+ SELECT *
68
+ FROM users
69
+ WHERE name = p_name)
70
+ LOOP
71
+ PIPE ROW(l_user);
72
+ END LOOP;
73
+ END FIND_USERS_BY_NAME;
74
+
75
+ END users_pkg;
76
+ /
77
+ ```
78
+
79
+ So you can set this function in your model instead of table name
80
+
81
+ ```ruby
82
+ class User < ActiveRecord::Base
83
+ include ActiveRecord::PLSQL::Pipelined
84
+
85
+ self.pipelined_function = 'users_pkg.find_users_by_name'
86
+
87
+ scope :alberts, where(p_name: 'Albert')
88
+ scope :einsteins, where(surname: 'Einstein')
89
+ end
90
+ ```
91
+
92
+ and use standard Rails scopes and finders
93
+
94
+ ```ruby
95
+ User.alberts
96
+ # [#<User id: #<BigDecimal:6fec77a4,'0.1E1',9(36)>, name: "Albert", surname: "Einstein">]
97
+ User.alberts.einsteins.first
98
+ # #<User id: #<BigDecimal:6fec77a4,'0.1E1',9(36)>, name: "Albert", surname: "Einstein">
99
+
100
+ User.all(conditions: {p_name: 'Max'})
101
+ # [#<User id: #<BigDecimal:6ee2c728,'0.3E1',9(36)>, name: "Max", surname: "Planck">]
102
+ ```
103
+
104
+ Pipelined function arguments must be set via `where` condition (see `p_name` usage above). If not they will be set to NULL.
105
+
106
+ ### Oracle procedures and functions as methods of ActiveRecord objects
107
+
108
+ If you have some PL/SQL package related with AR model you could bind it to class.
109
+
110
+ ```sql
111
+ CREATE OR REPLACE
112
+ PACKAGE users_pkg IS
113
+
114
+ FUNCTION salute(
115
+ p_name IN VARCHAR2)
116
+ RETURN VARCHAR2;
117
+
118
+ END users_pkg;
119
+ /
120
+
121
+ CREATE OR REPLACE
122
+ PACKAGE BODY users_pkg IS
123
+
124
+ FUNCTION salute(
125
+ p_name IN VARCHAR2)
126
+ RETURN VARCHAR2
127
+ IS
128
+ BEGIN
129
+ RETURN 'Hello, ' || p_name || '!';
130
+ END salute;
131
+
132
+ END users_pkg;
133
+ /
134
+ ```
135
+
136
+ ```ruby
137
+ class User < ActiveRecord::Base
138
+ include ActiveRecord::PLSQL::ProcedureMethods
139
+
140
+ self.plsql_package = plsql.users_pkg
141
+ procedure_method :salute
142
+ end
143
+ ```
144
+
145
+ After that you can call procedure as method
146
+
147
+ ```ruby
148
+ einstein = User.find_by_name('Albert')
149
+ # Just pass arguments as array or hash
150
+ einstein.salute(p_name: einstein.name) # 'Hello, Albert!'
151
+ einstein.salute([einstein.surname]) # 'Hello, Einstein!'
152
+ ```
153
+
154
+ Support
155
+ -------
156
+
157
+ Feel free to contact me at fg@flashgordon.ru or send a pull request.
@@ -0,0 +1,78 @@
1
+ require 'active_record/connection_adapters/oracle_enhanced_adapter'
2
+ require 'plsql/pipelined_function'
3
+
4
+ class ActiveRecord::StatementInvalid
5
+ attr_reader :original_exception
6
+
7
+ def initialize(message, original_exception)
8
+ @original_exception = original_exception
9
+ super(message)
10
+ end
11
+ end
12
+
13
+ module ActiveRecord
14
+ module ConnectionAdapters
15
+ # interface independent methods
16
+ class OracleEnhancedAdapter
17
+ def columns_without_cache_with_pipelined(table, name)
18
+ begin
19
+ return columns_without_cache_without_pipelined(table, name)
20
+ rescue OracleEnhancedConnectionException => error
21
+ # Will try to find a pipelined function
22
+ end
23
+
24
+ function_name, package_name = parse_function_name(table)
25
+
26
+ if package_name
27
+ function = plsql.send(package_name.downcase.to_sym)[function_name.downcase]
28
+ else
29
+ raise error.class, error.message
30
+ end
31
+
32
+ if function
33
+ arguments_metadata = function.arguments[0].sort_by {|arg| arg[1][:position]}
34
+ arguments = arguments_metadata.map do |(arg_name, argument)|
35
+ OracleEnhancedColumn.new(arg_name.to_s, nil, argument[:data_type], table)
36
+ end
37
+
38
+ return_columns = function.return[:element][:fields].sort_by {|(col_name, col)| col[:position]}.map do |(col_name, metadata)|
39
+ metadata.merge(name: col_name)
40
+ end
41
+
42
+ return_columns.map do |col|
43
+ OracleEnhancedColumn.new(col[:name].to_s, nil, col[:data_type], table)
44
+ end + arguments
45
+ else
46
+ raise error.class, error.message
47
+ end
48
+ end
49
+
50
+ alias_method_chain :columns_without_cache, :pipelined
51
+
52
+ def parse_function_name(name)
53
+ name = name.to_s.upcase
54
+ # We can get name of function with calling syntax
55
+ # Just extract function name
56
+ if name =~ /\ATABLE\((([^.]+\.)[^.]+)\([^)]+\)\)\z/
57
+ name = $1
58
+ end
59
+ name.split('.').reverse
60
+ end
61
+
62
+ protected
63
+
64
+ def translate_exception(exception, message) #:nodoc:
65
+ case @connection.error_code(exception)
66
+ when 1
67
+ RecordNotUnique.new(message, exception)
68
+ when 2291
69
+ InvalidForeignKey.new(message, exception)
70
+ when 20000..20999 # Skip user-defined errors
71
+ raise
72
+ else
73
+ ActiveRecord::StatementInvalid.new(message, exception)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,7 @@
1
+ module ActiveRecord::PLSQL
2
+ class Base < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ include Pipelined
5
+ include ProcedureMethods
6
+ end
7
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_record/plsql/pipelined'
2
+ require 'active_record/plsql/procedure_methods'
3
+ require 'active_record/plsql/base'
4
+ require 'active_record/plsql/pipelined_relation'
5
+ require 'active_record/oracle_enhanced_adapter_patch'
6
+ require 'plsql/log_subscriber'
7
+
8
+ module ActiveRecord::PLSQL
9
+ class Engine < ::Rails::Engine
10
+ initializer 'plsql.logger', after: 'active_record.logger' do
11
+ PLSQL::LogSubscriber.logger = ActiveRecord::Base.logger
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,137 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveRecord::PLSQL
4
+ module Pipelined
5
+ extend ActiveSupport::Concern
6
+
7
+ class PipelinedFunctionError < ActiveRecord::ActiveRecordError; end
8
+
9
+ class PipelinedFunctionTableName < Arel::Nodes::SqlLiteral
10
+ def to_s;self end
11
+ end
12
+
13
+ included do
14
+ self.pipelined_function = nil
15
+ end
16
+
17
+ module ClassMethods
18
+ def pipelined_arguments
19
+ raise PipelinedFunctionError, "Pipelined function didn't set" unless pipelined?
20
+ @pipelined_arguments ||= get_pipelined_arguments
21
+ end
22
+
23
+ def pipelined_arguments_names
24
+ pipelined_arguments.map(&:name)
25
+ end
26
+
27
+ def pipelined_function
28
+ @pipelined_function
29
+ end
30
+
31
+ alias pipelined? pipelined_function
32
+
33
+ def pipelined_function=(function)
34
+ case function
35
+ when String, Symbol
36
+ # Name without schema expected
37
+ function_name = function.to_s.split('.').map(&:downcase).map(&:to_sym)
38
+ case function_name.size
39
+ when 2
40
+ pipelined_function = plsql.send(function_name.first)[function_name.second]
41
+ when 1
42
+ pipelined_function = PLSQL::PipelinedFunction.find(plsql, function_name.first)
43
+ else
44
+ raise ArgumentError, 'Setting schema via string not supported yed'
45
+ end
46
+ raise ArgumentError, 'Pipelined function not found by string: %s' % function unless pipelined_function
47
+ when ::PLSQL::PipelinedFunction, nil
48
+ pipelined_function = function
49
+ else
50
+ raise ArgumentError, 'Unsupported type of pipelined function: %s' % function.inspect
51
+ end
52
+
53
+ if pipelined_function && pipelined_function.overloaded?
54
+ raise ArgumentError, 'Overloaded functions are not supported yet'
55
+ end
56
+
57
+ @pipelined_function = pipelined_function
58
+ @pipelined_arguments = nil
59
+ @table_name = pipelined_function_name if @pipelined_function
60
+ end
61
+
62
+ def pipelined_function_name
63
+ return @full_function_name if defined? @full_function_name
64
+ package_name, function_name = @pipelined_function.package, @pipelined_function.procedure
65
+ @full_function_name = [package_name, function_name].compact.join('.')
66
+ end
67
+
68
+ def arel_table
69
+ if pipelined?
70
+ @arel_table ||= Arel::Table.new(table_name_with_arguments, engine: arel_engine, as: pipelined_function_alias)
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ def pipelined_function_alias
77
+ # GET_USER_BY_NAME => GUBN
78
+ @pipelined_function.procedure.scan(/^\w|_\w/).join('').gsub('_', '')
79
+ end
80
+
81
+ def table_name_with_arguments
82
+ @table_name_with_arguments ||= PipelinedFunctionTableName.new(
83
+ "TABLE(%s(%s))" % [table_name, pipelined_arguments.map{|a| ":#{a.name}"}.join(',')]
84
+ )
85
+ end
86
+
87
+ def table_exist?
88
+ pipelined? || super
89
+ end
90
+
91
+ private
92
+
93
+ def get_pipelined_arguments
94
+ # Always select arguments of first function (overloading not supported)
95
+ arguments_metadata = pipelined_function.arguments[0].sort_by {|arg| arg[1][:position]}
96
+ arguments_metadata.map do |(name, argument)|
97
+ ActiveRecord::ConnectionAdapters::OracleEnhancedColumn.new(name.to_s, nil, argument[:data_type], pipelined_function_name)
98
+ end
99
+ end
100
+
101
+ def relation
102
+ return super unless pipelined?
103
+ @relation ||= PipelinedRelation.new(self, arel_table)
104
+ end
105
+ end
106
+
107
+ delegate :pipelined?, to: 'self.class'
108
+
109
+ attr_accessor :found_by_arguments
110
+
111
+ def reload(options = nil)
112
+ return super unless pipelined? && (found_by_arguments.present? || options)
113
+
114
+ clear_aggregation_cache
115
+ clear_association_cache
116
+
117
+ ActiveRecord::IdentityMap.without do
118
+ fresh_object = self.class.unscoped do
119
+ relation = self.class.where(self.class.primary_key => id)
120
+
121
+ if found_by_arguments
122
+ relation.bind_values += found_by_arguments
123
+ relation.to_a.first
124
+ else
125
+ relation.where(options).to_a.first
126
+ end
127
+ end
128
+
129
+ @attributes.update(fresh_object.instance_variable_get('@attributes'))
130
+ end
131
+
132
+ @attributes_cache = {}
133
+ self
134
+ end
135
+
136
+ end
137
+ end
@@ -0,0 +1,93 @@
1
+ module ActiveRecord::PLSQL
2
+ class PipelinedRelation < ActiveRecord::Relation
3
+ attr_accessor :pipelined_arguments_values
4
+
5
+ def where(opts, *rest)
6
+ return super unless @klass.pipelined? && pipelined_arguments.any?
7
+
8
+ pipelined_args = pipelined_arguments_names.map(&:to_sym)
9
+ opts = normalize_arguments_conditions(opts, pipelined_args)
10
+ return super if opts.blank?
11
+
12
+ relation = bind_pipelined_arguments(opts)
13
+
14
+ opts.reject! {|k| pipelined_args.include?(k)}
15
+ # bind rest of arguments
16
+ relation.where_values += build_where(opts, rest)
17
+ relation
18
+ end
19
+
20
+ def bind_pipelined_arguments(values)
21
+ relation = clone
22
+ if values.is_a?(Hash)
23
+ arguments_values = values.values_at(*pipelined_arguments_names.map(&:to_sym))
24
+ relation.bind_values += pipelined_arguments.zip(arguments_values)
25
+ else
26
+ relation.where_values += values
27
+ end
28
+ relation
29
+ end
30
+
31
+ def merge_pipelined_arguments(pos, values)
32
+ new_values_pos = pos + pipelined_arguments.size
33
+ exist_args = values[pos...new_values_pos]
34
+ new_values = values[new_values_pos..-1]
35
+ new_args_pos = pipelined_arguments_binds_pos(new_values)
36
+ # return if there are no new argument values
37
+ return unless new_args_pos
38
+ new_arguments = new_values[new_args_pos...(new_args_pos + pipelined_arguments.size)]
39
+ # overriding nil arguments
40
+ new_arguments.each_with_index {|val, idx| exist_args[idx][1] ||= val[1]}
41
+ # exclude new arguments
42
+ values[(new_values_pos + new_args_pos)...(new_values_pos + new_args_pos + pipelined_arguments.size)] = nil
43
+ # drop nil
44
+ values.compact!
45
+ end
46
+
47
+ def pipelined_arguments_binds_pos(binds = @bind_values)
48
+ binds.index {|(col,_)| col.name == pipelined_arguments.first.name}
49
+ end
50
+
51
+ # Safe arguments binding
52
+ def bind_values=(vals)
53
+ if @klass.pipelined? && (pos = pipelined_arguments_binds_pos)
54
+ vals = vals.map(&:dup)
55
+ merge_pipelined_arguments(pos, vals)
56
+ super(vals)
57
+ else
58
+ super
59
+ end
60
+ end
61
+
62
+ def exec_queries
63
+ return super unless @klass.pipelined? && pipelined_arguments.any?
64
+ return @records if loaded?
65
+ super
66
+ return @records if @records.empty?
67
+
68
+ pos = pipelined_arguments_binds_pos
69
+ found_by_arguments = @bind_values[pos...(pos + pipelined_arguments.size)]
70
+ # save arguments for easy reloading
71
+ @records.each {|record| record.found_by_arguments = found_by_arguments}
72
+ @records
73
+ end
74
+
75
+ protected
76
+
77
+ def normalize_arguments_conditions(opts, args)
78
+ case opts
79
+ when Hash
80
+ opts.symbolize_keys
81
+ when Arel::Nodes::Equality
82
+ column = opts.left.name.to_sym
83
+
84
+ # only simple types for a while
85
+ if args.include?(column) && !opts.right.is_a?(Arel::Attributes::Attribute)
86
+ {column => opts.right}
87
+ end
88
+ else
89
+ [opts]
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,154 @@
1
+ require 'active_support/concern'
2
+
3
+ module ActiveRecord::PLSQL
4
+ module ProcedureMethods
5
+ extend ActiveSupport::Concern
6
+
7
+ class CannotFetchId < StandardError; end
8
+
9
+ included do
10
+ class_attribute :plsql_package, :procedure_methods_cache, instance_writer: false
11
+ self.plsql_package = nil
12
+ self.procedure_methods_cache = Hash.new do |cache, klass|
13
+ cache[klass] = Hash.new do |methods, method|
14
+ # Inherits procedure methods from base class
15
+ if klass.superclass.respond_to?(:procedure_methods)
16
+ methods[method] = klass.superclass.procedure_methods[method]
17
+ else
18
+ nil
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def set_create_procedure(procedure, options = {}, &reload_block)
26
+ block ||= proc do |record, result|
27
+ case result
28
+ when Hash
29
+ record.id = result.values.first
30
+ when Numeric
31
+ record.id = result
32
+ else
33
+ raise CannotFetchId, "Couldn't fetch primary key from create procedure (%s) result: %s" %
34
+ [procedure, result.inspect]
35
+ end
36
+
37
+ reload_block ? reload_block.call(record) : record.reload
38
+
39
+ record.instance_variable_set(:@new_record, true)
40
+ record.id
41
+ end
42
+
43
+ procedure_method(:create, procedure, options, &block)
44
+ set_create_method {call_procedure_method(:create)}
45
+ end
46
+
47
+ def set_update_procedure(procedure, options = {})
48
+ procedure_method(:update, procedure, options) do |record|
49
+ record.reload
50
+ record.id
51
+ end
52
+ set_update_method {call_procedure_method(:update)}
53
+ end
54
+
55
+ def set_destroy_procedure(procedure, options = {})
56
+ procedure_method(:destroy, procedure, options)
57
+ set_delete_method {call_procedure_method(:destroy)}
58
+ end
59
+
60
+ def procedure_methods
61
+ procedure_methods_cache[self]
62
+ end
63
+
64
+ def procedure_method(method, procedure_name = method, options = {}, &block)
65
+ procedure = if PLSQL::Procedure === procedure_name
66
+ procedure_name
67
+ else
68
+ find_procedure(procedure_name)
69
+ end
70
+
71
+ # Raise error if procedure not found
72
+ raise ArgumentError, "Procedure (%s) not found for method (%s)" % [procedure_name, method] unless procedure
73
+
74
+ procedure_methods[method] = {procedure: procedure, options: options, block: block}
75
+
76
+ unless (instance_methods + private_instance_methods).find {|m| m == method}
77
+ generated_feature_methods.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
78
+ def #{method}(arguments = {}, options = {})
79
+ call_procedure_method(:#{method}, arguments, options)
80
+ end
81
+ RUBY
82
+ end
83
+ end
84
+
85
+ def procedures_arguments
86
+ @procedures_arguments ||= Hash.new do |cache, procedure|
87
+ # Always select arguments of first function (overloading not supported)
88
+ cache[procedure] = Hash[ procedure.arguments[0].sort_by {|arg| arg[1][:position]} ]
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def find_procedure(procedure_name)
95
+ procedure_name = procedure_name.to_s.split('.').compact
96
+
97
+ case procedure_name.size
98
+ when 2
99
+ plsql.send(procedure_name[0].to_sym)[procedure_name[1]]
100
+ when 1
101
+ if plsql_package
102
+ plsql_package[procedure_name[0]] || PLSQL::Procedure.find(plsql, procedure_name[0])
103
+ else
104
+ PLSQL::Procedure.find(plsql, procedure_name[0])
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ delegate :procedures_arguments, :procedure_methods, to: 'self.class'
111
+
112
+ private
113
+
114
+ def call_procedure_method(method, arguments = {}, opts = {})
115
+ procedure, options, block = procedure_methods[method].values_at(:procedure, :options, :block)
116
+ options = options.merge(opts)
117
+
118
+ if options[:arguments]
119
+ if arguments.is_a?(Hash)
120
+ arguments = arguments.merge(instance_exec(&options[:arguments]))
121
+ else
122
+ arguments += options[:arguments]
123
+ end
124
+ end
125
+
126
+ options[:arguments] = arguments
127
+ call_procedure(procedure, options, &block)
128
+ end
129
+
130
+ def call_procedure(procedure, options = {})
131
+ result = procedure.exec(*get_procedure_arguments(procedure, options))
132
+ if block_given?
133
+ yield(self, result)
134
+ else
135
+ result
136
+ end
137
+ end
138
+
139
+ def get_procedure_arguments(procedure, options)
140
+ arguments = options[:arguments]
141
+ arguments = arguments.dup if arguments.duplicable?
142
+
143
+ if Hash === arguments
144
+ arguments.symbolize_keys!
145
+ arguments_metadata = procedures_arguments[procedure]
146
+ # throw away unnecessary arguments
147
+ [arguments.select {|k,_| arguments_metadata[k]}]
148
+ else
149
+ arguments
150
+ end
151
+ end
152
+ end
153
+ end
154
+
@@ -0,0 +1,27 @@
1
+ class OracleNamedError < StandardError
2
+ class_attribute :error_code, instance_writer: false
3
+
4
+ UNHANDLED_ERROR = 6512
5
+
6
+ class << self
7
+ def ===(error)
8
+ error = error.original_exception if error.respond_to?(:original_exception)
9
+ error = error.cause if error.respond_to?(:cause) && error.cause
10
+
11
+ Java::JavaSql::SQLException === error &&
12
+ (error.get_error_code.in?([*error_code]) ||
13
+ # ORA-06512: at line 1
14
+ # ORA-20100: some exception description <--- real exception code in the second line
15
+ error.get_error_code == UNHANDLED_ERROR &&
16
+ error.message.split("\n")[1].try(:[], /\AORA-(\d+)/, 1).try(:to_i).in?([*error_code]))
17
+ end
18
+
19
+ def define_exception(class_name, error_code)
20
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
21
+ class ::#{class_name} < OracleNamedError
22
+ self.error_code = #{error_code}
23
+ end
24
+ RUBY
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ class OCI8
2
+ class OCINamedError < OCIError
3
+ class_attribute :oci_error_code, instance_writer: false
4
+
5
+ UNHANDLED_ERROR = 6512
6
+
7
+ class << self
8
+ alias error_code= oci_error_code=
9
+
10
+ def error_code
11
+ oci_error_code
12
+ end
13
+
14
+ def ===(error)
15
+ error = error.original_exception if error.respond_to?(:original_exception)
16
+ OCIError === error &&
17
+ (error.code.in?([*error_code]) ||
18
+ # ORA-06512: at line 1
19
+ # ORA-20100: some exception description <--- real exception code in the second line
20
+ error.code == UNHANDLED_ERROR &&
21
+ error.message.split("\n")[1].try(:[], /\AORA-(\d+)/, 1).try(:to_i).in?([*error_code]))
22
+ end
23
+
24
+ def define_exception(class_name, error_code)
25
+ class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
26
+ class ::#{class_name} < OCI8::OCINamedError
27
+ self.error_code = #{error_code}
28
+ end
29
+ RUBY
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,9 @@
1
+ module Oracle; end
2
+
3
+ if RUBY_ENGINE == 'ruby'
4
+ require 'oci8/oci_named_error'
5
+ Oracle::NamedError = OCI8::OCINamedError
6
+ elsif RUBY_ENGINE == 'jruby'
7
+ require 'java/oracle_sql_named_error'
8
+ Oracle::NamedError = OracleNamedError
9
+ end
@@ -0,0 +1,40 @@
1
+ module PLSQL
2
+ class LogSubscriber < ActiveSupport::LogSubscriber
3
+ def procedure_call(event)
4
+ return unless logger && (logger.debug? || uncaught_exception?(event.payload[:error]))
5
+ payload = event.payload
6
+ name = 'PL/SQL Procedure call (%.1fms)' % event.duration
7
+ sql = payload[:sql].strip
8
+
9
+ if payload[:arguments].empty?
10
+ arguments = nil
11
+ elsif payload[:arguments].size == 1 && Hash === payload[:arguments].first
12
+ arguments = ' ' + payload[:arguments].first.inspect
13
+ else
14
+ arguments = ' ' + payload[:arguments].inspect
15
+ end
16
+
17
+ if event.payload[:error]
18
+ exception = "Error occurred: %s\n%s" %
19
+ [event.payload[:error].class, event.payload[:error].message.split("\n").map{|l| " #{l}"}.join("\n")]
20
+
21
+ name = color(name, RED, true)
22
+ exception = color(exception, RED, true)
23
+ sql = color(sql, nil, true)
24
+
25
+ error " #{name} #{sql}#{arguments}\n #{exception}"
26
+ else
27
+ name = color(name, YELLOW, true)
28
+ sql = color(sql, nil, true)
29
+
30
+ debug " #{name} #{sql}#{arguments}"
31
+ end
32
+ end
33
+
34
+ def uncaught_exception?(error)
35
+ error && OCIError === error && !error.code.in?(20000..20999)
36
+ end
37
+ end
38
+ end
39
+
40
+ PLSQL::LogSubscriber.attach_to :plsql
@@ -0,0 +1,9 @@
1
+ if RUBY_ENGINE == 'ruby'
2
+ require 'oci8'
3
+ end
4
+ require 'active_record'
5
+ require 'activerecord-oracle_enhanced-adapter'
6
+ require 'ruby-plsql'
7
+ require 'oracle/named_error'
8
+ require 'rails/engine'
9
+ require 'active_record/plsql/engine'
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-plsql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.5
5
+ platform: ruby
6
+ authors:
7
+ - Nikita Shilnikov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: flash-gordons-ruby-plsql
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '='
18
+ - !ruby/object:Gem::Version
19
+ version: 0.5.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '='
25
+ - !ruby/object:Gem::Version
26
+ version: 0.5.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '='
39
+ - !ruby/object:Gem::Version
40
+ version: 3.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: activerecord-oracle_enhanced-adapter
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 1.4.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 1.4.0
55
+ description: rails-plsql adds functional that allows to use some special Oracle Database
56
+ features in standard ActiveRecord models.
57
+ email:
58
+ - fg@flashgordon.ru
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - MIT-LICENSE
64
+ - README.md
65
+ - lib/active_record/oracle_enhanced_adapter_patch.rb
66
+ - lib/active_record/plsql/base.rb
67
+ - lib/active_record/plsql/engine.rb
68
+ - lib/active_record/plsql/pipelined.rb
69
+ - lib/active_record/plsql/pipelined_relation.rb
70
+ - lib/active_record/plsql/procedure_methods.rb
71
+ - lib/java/oracle_sql_named_error.rb
72
+ - lib/oci8/oci_named_error.rb
73
+ - lib/oracle/named_error.rb
74
+ - lib/plsql/log_subscriber.rb
75
+ - lib/rails-plsql.rb
76
+ homepage: http://github.com/flash-gordon/rails-plsql
77
+ licenses:
78
+ - MIT
79
+ metadata: {}
80
+ post_install_message:
81
+ rdoc_options: []
82
+ require_paths:
83
+ - lib
84
+ required_ruby_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ! '>='
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ required_rubygems_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 2.2.2
97
+ signing_key:
98
+ specification_version: 4
99
+ summary: Extension for ActiveRecord that provides convenient using some of Oracle
100
+ PL/SQL features.
101
+ test_files: []