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