rdo 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|