mysql2_model 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,7 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ features/**/*.feature
4
+ -
5
+ CHANGELOG.md
6
+ VERSION
7
+ MIT-LICENSE
data/.gitignore ADDED
@@ -0,0 +1,28 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+ ._*
4
+
5
+ ## TEXTMATE
6
+ *.tmproj
7
+ tmtags
8
+
9
+ ## EMACS
10
+ *~
11
+ \#*
12
+ .\#*
13
+
14
+ ## VIM
15
+ *.swp
16
+
17
+ ## NETBEANS
18
+ nbproject
19
+
20
+ ## PROJECT::GENERAL
21
+ coverage
22
+ rdoc
23
+ pkg
24
+ .yardoc
25
+ doc
26
+
27
+ ## PROJECT::SPECIFIC
28
+ spec/repositories.yml
data/.yardopts ADDED
@@ -0,0 +1,2 @@
1
+ --no-private
2
+ -
data/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (September 2nd, 2010)
4
+ * First Gem Release
5
+ * Added Documentation
6
+
7
+ ## 0.0.5 (September 1st, 2010)
8
+ * Added naive Date/Time Coercion (Be vewy vewy wary)
9
+ * Added support for a Logger provided by Logging
10
+ * Added support for sending the same query to multiple repositories and aggregating the results.
11
+
12
+ ## 0.0.4 (August 30th, 2010)
13
+ * Refactored Mysql2Model::Client to use class instance instead of class variables.
14
+ * Refactored Mysql2Model::Config to use class instance instead of class variables.
15
+
16
+ ## 0.0.3 (August 28th, 2010)
17
+ * Fixed misspelling of "repository_path" in Mysql2Model::Config
18
+ * Added specs for all classes
19
+ * Added value method to Mysql2Model::Container
20
+ * Added interpolating values into the queries called "composing"
21
+
22
+ ## 0.0.2 (August 26th, 2010)
23
+ * First Working Version
24
+
25
+ ## 0.0.1 (August 25th, 2010)
26
+ * Project Started
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :gemcutter
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,30 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ mysql2_model (0.1.0)
5
+ activesupport (~> 2.3)
6
+ builder (~> 2.1.2)
7
+ logging (~> 1)
8
+ mysql2 (~> 0.2)
9
+
10
+ GEM
11
+ remote: http://rubygems.org/
12
+ specs:
13
+ activesupport (2.3.8)
14
+ builder (2.1.2)
15
+ little-plugger (1.1.2)
16
+ logging (1.4.3)
17
+ little-plugger (>= 1.1.2)
18
+ mysql2 (0.2.3)
19
+ rspec (1.3.0)
20
+
21
+ PLATFORMS
22
+ ruby
23
+
24
+ DEPENDENCIES
25
+ activesupport (~> 2.3)
26
+ builder (~> 2.1.2)
27
+ logging (~> 1)
28
+ mysql2 (~> 0.2)
29
+ mysql2_model!
30
+ rspec (~> 1.3)
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Donovan Bray "donnoman@donovanbray.com" http://github.com/donnoman
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.
data/README.rdoc ADDED
@@ -0,0 +1,170 @@
1
+ = mysql2_model the QRM (Query Resource Manager)
2
+
3
+ Provides a class suitable to be used as a model, that includes connection management, variable interpolation,
4
+ object coercion and helper methods to write concise MySQL statements.
5
+
6
+ This library is ideal for use when more adaptable, extensible, but perhaps less flexible ORM's present a significant
7
+ obstacle to writing and running the specific MySQL statements you need.
8
+
9
+ While the example given in this README is trivial, the most likely usage would be with
10
+ particularly gnarly business logic as you might need in generating analytics
11
+ where a typical ORM may yield numerous sub-optimal MySQL statements.
12
+
13
+ == Inspiration
14
+
15
+ This library was conceived when I wanted to solve a performance issue in a small Sinatra app that used a well established ORM.
16
+ I wanted to create a simple class that could act as a stand-in for the original object and directly utilize carefully crafted
17
+ MySQL statements. The process generated a 21mb (and growing) XML file, at initially at cost of over 300k queries, and 27 minutes.
18
+ The resulting Mysql2Model derived class was able to accomplish the same task in 56 seconds, with about 64 queries and dramatically
19
+ reduced the memory footprint.
20
+
21
+ == Usage
22
+
23
+ === Install It
24
+
25
+ gem install mysql2_model
26
+
27
+ === Require it
28
+
29
+ require 'mysql2_model'
30
+
31
+ === Create a yml structure to point to the databases
32
+
33
+ * See examples/repositories.yml
34
+
35
+ === Config it
36
+
37
+ Mysql2Model::Config.repository_path = 'config/repositories.yml'
38
+
39
+ === Create your model
40
+
41
+ class Mtdb
42
+ include Mysql2Model::Container
43
+
44
+ def self.all
45
+ query "SELECT id, database_name, db_server_host, db_server_port, db_server_user, db_server_password * FROM mtdbs"
46
+ end
47
+
48
+ def self.count
49
+ value("SELECT COUNT(*) FROM mtdbs")
50
+ end
51
+
52
+ # ? Mark substitution
53
+ def self.find_by_database_name_and_host(name,host)
54
+ query("SELECT * FROM mtdbs WHERE database_name = '?' and database_host = '?'",name,host)
55
+ end
56
+
57
+ # printf style
58
+ def self.find_by_host(host)
59
+ query("SELECT * FROM mtdbs WHERE database_host = '%s'",host)
60
+ end
61
+
62
+ # Named Binds
63
+ def self.arrange_by_custom_order(order,user)
64
+ query("SELECT * FROM mtdbs WHERE db_server_user = ':user' ORDER BY database_name :order, db_server_host :order", :order => order, :user => user)
65
+ end
66
+
67
+ def config_name
68
+ "mtdb#{id}".to_sym
69
+ end
70
+
71
+ def to_config
72
+ {
73
+ config_name => {
74
+ :host => db_server_host,
75
+ :database => database_name,
76
+ :port => db_server_port,
77
+ :username => db_server_user,
78
+ :password => db_server_password_unencrypted
79
+ }
80
+ }
81
+ end
82
+
83
+ def db_server_password_unencrypted
84
+ nil # You have to invent your own.
85
+ end
86
+
87
+ def self.default_repository_name # You don't need this if you want to use the default repo
88
+ :infrastructure
89
+ end
90
+
91
+ end
92
+
93
+ === Use it
94
+
95
+ Mtdb.all.each |mtdb|
96
+ puts "Database: #{mtdb.database_name}:" #direct method access
97
+ puts "Host: #{mtdb[:db_server_host]}" #direct member access
98
+ puts "Config: {mtdb.to_config.inspect}" #model methods
99
+ end
100
+
101
+ === Consume Multiple Repositories
102
+
103
+ # Assume :subscribers1 has 200 rows, :subscriber2 has 150, :subscriber3 has 50.
104
+
105
+ class Subscriber
106
+
107
+ include Mysql2Model::Container
108
+
109
+ def self.all
110
+ query("SELECT * FROM subscribers") # => resultset of 400 rows
111
+ end
112
+
113
+ def self.count
114
+ value_sum("SELECT COUNT(*) FROM subscribers") # => 400
115
+ end
116
+
117
+ def self.count_from_each
118
+ value("SELECT COUNT(*) FROM subscribers") # => [200,150,50]
119
+ end
120
+
121
+ def self.default_repository_name
122
+ [:subscribers1,:subscribers2,:subscribers3]
123
+ end
124
+
125
+ end
126
+
127
+ == Documentation
128
+
129
+ * http://rubydoc.info/github/donnoman/mysql2_model
130
+
131
+ == Troubleshooting
132
+
133
+ * {Mysql2Model issue tracker}[http://github.com/donnoman/mysql2_model/issues/]
134
+
135
+ == Note on Patches/Pull Requests
136
+
137
+ * Fork the project.
138
+ * Make your feature addition or bug fix.
139
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
140
+ * Commit, do not mess with rakefile, version, or changelog.
141
+ (if you want to have your own version, that is fine but bump version in a commit by itself so that I can ignore it when I pull)
142
+ * Send me a pull request. Bonus points for topic branches.
143
+
144
+ == Todo
145
+ * Improve coupling between classes
146
+ * Time formatting when using the composing pattern to seamlessly pass time objects to mysql2
147
+ * Support ActiveSupport::TimeWithZone?
148
+ * Compliance with Mysql2 time handling
149
+ * Incorporate Test,Production,Development environments into the repositories.yml
150
+ * Improve instantiating the results so that we can regain mysql2's lazy loading.
151
+ * Evented Connection Pools
152
+ * Evented Query Patterns
153
+ * Iterate in batches to allow more efficient garbage collection of large resultsets
154
+
155
+ == Similar Projects
156
+ * Sequel with the mysql2 adapter http://sequel.rubyforge.org
157
+ * ActiveRecord with the mysql2 Adapter http://github.com/brianmario/mysql2/blob/master/lib/active_record/connection_adapters/mysql2_adapter.rb
158
+ * Datamapper with mysql2 adapter http://datamapper.org
159
+ * RBatis is the port of iBatis to Ruby and Ruby on Rails. http://ibatis.apache.org/docs/ruby (Appears to be discontinued)
160
+
161
+ == Special Thanks
162
+
163
+ * Brian Lopez - Mysql2 Gem (http://github.com/brianmario/mysql2)
164
+
165
+ == Copyright
166
+
167
+ Copyright (c) 2010 Donovan Bray "donnoman@donovanbray.com" http://github.com/donnoman
168
+
169
+ See MIT-LICENSE for details.
170
+
data/Rakefile ADDED
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "mysql2_model"
8
+ gem.summary = %Q{Mysql2Model provides a container for creating model code based on MySQL Statements utilizing the Mysql2 client}
9
+ gem.description = %Q{Provides a class suitable to be used as a model, that includes connection management, variable interpolation, object coercion and helper methods to support using direct MySQL statements for database interaction.}
10
+ gem.email = "donnoman@donovanbray.com"
11
+ gem.homepage = "http://github.com/donnoman/mysql2_model"
12
+ gem.authors = ["donnoman"]
13
+ gem.add_runtime_dependency "mysql2", "~> 0.2"
14
+ gem.add_runtime_dependency "activesupport", "~> 2.3"
15
+ gem.add_runtime_dependency 'builder', '~> 2.1.2' #Not needed if using entire active_support
16
+ gem.add_runtime_dependency 'logging', '~> 1'
17
+ gem.add_development_dependency "rspec", "~> 1.3"
18
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
19
+ end
20
+ Jeweler::GemcutterTasks.new
21
+ rescue LoadError
22
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
23
+ end
24
+
25
+ require 'spec/rake/spectask'
26
+ Spec::Rake::SpecTask.new(:spec) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.spec_files = FileList['spec/**/*_spec.rb']
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+ task :spec => :check_dependencies
38
+
39
+ task :default => :spec
40
+
41
+ require 'rake/rdoctask'
42
+ Rake::RDocTask.new do |rdoc|
43
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
44
+
45
+ rdoc.rdoc_dir = 'rdoc'
46
+ rdoc.title = "mysql2_model #{version}"
47
+ rdoc.rdoc_files.include('README*')
48
+ rdoc.rdoc_files.include('lib/**/*.rb')
49
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
data/examples/mtdb.rb ADDED
@@ -0,0 +1,43 @@
1
+ require "mysql2_model"
2
+
3
+ Mysql2Model::Config.repository_path = File.expand_path(File.dirname(__FILE__) + '/../examples/repositories.yml')
4
+
5
+ # # Use like:
6
+ # Mtdb.all.each |mtdb|
7
+ # puts "Database: #{mtdb.database_name}:" #direct method access
8
+ # puts "Host: #{mtdb[:db_server_host]}" #direct member access
9
+ # puts "Config: {mtdb.to_config.inspect}" #model methods
10
+ # end
11
+
12
+ class Mtdb
13
+ include Mysql2Model::Container
14
+
15
+ def self.all
16
+ query("SELECT id, database_name, db_server_host, db_server_port, db_server_user, db_server_password * FROM mtdbs")
17
+ end
18
+
19
+ def config_name
20
+ "mtdb#{id}".to_sym
21
+ end
22
+
23
+ def to_config
24
+ {
25
+ config_name => {
26
+ :host => db_server_host,
27
+ :database => database_name,
28
+ :port => db_server_port,
29
+ :username => db_server_user,
30
+ :password => db_server_password_unencrypted
31
+ }
32
+ }
33
+ end
34
+
35
+ def db_server_password_unencrypted
36
+ nil # You have to invent your own.
37
+ end
38
+
39
+ def self.default_repository_name # You don't need this if you want to use the default repo
40
+ :infrastructure
41
+ end
42
+
43
+ end
@@ -0,0 +1,26 @@
1
+ :repositories:
2
+ :default:
3
+ :database: community_production
4
+ :username: root
5
+ :password: gibberish
6
+ :host: localhost
7
+ :infrastructure:
8
+ :database: infrastructure_production
9
+ :username: infra_user
10
+ :password: more_gibberish
11
+ :host: localhost
12
+ :subscribers1:
13
+ :database: subscribers1_production
14
+ :username: sub_user
15
+ :password: sub_gibberish
16
+ :host: localhost
17
+ :subscribers2:
18
+ :database: subscribers2_production
19
+ :username: sub_user
20
+ :password: sub_gibberish
21
+ :host: localhost
22
+ :subscribers3:
23
+ :database: subscribers3_production
24
+ :username: sub_user
25
+ :password: sub_gibberish
26
+ :host: localhost
@@ -0,0 +1,27 @@
1
+ require "mysql2_model"
2
+
3
+ Mysql2Model::Config.repository_path = File.expand_path(File.dirname(__FILE__) + '/../examples/repositories.yml')
4
+
5
+ class Subscriber
6
+
7
+ include Mysql2Model::Container
8
+
9
+ # Assume :subscribers1 has 200 rows, :subscriber2 has 150, :subscriber3 has 50.
10
+
11
+ def self.all
12
+ query("SELECT * FROM subscribers") # => resultset of 400 rows
13
+ end
14
+
15
+ def self.count
16
+ value_sum("SELECT COUNT(*) FROM subscribers") # => 400
17
+ end
18
+
19
+ def self.count_from_each
20
+ value("SELECT COUNT(*) FROM subscribers") # => [200,150,50]
21
+ end
22
+
23
+ def self.default_repository_name
24
+ [:subscribers1,:subscribers2,:subscribers3]
25
+ end
26
+
27
+ end
@@ -0,0 +1,66 @@
1
+ module Mysql2Model
2
+ # multi-repository aware mysql2 client proxy
3
+ # @todo Evented Connection Pool
4
+ class Client
5
+ # @return a multi-repository proxy
6
+ def initialize(repos)
7
+ @repos = repos
8
+ end
9
+ # Collect the results of a multi-repository query into a single resultset
10
+ # @param [String] statement MySQL statement
11
+ def query(statement)
12
+ collection = []
13
+ @repos.each do |repo|
14
+ self.class[repo].query(statement).each do |row|
15
+ collection << row
16
+ end
17
+ end
18
+ collection
19
+ end
20
+ # Use the first connection to execute a single escape
21
+ # @param [String] statement MySQL statement
22
+ def escape(statement)
23
+ self.class[@repos.first].escape(statement)
24
+ end
25
+
26
+ class << self
27
+ # Stores a collection of mysql2 connections
28
+ def repositories
29
+ load_repos
30
+ @repositories
31
+ end
32
+ # loads the repositories with the YAML object pointed to by {Mysql2Model::Config.repository_path},
33
+ # subsequent calls are ignored unless forced.
34
+ # @param [boolean] force Use force = true to reload the repositories and overwrite the existing Hash.
35
+ def load_repos(force=false)
36
+ unless force
37
+ return unless @repositories.blank?
38
+ end
39
+ repos = YAML.load(File.new(Mysql2Model::Config.repository_path, 'r'))
40
+ repos[:repositories].each do |repo, config|
41
+ self[repo] = config
42
+ end
43
+ end
44
+ # Repository accessor lazily instantiates Mysql2::Clients or delegates them to an instance of the multi-repository proxy
45
+ def [](repository_name)
46
+ if repository_name.is_a?(Array)
47
+ self.new(repository_name)
48
+ else
49
+ load_repos
50
+ @repositories[repository_name][:client] ||= begin
51
+ c = Mysql2::Client.new(@repositories[repository_name][:config])
52
+ c.query_options.merge!(:symbolize_keys => true)
53
+ c
54
+ end
55
+ end
56
+ end
57
+ # Repository accessor stores the connection parameters for later use
58
+ def []=(repository_name,config)
59
+ @repositories ||= {}
60
+ @repositories[repository_name] ||= {}
61
+ @repositories[repository_name][:config] = config
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,105 @@
1
+ module Mysql2Model
2
+
3
+ # Generic Mysql2Model exception class.
4
+ class Mysql2ModelError < StandardError
5
+ end
6
+
7
+ # Raised when number of bind variables in statement does not match number of expected variables.
8
+ # @example two placeholders are given but only one variable to fill them.
9
+ # query("SELECT FROM locs WHERE lat = ? AND lng = ?", 53.7362)
10
+ class PreparedStatementInvalid < Mysql2ModelError
11
+ end
12
+
13
+ # Adapted from Rails ActiveRecord::Base
14
+ #
15
+ # Changed the language from "Sanitize", since not much sanitization is going on here.
16
+ # There is some escaping and coercion going on, but nothing explicity santizes the resulting statement.
17
+ module Composer
18
+
19
+ # Accepts multiple arguments, an array, or string of SQL and composes them
20
+ # @example String
21
+ # "name='foo''bar' and group_id='4'" #=> "name='foo''bar' and group_id='4'"
22
+ # @example Array
23
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] #=> "name='foo''bar' and group_id='4'"
24
+ # @param [Array,String]
25
+ def compose_sql(*statement)
26
+ raise PreparedStatementInvalid, "Statement is blank!" if statement.blank?
27
+ if statement.is_a?(Array)
28
+ if statement.size == 1 #strip the outer array
29
+ compose_sql_array(statement.first)
30
+ else
31
+ compose_sql_array(statement)
32
+ end
33
+ else
34
+ statement
35
+ end
36
+ end
37
+
38
+ # Accepts an array of conditions. The array has each value
39
+ # sanitized and interpolated into the SQL statement.
40
+ # @param [Array] ary
41
+ # @example Array
42
+ # ["name='%s' and group_id='%s'", "foo'bar", 4] #=> "name='foo''bar' and group_id='4'"
43
+ # @private
44
+ def compose_sql_array(ary)
45
+ statement, *values = ary
46
+ if values.first.is_a?(Hash) and statement =~ /:\w+/
47
+ replace_named_bind_variables(statement, values.first)
48
+ elsif statement.include?('?')
49
+ replace_bind_variables(statement, values)
50
+ else
51
+ statement % values.collect { |value| client.escape(value.to_s) }
52
+ end
53
+ end
54
+
55
+ def replace_bind_variables(statement, values) # @private
56
+ raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
57
+ bound = values.dup
58
+ statement.gsub('?') { escape_bound_value(bound.shift) }
59
+ end
60
+
61
+ def replace_named_bind_variables(statement, bind_vars) # @private
62
+ statement.gsub(/(:?):([a-zA-Z]\w*)/) do
63
+ if $1 == ':' # skip postgresql casts
64
+ $& # return the whole match
65
+ elsif bind_vars.include?(match = $2.to_sym)
66
+ escape_bound_value(bind_vars[match])
67
+ else
68
+ raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
69
+ end
70
+ end
71
+ end
72
+
73
+
74
+ def escape_bound_value(value) # @private
75
+ if value.respond_to?(:map) && !value.is_a?(String)
76
+ if value.respond_to?(:empty?) && value.empty?
77
+ escape(nil)
78
+ else
79
+ value.map { |v| escape(v) }.join(',')
80
+ end
81
+ else
82
+ escape(value)
83
+ end
84
+ end
85
+
86
+ def raise_if_bind_arity_mismatch(statement, expected, provided) # @private
87
+ unless expected == provided
88
+ raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
89
+ end
90
+ end
91
+
92
+ def escape(value) # @private
93
+ client.escape(convert(value))
94
+ end
95
+
96
+ # Attempt to coerce the value to be a representation consistent with the database
97
+ # @private
98
+ def convert(value)
99
+ return value.to_formatted_s(:db) if value.respond_to?(:to_formatted_s)
100
+ value.to_s
101
+ end
102
+
103
+ end
104
+
105
+ end
@@ -0,0 +1,15 @@
1
+ module Mysql2Model
2
+ # Configuration Attributes
3
+ class Config
4
+ private_class_method :new
5
+ class << self
6
+ attr_writer :repository_path
7
+ # Location of the YAML file to define the repositories
8
+ # @todo Identify a default repositories.yml inside the consuming projects' root.
9
+ # How to identify config/repositories.yml when we don't know what framework they are using?
10
+ def repository_path
11
+ @repository_path ||= 'repositories.yml'
12
+ end
13
+ end
14
+ end
15
+ end