rails-plsql 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/MIT-LICENSE +7 -0
- data/README.md +157 -0
- data/lib/active_record/oracle_enhanced_adapter_patch.rb +78 -0
- data/lib/active_record/plsql/base.rb +7 -0
- data/lib/active_record/plsql/engine.rb +14 -0
- data/lib/active_record/plsql/pipelined.rb +137 -0
- data/lib/active_record/plsql/pipelined_relation.rb +93 -0
- data/lib/active_record/plsql/procedure_methods.rb +154 -0
- data/lib/java/oracle_sql_named_error.rb +27 -0
- data/lib/oci8/oci_named_error.rb +33 -0
- data/lib/oracle/named_error.rb +9 -0
- data/lib/plsql/log_subscriber.rb +40 -0
- data/lib/rails-plsql.rb +9 -0
- metadata +101 -0
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,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,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
|
data/lib/rails-plsql.rb
ADDED
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: []
|