rdo 0.0.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/.gitignore +17 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +309 -0
- data/Rakefile +2 -0
- data/lib/rdo.rb +32 -0
- data/lib/rdo/connection.rb +113 -0
- data/lib/rdo/driver.rb +120 -0
- data/lib/rdo/emulated_statement_executor.rb +36 -0
- data/lib/rdo/exception.rb +11 -0
- data/lib/rdo/result.rb +95 -0
- data/lib/rdo/statement.rb +28 -0
- data/lib/rdo/util.rb +102 -0
- data/lib/rdo/version.rb +3 -0
- data/rdo.gemspec +52 -0
- data/spec/rdo/connection_spec.rb +220 -0
- data/spec/rdo/driver_spec.rb +29 -0
- data/spec/rdo/emulated_statements_spec.rb +22 -0
- data/spec/rdo/result_spec.rb +117 -0
- data/spec/rdo/statement_spec.rb +24 -0
- data/spec/rdo/util_spec.rb +92 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/support/driver_with_everything.rb +38 -0
- data/spec/support/driver_without_statements.rb +23 -0
- data/util/macros.h +177 -0
- metadata +110 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
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
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
|