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 +7 -0
- data/.gitignore +28 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +26 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +30 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +170 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/examples/mtdb.rb +43 -0
- data/examples/repositories.yml +26 -0
- data/examples/subscriber.rb +27 -0
- data/lib/mysql2_model/client.rb +66 -0
- data/lib/mysql2_model/composer.rb +105 -0
- data/lib/mysql2_model/config.rb +15 -0
- data/lib/mysql2_model/container.rb +167 -0
- data/lib/mysql2_model.rb +18 -0
- data/mysql2_model.gemspec +88 -0
- data/spec/mysql2_model/client_spec.rb +84 -0
- data/spec/mysql2_model/composer_spec.rb +123 -0
- data/spec/mysql2_model/config_spec.rb +10 -0
- data/spec/mysql2_model/container_spec.rb +48 -0
- data/spec/mysql2_model_spec.rb +7 -0
- data/spec/repositories.yml.fixture +26 -0
- data/spec/spec.opts +5 -0
- data/spec/spec_helper.rb +43 -0
- metadata +174 -0
data/.document
ADDED
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
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
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
|