rdo 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rdo.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright © 2012 Chris Corbyn.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,309 @@
1
+ # RDO—Ruby Data Objects
2
+
3
+ RDO provides a simple, robust standardized way to access various RDBMS
4
+ implementations in Ruby. Drivers all conform to the same, beautiful rubyesque
5
+ interface. Where a feature is not natively supported by the DBMS—for example,
6
+ prepared statements—it is seamlessly emulated, so you don't need to code
7
+ around it.
8
+
9
+ It targets **Ruby version 1.9** and newer.
10
+
11
+ ``` ruby
12
+ require "rdo"
13
+ require "rdo-postgres"
14
+
15
+ conn = RDO.connect("postgres://user:pass@localhost/dbname?encoding=utf-8")
16
+
17
+ result = conn.execute(
18
+ "INSERT INTO users (
19
+ name, password_hash, created_at, updated_at
20
+ ) VALUES (?, ?, ?, ?) RETURNING id",
21
+ 'bob',
22
+ Digest::MD5.hexdigest('secret'),
23
+ Time.now,
24
+ Time.now
25
+ )
26
+
27
+ puts "Inserted user ID = #{result.insert_id}"
28
+
29
+ result = conn.execute("SELECT * FROM users WHERE name LIKE ?", "%jim%")
30
+ result.each do |row|
31
+ puts "#{row[:id]}: #{row[:name]}"
32
+ end
33
+
34
+ conn.close
35
+ ```
36
+
37
+ ## Why your ORM so shit?
38
+
39
+ RDO provides access to a number of RDBMS's. It allows you to query using SQL
40
+ and issue commands using DDL, as thinly as is necessary. It is absolutely not,
41
+ nor is it trying to be an SQL abstraction layer, an ORM or anything of that
42
+ nature. The intention is to provide a way to allow Ruby developers to write
43
+ applications that use a database, but don't use an ORM (*scoff!*).
44
+
45
+ Or perhaps you're actually writing the next kick-ass ORM? Either way, RDO
46
+ just lets you talk directly to your database.
47
+
48
+ ## Meh, what does it provide?
49
+
50
+ Let's face it, we've been writing database applications since the dark ages—
51
+ it's not that hard. What's lacking from Ruby, however, is any consistency for
52
+ dealing with a database directly. Several beautiful ORMs exist, but they
53
+ serve a different need. [DataMapper](https://github.com/datamapper/dm-core)
54
+ has a layer underneath it called [data_objects](https://github.com/datamapper/do),
55
+ but it isn't particularly user-friendly when used standalone and it requires
56
+ jumping through hoops to deal with certain database RDBMS features, such as
57
+ PostgreSQL bytea fields.
58
+
59
+ RDO makes the following things standard:
60
+
61
+ - **Consistent** class/method contracts for all drivers
62
+ - **Native bind parameters** where possible; emulated where not
63
+ - **Prepared statements** where possible; emulated where not
64
+ - **Type-casting** to equivalent Ruby types (e.g. Fixnum, BigDecimal,
65
+ Float, even Array)
66
+ - **Buffered result sets** where possible–enumerate millions of rows
67
+ without memory issues
68
+ - Access meta data after write operations, with insert IDs standardized
69
+ - **Use simple core data types** (Hash) for reading values and field names
70
+
71
+ ## Installation
72
+
73
+ RDO doesn't do anything by itself. You need to also install the driver for
74
+ your DBMS. Install via Rubygems.
75
+
76
+ $ gem install rdo
77
+ $ gem install rdo-postgres
78
+
79
+ Or add a line to your application's Gemfile:
80
+
81
+ gem "rdo"
82
+ gem "rdo-postgres"
83
+
84
+ And then execute:
85
+
86
+ $ bundle
87
+
88
+ ## Available Drivers
89
+
90
+ <table>
91
+ <thead>
92
+ <tr>
93
+ <th>Database Vendor</th>
94
+ <th>URI Schemes</th>
95
+ <th>Gem</th>
96
+ <th>Author</th>
97
+ </tr>
98
+ </thead>
99
+ <tbody>
100
+ <tr>
101
+ <th>SQLite</th>
102
+ <td>sqlite</td>
103
+ <td><a href="https://github.com/d11wtq/rdo-sqlite">rdo-sqlite</a></td>
104
+ <td><a href="https://github.com/d11wtq">d11wtq</a></td>
105
+ </tr>
106
+ <tr>
107
+ <th>PostgreSQL</th>
108
+ <td>postgresql, postgres</td>
109
+ <td><a href="https://github.com/d11wtq/rdo-postgres">rdo-postgres</a></td>
110
+ <td><a href="https://github.com/d11wtq">d11wtq</a></td>
111
+ </tr>
112
+ <tr>
113
+ <th>MySQL</th>
114
+ <td>mysql</td>
115
+ <td><a href="https://github.com/d11wtq/rdo-mysql">rdo-mysql</a></td>
116
+ <td><a href="https://github.com/d11wtq">d11wtq</a></td>
117
+ </tr>
118
+ </tbody>
119
+ </table>
120
+
121
+ ## Usage
122
+
123
+ The interface for RDO is intentionally minimal. It should take a few minutes
124
+ to learn just about everything.
125
+
126
+ ### Connecting to a database
127
+
128
+ A connection is established when you initialize an RDO::Connection. The
129
+ easiest way to do that is through `RDO.connect`. Make sure you have required
130
+ the driver for RDO first, or it will explode, like, all in your face and stuff.
131
+
132
+ ``` ruby
133
+ require "rdo"
134
+ require "rdo-postgres"
135
+
136
+ conn = RDO.connect("postgresql://user:pass@host:port/db_name?encoding=utf-8")
137
+ p conn.open? #=> true
138
+ ```
139
+
140
+ If it is not possible to establish a connection an RDO::Exception is raised,
141
+ which should provide any reason given by the DBMS.
142
+
143
+ ### Disconnecting
144
+
145
+ RDO will disconnect automatically when the connection is garbage-collected,
146
+ or when the program exits, but if you need to disconnect explicitly,
147
+ call #close. It is safe to call this even if the connection is already closed.
148
+
149
+ Call #open to re-connect after closing a connection, for example when forking
150
+ child processes.
151
+
152
+ ``` ruby
153
+ conn.close
154
+ p conn.open? #=> false
155
+
156
+ conn.open
157
+ p conn.open? #=> true
158
+ ```
159
+
160
+ ### Performing non-read commands
161
+
162
+ All SQL and DDL (Data Definition Language) is executed with #execute, which
163
+ always returns a RDO::Result object. Query inputs should be provided as
164
+ binding placeholders and additional arguments. No explicit type-conversion is
165
+ necessary.
166
+
167
+ ``` ruby
168
+ result = conn.execute("CREATE TABLE bob ( ... )")
169
+ result = conn.execute("UPDATE users SET banned = ?", true)
170
+
171
+ p result.affected_rows #=> 5087
172
+
173
+ result = conn.execute(
174
+ "INSERT INTO users (name, created_at) VALUES (?, ?) RETURNING id",
175
+ "Jimbo Baggins",
176
+ Time.now
177
+ )
178
+
179
+ p result.insert_id #=> 5088
180
+ p result.execution_time #=> 0.0000587
181
+
182
+ # fields from the RETURNING clause are included in the result, like a SELECT
183
+ result.each do |row|
184
+ p row[:id] #=> 5088
185
+ end
186
+ ```
187
+
188
+ In the event of a query error, an RDO::Exception is raised, which should
189
+ include any error messaage provided by the DBMS.
190
+
191
+ ### Performing read queries
192
+
193
+ There is no difference in the interface for reads or writes. Just call
194
+ the #execute method—which always returns a RDO::Result—for both.
195
+ RDO::Result includes the Enumerable module. Some operations, such as #count
196
+ may be optimized by the driver.
197
+
198
+ ``` ruby
199
+ result = conn.execute("SELECT id, name FROM users WHERE created_at > ?", 1.week.ago)
200
+
201
+ p result.count #=> 120
202
+
203
+ result.each do |row|
204
+ p "#{row[:id]}: #{row[:name]}"
205
+ end
206
+ ```
207
+
208
+ In the event of a query error, an RDO::Exception is raised, which should
209
+ include any error messaage provided by the DBMS.
210
+
211
+ ### Using prepared statements
212
+
213
+ Most mainstream databases support them. Some don't, but RDO emulates them in
214
+ that case. Prepared statements provide safety through bind parameters and
215
+ efficiency through query re-use, because the query planner only executes once.
216
+
217
+ Prepare a statement with #prepare, then execute it with #execute, passing in
218
+ any bind parameters. An RDO::Result is returned.
219
+
220
+ ``` ruby
221
+ stmt = conn.prepare("SELECT * FROM users WHERE name LIKE ? AND banned = ?")
222
+
223
+ %w[bob jim harry].each do |name|
224
+ result = stmt.execute("%#{name}%", false)
225
+ result.each do |row|
226
+ p "#{row[:id]: row[:name]}"
227
+ end
228
+ end
229
+ ```
230
+
231
+ RDO simply delegates to #execute if the driver doesn't support prepared
232
+ statements.
233
+
234
+ In the event of a query error, an RDO::Exception is raised, which should
235
+ include any error messaage provided by the DBMS.
236
+
237
+ ### Tread carefully, there be danger ahead
238
+
239
+ While driver developers are expected to provide a suitable implementation,
240
+ it is generally riskier to use #quote and interpolate inputs directly into
241
+ the SQL, than it is to use bind parameters. There are times where you might
242
+ need to escape some input yourself, however. For that, you can call #quote.
243
+
244
+ ``` ruby
245
+ conn.execute("INSERT INTO users (name) VALUES ('#{conn.quote(params[:name])}')")
246
+ ```
247
+
248
+ ### Column names with whitespace in them
249
+
250
+ RDO uses Symbols as keys in the hashes that represent data rows. Most of the
251
+ time this is desirable. If you query for something that returns field names
252
+ containing spaces, or punctuation, you need to convert a String to a Symbol
253
+ using #to_sym or #intern. Or wrap the Hash with a Mash of some sort.
254
+
255
+ ``` ruby
256
+ result = conn(%q{SELECT 42 AS "The Meaning"})
257
+ p result.first["The Meaning".intern]
258
+ ```
259
+
260
+ I weighed up the possibility of using a custom data type, but I prefer core
261
+ ruby types unless there's an overwhelming reason to use a custom type, sorry.
262
+
263
+ ### Selecting just a single value
264
+
265
+ RDO::Result has a #first_value method for convenience if you are only
266
+ selecting one row and one column.
267
+
268
+ ``` ruby
269
+ p conn.execute("SELECT count(true) FROM users").first_value #=> 5088
270
+ ```
271
+
272
+ This method returns nil if there are no rows, so if you need to distinguish
273
+ between NULL and no rows, you will need to check the result contents the
274
+ longer way around.
275
+
276
+ ## Contributing
277
+
278
+ If you find a bug in RDO, send a pull request if you think you can fix it.
279
+ Your contribution will be recognized here. If you don't know how to fix it,
280
+ file an issue in the issue tracker on GitHub.
281
+
282
+ When sending pull requests, please use topic branches—don't send a pull
283
+ request from the master branch of your fork, as that may change
284
+ unintentionally.
285
+
286
+ ### Writing a driver for RDO
287
+
288
+ The more drivers that RDO has support for, the better. Writing drivers for
289
+ RDO is quite painless. They are just thin wrappers around the C API for the
290
+ DBMS, which conform to RDO's interface.
291
+
292
+ Some of the more boilerplate things you'd normally have to do are covered by
293
+ C macros in the util/macros.h file you'll find in this repository. Copy that
294
+ file to your own project and include it for one-line type conversions etc.
295
+ Take a look at one of the existing drivers to get an idea how to write a
296
+ driver.
297
+
298
+ Because one person could not possibly maintain drivers for all conceivable
299
+ DBMS's, it is better that different developers write and maintain different
300
+ drivers. If you have written a driver for RDO, please fork this git repo and
301
+ edit this README to list it, then send a pull request. That way others will
302
+ find it more easily.
303
+
304
+ ## Copyright & Licensing
305
+
306
+ Written and maintained by Chris Corbyn.
307
+
308
+ Licensed under the MIT license. That pretty much means it's fair game to use
309
+ RDO as you please, but you should refer to the LICENSE file for details.
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
data/lib/rdo.rb ADDED
@@ -0,0 +1,32 @@
1
+ ##
2
+ # RDO: Ruby Data Objects.
3
+ # Copyright © 2012 Chris Corbyn.
4
+ #
5
+ # See LICENSE file for details.
6
+ ##
7
+
8
+ require "rdo/version"
9
+ require "rdo/exception"
10
+ require "rdo/driver"
11
+ require "rdo/connection"
12
+ require "rdo/statement"
13
+ require "rdo/emulated_statement_executor"
14
+ require "rdo/result"
15
+ require "rdo/util"
16
+
17
+ module RDO
18
+ class << self
19
+ # Establish a connection to the RDBMS.
20
+ #
21
+ # The needed driver must be loaded before calling this.
22
+ #
23
+ # @param [Object] options
24
+ # either a connection URI string, or an option Hash
25
+ #
26
+ # @return [Connection]
27
+ # a Connection for the required driver
28
+ def connect(options)
29
+ Connection.new(options)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,113 @@
1
+ ##
2
+ # RDO: Ruby Data Objects.
3
+ # Copyright © 2012 Chris Corbyn.
4
+ #
5
+ # See LICENSE file for details.
6
+ ##
7
+
8
+ require "uri"
9
+ require "cgi"
10
+ require "forwardable"
11
+
12
+ module RDO
13
+ # Wrapper class to manage Driver classes.
14
+ #
15
+ # This is the user-facing connection class. Users do not instantiate
16
+ # drivers directly.
17
+ class Connection
18
+ class << self
19
+ # List all known drivers, as a Hash mapping the URI scheme to the Class.
20
+ #
21
+ # @return [Hash]
22
+ # the mapping of driver names to class names
23
+ def drivers
24
+ @drivers ||= {}
25
+ end
26
+
27
+ # Register a known driver class for the given URI scheme name.
28
+ #
29
+ # @param [String] name
30
+ # the name of the URI scheme (e.g. sqlite)
31
+ #
32
+ # @param [Class<RDO::Driver>] klass
33
+ # a subclass of RDO::Driver that provides the driver
34
+ def register_driver(name, klass)
35
+ drivers[name.to_s] = klass
36
+ end
37
+ end
38
+
39
+ extend Forwardable
40
+
41
+ # Options passed to initialize.
42
+ attr_reader :options
43
+
44
+ # Most instance methods are delegated to the driver
45
+ def_delegators :@driver, :open, :open?, :close, :execute, :prepare, :quote
46
+
47
+ # Initialize a new Connection.
48
+ #
49
+ # This method instantiates the necessary driver.
50
+ #
51
+ # If no suitable driver is loaded, an RDO::Exception is raised.
52
+ #
53
+ # @param [Object] options
54
+ # either a connection URI, or a Hash of options
55
+ #
56
+ # @return [RDO::Connection]
57
+ # a Connection for the given options
58
+ def initialize(options)
59
+ @options = normalize_options(options)
60
+
61
+ unless self.class.drivers.key?(@options[:driver])
62
+ raise RDO::Exception,
63
+ "Unregistered driver #{@options[:driver].inspect}"
64
+ end
65
+
66
+ @driver = self.class.drivers[@options[:driver]].new(@options)
67
+ @driver.open or raise RDO::Exception,
68
+ "Unable to connect, but the driver did not provide a reason"
69
+ end
70
+
71
+ private
72
+
73
+ # Normalizes the given options String or Hash into a Symbol-keyed Hash.
74
+ #
75
+ # @param [Object] options
76
+ # either a String, a URI or a Hash
77
+ #
78
+ # @return [Hash]
79
+ # a Symbol-keyed Hash
80
+ def normalize_options(options)
81
+ case options
82
+ when Hash
83
+ Hash[options.map{|k,v| [k.respond_to?(:to_sym) ? k.to_sym : k, v]}].tap do |opts|
84
+ opts[:driver] = opts[:driver].to_s if opts[:driver]
85
+ end
86
+ when String, URI
87
+ parse_connection_uri(options)
88
+ else
89
+ raise RDO::Exception,
90
+ "Unsupported connection argument format: #{options.class.name}"
91
+ end
92
+ end
93
+
94
+ def parse_connection_uri(str)
95
+ uri = URI.parse(str.to_s)
96
+ normalize_options(
97
+ {
98
+ driver: uri.scheme,
99
+ host: uri.host,
100
+ port: uri.port,
101
+ path: uri.path,
102
+ database: uri.path.to_s.sub("/", ""),
103
+ user: uri.user,
104
+ password: uri.password
105
+ }.merge(parse_query_string(uri.query))
106
+ )
107
+ end
108
+
109
+ def parse_query_string(str)
110
+ str.nil? ? {} : Hash[CGI.parse(str).map{|k,v| [k, v.size == 1 ? v.first : v]}]
111
+ end
112
+ end
113
+ end