mysql2_model 0.1.1

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.
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