uses-store-procedures 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Copyright 2013 Leopold O'Donnell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,133 @@
1
+ [![Build Status](https://travis-ci.org/leopoldodonnell/uses-stored-procedures.png?branch=master)](https://travis-ci.org/leopoldodonnell/uses-stored-procedures)
2
+ [![Dependency Status](https://gemnasium.com/leopoldodonnell/uses-stored-procedures.png)](https://gemnasium.com/leopoldodonnell/uses-stored-procedures)
3
+ [![Code Climate](https://codeclimate.com/github/leopoldodonnell/uses-stored-procedures.png)](https://codeclimate.com/github/leopoldodonnell/uses-stored-procedures)
4
+
5
+ # UsesStoredProcedures
6
+
7
+ Extends ActiveRecord with the ability to use SQL Stored Procedures in Rails.
8
+
9
+ So, you really need to use stored procedures in your Rails application. This gem extends ActiveRecord with a class method to add to your models or services. Sure stored procedures are *not the Rails way* they are an occasional project necessity. Your reasons may stem from a need to integrate with a legacy database with built-in business rules, or a need to improve performance where other avenues have been exhausted (given available project time). If you need this, *you know who you are*, I don't need to tell you why.
10
+
11
+ ## Version
12
+
13
+ 0.1.0
14
+
15
+ ## Requirements
16
+
17
+ ### Ruby
18
+
19
+ - 1.8.7, 1.9
20
+
21
+ ### Rails
22
+
23
+ - 3.1.x or 3.2.x
24
+
25
+ ### ORM
26
+
27
+ - ActiveRecord
28
+ - Mysql2, MySQL (Tested/Passing)
29
+ - SQLServer (Untested, but should work)
30
+ - PostgreSQL (Untested, unsure - please try it and notify the author)
31
+
32
+ ## Installation
33
+
34
+ The current state of this gem is tested with MySQL, but it also does detection of SQLServer and PostgreSQL which will
35
+ be tested when time permits. Please contact the author if you've gotten it to work before this happens.
36
+
37
+ In your Gemfile
38
+
39
+ gem 'uses-stored-procedures', git => "git://github.com/leopoldodonnell/uses-stored-procedures"
40
+
41
+ Then from your project root:
42
+
43
+ > bundle
44
+
45
+
46
+ ## Using uses_stored_procedures to call stored procedures from Rails
47
+
48
+ **UsesStoredProcedues** extends ActiveRecord::Base with the single class method that takes the name of the procedure as a symbol, and optional parameters to generate a class and instance method that can be called to run the stored procedure.
49
+
50
+ Here's a simple example:
51
+
52
+ ```ruby
53
+ class MyClass < ActiveRecord::Base
54
+ use_stored_proc :get_office_by_country {|row| "#{row.country} #{row.city}, phone: #{row.phone}"}
55
+ end
56
+ ```
57
+
58
+ Which generates:
59
+
60
+ ```ruby
61
+ def self.get_office_by_country(*args, &block)
62
+ # code
63
+ end
64
+
65
+ def get_office_by_country(*args, &block)
66
+ self.class.get_offic_by_country(args, block)
67
+ end
68
+ ```
69
+
70
+ So later you can do the following:
71
+
72
+ ```ruby
73
+ result = MyClass.get_office_by_country 'Canada'
74
+ result.each {|item| puts item}
75
+ ```
76
+
77
+ Note that the object passed to the block is a *UsesStoredProcedures#HashWithAttributes* instance that provides accessors for all of its keys.
78
+
79
+ ### Syntax
80
+
81
+ The method signature for *use_stored_proc* is:
82
+
83
+ ```ruby
84
+ def self.use_stored_proc(method_name, *options, &block)
85
+ ```
86
+
87
+ That then generates a class and instance method named *method_name*.
88
+
89
+ * method_name - is the symbolic name of the stored procedure and is the name given to the generated class and instance methods. By default, the stored procedure that is called will be a stringified version of this method_name. Use the :proc_name option if the method_name won't be matching actual stored procedure name.
90
+
91
+ * options - is an array of arguments that are provided as *:arg_name => value* and where the last argument may be a block, or method name.
92
+
93
+ ####Options
94
+
95
+ * proc_name - if the stored procedure name is different than method_name parameter, provide a string that matches the actual stored procedure name.
96
+
97
+ * filter - the name of a class method that receives an instance of *UsesStoredProcedures#HashWithAttributes* to map returned rows.
98
+
99
+ * block - make the last parameter a block that receive an instance of *UsesStoredProcedures#HashWithAttributes* to map returned rows.
100
+
101
+ #### Generated Methods
102
+
103
+ The generated methods all take a variable number of parameters that are mapped to the stored procedure call and can also take a block that can be used to map the returned rows. By default the returned value will be an array of values that are either instances of *UsesStoredProcedures#HashWithAttributes*, or mapped by the block. Note that passing a block overrides the block or method provided when calling *#use_stored_proc*
104
+
105
+ ### Another Example
106
+
107
+ ```ruby
108
+ require 'uses_stored_procedures'
109
+
110
+ class ClientServices
111
+ include UsesStoredProcedures
112
+
113
+ use_stored_proc :list_inactive_clients, :proc_name => 'GET_CLIENTS_INACTIVE_STATUS', :filter => :make_clients
114
+
115
+ def self.make_clients(item)
116
+ Client.new item
117
+ end
118
+ end
119
+
120
+ client_services = ClientServices.new
121
+
122
+ # ...
123
+
124
+ client_list = client_services.list_inactive_clients(first_of_year)
125
+
126
+ # and so on ...
127
+ ```
128
+
129
+ ### License
130
+
131
+ MIT License. Copyright 2013 Leopold O'Donnell
132
+
133
+
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env rake
2
+ require 'rspec/core/rake_task'
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+ begin
9
+ require 'rdoc/task'
10
+ rescue LoadError
11
+ require 'rdoc/rdoc'
12
+ require 'rake/rdoctask'
13
+ RDoc::Task = Rake::RDocTask
14
+ end
15
+
16
+ RDoc::Task.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.title = 'UsesStoredProcedures'
19
+ rdoc.options << '--line-numbers'
20
+ rdoc.rdoc_files.include('README.rdoc')
21
+ rdoc.rdoc_files.include('lib/**/*.rb')
22
+ end
23
+
24
+ Bundler::GemHelper.install_tasks
25
+
26
+ desc "Run all specs"
27
+ RSpec::Core::RakeTask.new(:spec)
28
+
29
+ task :default => :spec
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :uses-store-procedures do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,107 @@
1
+ require 'uses_stored_procedures/hash_with_attributes'
2
+
3
+ ##
4
+ # UsesStoredProcedures provides that ability to extend a class with
5
+ # the ability to call stored_procedures providing an array of hash
6
+ # entries that can be mapped to an array of other objects.
7
+ #
8
+ module UsesStoredProcedures
9
+ extend ActiveSupport::Concern
10
+
11
+ module ClassMethods
12
+ ##
13
+ # Add a stored procedure that can ba called on a class or a
14
+ # class instance.
15
+ #
16
+ # @param [Symbol] name is the symbolic name of the method to be created.
17
+ # By default, it is assumed that the stored procedure has the same
18
+ # name as a string.
19
+ #
20
+ # @param args is a variable list of optional arguments. Available Options
21
+ # include:
22
+ #
23
+ # * :proc_name - is the name of the stored procedure. Use this if the
24
+ # name doesn't match the name paramter
25
+ # * :filter - is a class method used to filter the return results. The
26
+ # method should take a HashWithAttributes instance and return an object
27
+ # for the array entry
28
+ #
29
+ # @param [Proc] block - is a block that takes a HashWithAttributes instance
30
+ # and returns an object for the array entry.
31
+ #
32
+ # @raises if the current SQL adapter isn't supported.
33
+ #
34
+ def uses_stored_proc(name, *args, &block)
35
+ options = args.extract_options!
36
+ proc_name = options[:proc_name] || name.to_s
37
+
38
+ # Install the row mapper block or method
39
+ self.install_stored_proc_methods(name, proc_name, call_stored_proc_verb, options[:filter] || block)
40
+ end
41
+
42
+
43
+ # Return the SQL verb for calling a stored procedure.
44
+ def call_stored_proc_verb()
45
+ case ActiveRecord::Base.connection.adapter_name.to_sym
46
+ when :Mysql2, :MySQL
47
+ 'call'
48
+ when :PostgreSQL
49
+ 'select'
50
+ when :SQLServer
51
+ 'exec'
52
+ else
53
+ raise "uses_stored_procedurs does not support your connection adapter"
54
+ end
55
+ end
56
+
57
+ # Define the class and instance methods. Note the need to
58
+ # create a singleton class instance to create the stored
59
+ # procedure class method.
60
+ def install_stored_proc_methods(name, proc_name, call_stored_proc_verb, filter) #:nodoc:
61
+ define_method name do |*args|
62
+ self.class.exec_stored_proc(proc_name, call_stored_proc_verb, filter, args)
63
+ end
64
+
65
+ singleton_class = class << self; self; end
66
+ singleton_class.send(:define_method, name) do |*args|
67
+ exec_stored_proc(proc_name, call_stored_proc_verb, filter, args)
68
+ end
69
+
70
+ end
71
+
72
+ # Run the stored procedure with the arguments provided and send
73
+ # them through a filter method or block if specified.
74
+ def exec_stored_proc(name, call_stored_proc_verb, filter, args) #:nodoc:
75
+ sql = "#{call_stored_proc_verb} #{name} (" + args.map {|a| "'#{a}'"}.join(',') + ")"
76
+
77
+ # Call the stored procedure. Note the need to reset the connection
78
+ # because the mysql connection tends to hangup when stored procedures
79
+ # are called. Note that testing for ::connected? doesn't work.
80
+ #
81
+ # TODO: investigate why stored procedures cause connection hangups.
82
+ records = ActiveRecord::Base.connection.select_all(sql)
83
+ ActiveRecord::Base.connection.reconnect!
84
+
85
+ filter_stored_proc_results(records, filter)
86
+ end
87
+
88
+ private
89
+ def filter_stored_proc_results(results, filter)
90
+ case filter
91
+ when Symbol
92
+ # Map with a member function
93
+ results.map do |r| self.send(filter.to_s, HashWithAttributes.new(r)) end
94
+ when Proc
95
+ # Map with a block
96
+ results.map do |r| filter.call(HashWithAttributes.new(r)) end
97
+ else
98
+ # Just map it
99
+ results.map do |r| HashWithAttributes.new r end
100
+ end
101
+ end
102
+
103
+ end
104
+ end
105
+
106
+ require 'uses_stored_procedures/railtie'
107
+
@@ -0,0 +1,68 @@
1
+ module UsesStoredProcedures
2
+ ##
3
+ # HashWithAttributes augments Hash with getters and setters on
4
+ # an as needed basis.
5
+ #
6
+ # When ever an instance of HashWithAttributes is sent an accessor
7
+ # message, or a respond_to? message that matches a hash key for
8
+ # the first time, a setter and getter is created for that key which
9
+ # may be either a symbol or a string.
10
+ #
11
+ # @example
12
+ #
13
+ # h = HashWithAttributes.new
14
+ # h[:one] = 1
15
+ # h['two'] = 2
16
+ # h.one = 3
17
+ # h.two = 4
18
+ # puts "One is #{h.one} and Two is #{h.two}"
19
+ # # > One is 3 and Two is 4
20
+ #
21
+ class HashWithAttributes < Hash
22
+
23
+ ##
24
+ # Create a new HashWithAttributes.
25
+ #
26
+ # @param [Object] hash_or_obj will initialize the hash
27
+ # from another hash, or work like Hash where the object
28
+ # is the default for empty entries.
29
+ #
30
+ def initialize(hash_or_obj = nil)
31
+ if hash_or_obj && hash_or_obj.kind_of?(Hash)
32
+ super()
33
+ self.merge! hash_or_obj
34
+ else
35
+ super
36
+ end
37
+ end
38
+
39
+ def method_missing(name, *args, &block)
40
+ if check_and_create_accessors(name)
41
+ args.length > 0 ? send(name.to_s, args.first) : send(name.to_s)
42
+ else
43
+ super
44
+ end
45
+ end
46
+
47
+ def respond_to?(symbol, include_private = false)
48
+ super || check_and_create_accessors(symbol)
49
+ end
50
+
51
+ private
52
+ def check_and_create_accessors(name)
53
+ item_name = name.to_s.chomp '='
54
+ if has_key?(item_name) || has_key?(item_name.to_sym)
55
+ self.class.create_accessors(item_name)
56
+ return true
57
+ end
58
+ false
59
+ end
60
+
61
+ def self.create_accessors(accessor)
62
+ define_method(accessor) { fetch accessor }
63
+ define_method("#{accessor}=") { |v| store(accessor, v) }
64
+ true
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,10 @@
1
+ require 'active_record/railtie'
2
+ require 'active_support/core_ext'
3
+
4
+ module UsesStoredProcedures
5
+ class Railtie < Rails::Railtie
6
+ if defined?(ActiveRecord::Base)
7
+ ActiveRecord::Base.send :include, UsesStoredProcedures
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module UsesStoreProcedures
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: uses-store-procedures
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Leo O'Donnell
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-01 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.12
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 3.2.12
30
+ description: ! 'So, you really need to use stored procedures in your Rails application.
31
+ This gem extends
32
+
33
+ ActiveRecord with a class method to add to your models or services.
34
+
35
+ '
36
+ email:
37
+ - leopold.odonnell@gmail.com
38
+ executables: []
39
+ extensions: []
40
+ extra_rdoc_files: []
41
+ files:
42
+ - lib/tasks/uses-stored-procedures_tasks.rake
43
+ - lib/uses_stored_procedures/hash_with_attributes.rb
44
+ - lib/uses_stored_procedures/railtie.rb
45
+ - lib/uses_stored_procedures/version.rb
46
+ - lib/uses_stored_procedures.rb
47
+ - MIT-LICENSE
48
+ - Rakefile
49
+ - README.md
50
+ homepage: https://github.com/leopoldodonnell/uses-stored-procedures
51
+ licenses: []
52
+ post_install_message:
53
+ rdoc_options: []
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubyforge_project:
70
+ rubygems_version: 1.8.24
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: Extend ActiveRecord with the ability to use Stored Procedures.
74
+ test_files: []
75
+ has_rdoc: