rdo 0.0.8 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -36,18 +36,18 @@ end
36
36
  conn.close
37
37
  ```
38
38
 
39
- ## Why your ORM so shit?
39
+ ## Strange looking ORM you have there, Sir
40
40
 
41
- RDO provides access to a number of RDBMS's. It allows you to query using SQL
42
- and other things using DDL, as thinly as is necessary. It is absolutely not,
43
- nor is it trying to be an SQL abstraction layer, an ORM or anything of that
44
- nature. The intention is to provide a way to allow Ruby developers to write
45
- applications that use a database, but don't use an ORM (*scoff!*).
41
+ It's not an ORM. RDO provides access to a number of RDBMS's. It allows you to
42
+ query using pure SQL/DDL commands, as thinly as is necessary. It is absolutely
43
+ not, nor is it trying to be an SQL abstraction layer, an ORM or anything of
44
+ that nature. The intention is to provide a way to allow Ruby developers to
45
+ write applications that use a database, but don't use an ORM (*scoff!*).
46
46
 
47
47
  Or perhaps you're actually writing the next kick-ass ORM? Either way, RDO
48
48
  just lets you talk directly to your database.
49
49
 
50
- ## Meh, what does it provide?
50
+ ## What's the point?
51
51
 
52
52
  Let's face it, we've been writing database applications since the dark ages—
53
53
  it's not that hard. What's lacking from Ruby, however, is any consistency for
@@ -56,7 +56,7 @@ serve a different need. [DataMapper](https://github.com/datamapper/dm-core)
56
56
  has a layer underneath it called [data_objects](https://github.com/datamapper/do),
57
57
  but it isn't particularly user-friendly when used standalone and it requires
58
58
  jumping through hoops to deal with certain database RDBMS features, such as
59
- PostgreSQL bytea fields.
59
+ PostgreSQL bytea fields (which, in RDO, "just work").
60
60
 
61
61
  RDO makes the following things standard:
62
62
 
@@ -65,11 +65,11 @@ RDO makes the following things standard:
65
65
  - **Prepared statements** where possible; emulated where not
66
66
  - **Type-casting** to equivalent Ruby types (e.g. Fixnum, BigDecimal,
67
67
  Float, even Array)
68
- - **Buffered result sets** where possible–enumerate millions of rows
69
- without memory issues
70
68
  - Access meta data after write operations, with insert IDs standardized
71
69
  - **Use simple core data types** (Hash) for reading values and field names
72
70
 
71
+ Note that data-type support is limited to whatever the DBMS actually supports.
72
+
73
73
  ## Installation
74
74
 
75
75
  RDO doesn't do anything by itself. You need to also install the driver for
@@ -125,7 +125,10 @@ And then execute:
125
125
  <td>mysql</td>
126
126
  <td><a href="https://github.com/d11wtq/rdo-mysql">rdo-mysql</a></td>
127
127
  <td><a href="https://github.com/d11wtq">d11wtq</a></td>
128
- <td>In development</td>
128
+ <td>
129
+ <img src="https://secure.travis-ci.org/d11wtq/rdo-mysql.png?branch=master"
130
+ alt="Build Status" title="Build Status" />
131
+ </td>
129
132
  </tr>
130
133
  </tbody>
131
134
  </table>
@@ -154,6 +157,10 @@ For semantic reasons, #connect is aliased to #open.
154
157
  If it is not possible to establish a connection an RDO::Exception is raised,
155
158
  which should provide any reason given by the DBMS.
156
159
 
160
+ You can also pass a block to #connect. This has the same semantics as passing
161
+ a block to File#open (i.e. it passes itself to the block, returns the value
162
+ of the block and finally closes the connection).
163
+
157
164
  ### Disconnecting
158
165
 
159
166
  RDO will disconnect automatically when the connection is garbage-collected,
@@ -170,18 +177,6 @@ conn.open
170
177
  p conn.open? #=> true
171
178
  ```
172
179
 
173
- ### One-time use connections
174
-
175
- If you pass a block to RDO.connect, RDO yields the connection into the block,
176
- returns the result of the block, then closes the connection.
177
-
178
- ``` ruby
179
- puts RDO.open("sqlite:some.db") do |c|
180
- c.execute("SELECT value FROM config WHERE name = ?", "api_key").first_value
181
- end
182
- # => "EXAMPLE_KEY"
183
- ```
184
-
185
180
  ### Performing non-read commands
186
181
 
187
182
  All SQL and DDL (Data Definition Language) is executed with #execute, which
@@ -216,7 +211,7 @@ include any error messaage provided by the DBMS.
216
211
  ### Performing read queries
217
212
 
218
213
  There is no difference in the interface for reads or writes. Just call
219
- the #execute method—which always returns a RDO::Result—for both.
214
+ the #execute method in both cases. This always returns a RDO::Result.
220
215
  RDO::Result includes the Enumerable module. Some operations, such as #count
221
216
  may be optimized by the driver.
222
217
 
@@ -298,6 +293,16 @@ This method returns nil if there are no rows, so if you need to distinguish
298
293
  between NULL and no rows, you will need to check the result contents the
299
294
  longer way around.
300
295
 
296
+ ### Logging Statements
297
+
298
+ A Logger instance (with an interface like that in Ruby stdlib) may be passed
299
+ in the options when creating a connection. All queries will be logged with
300
+ DEBUG severity. Errors will be logged with FATAL severity.
301
+
302
+ ``` ruby
303
+ RDO.connect("postgres://user:pass@host/db", logger: Logger.new(STDOUT))
304
+ ```
305
+
301
306
  ## Contributing
302
307
 
303
308
  If you find a bug in RDO, send a pull request if you think you can fix it.
@@ -330,6 +335,9 @@ drivers. If you have written a driver for RDO, please fork this git repo and
330
335
  edit this README to list it, then send a pull request. That way others will
331
336
  find it more easily.
332
337
 
338
+ I'm particularly interested in drivers for Oracle and Microsoft SQL Server,
339
+ though I don't personally use these.
340
+
333
341
  ## Copyright & Licensing
334
342
 
335
343
  Written and maintained by Chris Corbyn.
data/lib/rdo.rb CHANGED
@@ -13,6 +13,7 @@ require "rdo/statement"
13
13
  require "rdo/emulated_statement_executor"
14
14
  require "rdo/result"
15
15
  require "rdo/util"
16
+ require "rdo/colored_logger"
16
17
 
17
18
  # c extension
18
19
  require "rdo/rdo"
@@ -28,24 +29,36 @@ module RDO
28
29
  # closed at the end of the block, before this method finally returns
29
30
  # the result of the block.
30
31
  #
31
- # @param [Object] options
32
- # either a connection URI string, or an option Hash
32
+ # @param [Object] uri
33
+ # either a connection URI string, or an options Hash
34
+ #
35
+ # @param [Hash] options
36
+ # if a URI is provided for the first argument, additional options may
37
+ # be specified here. These may override settings in the first argument.
33
38
  #
34
39
  # @return [Connection]
35
40
  # a Connection for the required driver
36
- def connect(options)
41
+ def connect(uri, options = {})
37
42
  if block_given?
38
43
  begin
39
- c = Connection.new(options)
44
+ c = Connection.new(uri, options)
40
45
  yield c
41
46
  ensure
42
47
  c.close unless c.nil?
43
48
  end
44
49
  else
45
- Connection.new(options)
50
+ Connection.new(uri, options)
46
51
  end
47
52
  end
48
53
 
49
54
  alias_method :open, :connect
50
55
  end
56
+
57
+ # A suitable NULL device for writing nothing
58
+ DEV_NULL =
59
+ if defined? IO::NULL
60
+ IO::NULL
61
+ else
62
+ ENV["OS"] =~ /Windows/ ? "NUL" : "/dev/null"
63
+ end
51
64
  end
@@ -0,0 +1,25 @@
1
+ ##
2
+ # RDO: Ruby Data Objects.
3
+ # Copyright © 2012 Chris Corbyn.
4
+ #
5
+ # See LICENSE file for details.
6
+ ##
7
+
8
+ require "logger"
9
+
10
+ module RDO
11
+ # A Logger that outputs using color to highlight errors etc.
12
+ class ColoredLogger < Logger
13
+ def initialize(*)
14
+ super
15
+ self.formatter =
16
+ Proc.new do |severity, time, prog, msg|
17
+ case severity
18
+ when "DEBUG" then "\033[35mSQL\033[0m \033[36m~\033[0m %s\n" % msg
19
+ when "FATAL" then "\033[31mERROR ~ %s\033[0m\n" % msg
20
+ else "%s ~ %s\n" % [severity, msg]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -7,6 +7,7 @@
7
7
 
8
8
  require "uri"
9
9
  require "cgi"
10
+ require "logger"
10
11
  require "forwardable"
11
12
 
12
13
  module RDO
@@ -41,8 +42,11 @@ module RDO
41
42
  # Options passed to initialize.
42
43
  attr_reader :options
43
44
 
45
+ # A Logger (from ruby stdlib)
46
+ attr_accessor :logger
47
+
44
48
  # Most instance methods are delegated to the driver
45
- def_delegators :@driver, :open, :open?, :close, :execute, :prepare, :quote
49
+ def_delegators :@driver, :open, :open?, :close, :quote
46
50
 
47
51
  # Initialize a new Connection.
48
52
  #
@@ -50,13 +54,18 @@ module RDO
50
54
  #
51
55
  # If no suitable driver is loaded, an RDO::Exception is raised.
52
56
  #
53
- # @param [Object] options
54
- # either a connection URI, or a Hash of options
57
+ # @param [Object] uri
58
+ # either a connection URI string, or an options Hash
59
+ #
60
+ # @param [Hash] options
61
+ # if a URI is provided for the first argument, additional options may
62
+ # be specified here. These may override settings in the first argument.
55
63
  #
56
64
  # @return [RDO::Connection]
57
65
  # a Connection for the given options
58
- def initialize(options)
59
- @options = normalize_options(options)
66
+ def initialize(uri, options = {})
67
+ @options = normalize_options(uri).merge(normalize_options(options))
68
+ @logger = @options.fetch(:logger, null_logger)
60
69
 
61
70
  unless self.class.drivers.key?(@options[:driver])
62
71
  raise RDO::Exception,
@@ -68,6 +77,47 @@ module RDO
68
77
  "Unable to connect, but the driver did not provide a reason"
69
78
  end
70
79
 
80
+ # Execute a statement with the configured Driver.
81
+ #
82
+ # The statement can either be a read, or a write operation.
83
+ # Placeholders marked by '?' may be interpolated in the statement, so
84
+ # that bind parameters can be safely provided.
85
+ #
86
+ # Where the RDBMS natively supports bind parameters, this functionality is
87
+ # used; otherwise, the values are quoted using #quote.
88
+ #
89
+ # @param [String] statement
90
+ # a string of SQL or DDL to be executed
91
+ #
92
+ # @param [Array] *bind_values
93
+ # a list of parameters to substitute in the statement
94
+ #
95
+ # @return [Result]
96
+ # the result of the query
97
+ def execute(statement, *bind_values)
98
+ @driver.execute(statement, *bind_values).tap do
99
+ if logger.debug?
100
+ logger.debug("#{statement}#{" <Bind: #{bind_values.inspect}>" unless bind_values.empty?}")
101
+ end
102
+ end
103
+ rescue RDO::Exception => e
104
+ logger.fatal(e.message) if logger.fatal?
105
+ raise
106
+ end
107
+
108
+ # Create a prepared statement to later be executed with some inputs.
109
+ #
110
+ # Not all drivers support this natively, but it is emulated by default.
111
+ #
112
+ # @param [String] statement
113
+ # a string of SQL or DDL, with '?' placeholders for bind parameters
114
+ #
115
+ # @return [Statement]
116
+ # a prepared statement to later be executed
117
+ def prepare(command)
118
+ Statement.new(@driver.prepare(command), logger)
119
+ end
120
+
71
121
  private
72
122
 
73
123
  # Normalizes the given options String or Hash into a Symbol-keyed Hash.
@@ -127,5 +177,9 @@ module RDO
127
177
  def parse_query_string(str)
128
178
  str.nil? ? {} : Hash[CGI.parse(str).map{|k,v| [k, v.size == 1 ? v.first : v]}]
129
179
  end
180
+
181
+ def null_logger
182
+ Logger.new(RDO::DEV_NULL).tap{|l| l.level = Logger::UNKNOWN}
183
+ end
130
184
  end
131
185
  end
@@ -68,19 +68,19 @@ module RDO
68
68
  # @param [String] statement
69
69
  # a string of SQL or DDL, with '?' placeholders for bind parameters
70
70
  #
71
- # @return [Statement]
71
+ # @return [StatementExecutor]
72
72
  # a prepared statement to later be executed
73
73
  def prepare(statement)
74
- Statement.new(emulated_statement_executor(statement))
74
+ emulated_statement_executor(statement)
75
75
  end
76
76
 
77
77
  # Execute a statement against the RDBMS.
78
78
  #
79
- # The statement can either by a read, or a write operation.
79
+ # The statement can either be a read, or a write operation.
80
80
  # Placeholders marked by '?' may be interpolated in the statement, so
81
81
  # that bind parameters can be safely provided.
82
82
  #
83
- # Where the RDBMS natively support bind parameters, this functionality is
83
+ # Where the RDBMS natively supports bind parameters, this functionality is
84
84
  # used; otherwise, the values are quoted using #quote.
85
85
  #
86
86
  # Drivers MUST override this.
@@ -9,20 +9,20 @@ module RDO
9
9
  # This StatementExecutor is used as a fallback for prepared statements.
10
10
  #
11
11
  # If a DBMS driver does not implement prepared statements, this is used instead.
12
- # The #execute method simply delegates back to the connection object.
12
+ # The #execute method simply delegates back to the driver.
13
13
  class EmulatedStatementExecutor
14
14
  attr_reader :command
15
15
 
16
- # Initialize a new statement executor for the given connection & command.
16
+ # Initialize a new statement executor for the given driver & command.
17
17
  #
18
- # @param [RDO::Connection] connection
19
- # the connection on which #prepare was invoked
18
+ # @param [RDO::Driver] driver
19
+ # the Driver on which #prepare was invoked
20
20
  #
21
21
  # @param [String] command
22
22
  # a string of SQL/DDL to execute
23
- def initialize(connection, command)
24
- @connection = connection
25
- @command = command
23
+ def initialize(driver, command)
24
+ @driver = driver
25
+ @command = command
26
26
  end
27
27
 
28
28
  # Execute the command using the given bind values.
@@ -30,7 +30,7 @@ module RDO
30
30
  # @param [Object...] args
31
31
  # bind parameters to use in place of '?'
32
32
  def execute(*args)
33
- @connection.execute(command, *args)
33
+ @driver.execute(command, *args)
34
34
  end
35
35
  end
36
36
  end
@@ -15,14 +15,36 @@ module RDO
15
15
  class Statement
16
16
  extend Forwardable
17
17
 
18
- def_delegators :@executor, :command, :execute
18
+ def_delegators :@executor, :command
19
19
 
20
20
  # Initialize a new Statement wrapping the given StatementExecutor.
21
21
  #
22
22
  # @param [Object] executor
23
23
  # any object that responds to #execute, #connection and #command
24
- def initialize(executor)
24
+ def initialize(executor, logger)
25
25
  @executor = executor
26
+ @logger = logger
27
+ end
28
+
29
+ # Execute the command using the given bind values.
30
+ #
31
+ # @param [Object...] args
32
+ # bind parameters to use in place of '?'
33
+ def execute(*bind_values)
34
+ @executor.execute(*bind_values).tap do
35
+ if logger.debug?
36
+ logger.debug("#{command}#{" <Bind: #{bind_values.inspect}>" unless bind_values.empty?}")
37
+ end
38
+ end
39
+ rescue RDO::Exception => e
40
+ logger.fatal(e.message) if logger.fatal?
41
+ raise
42
+ end
43
+
44
+ private
45
+
46
+ def logger
47
+ @logger
26
48
  end
27
49
  end
28
50
  end
@@ -6,5 +6,5 @@
6
6
  ##
7
7
 
8
8
  module RDO
9
- VERSION = "0.0.8"
9
+ VERSION = "0.1.0"
10
10
  end
@@ -1,4 +1,5 @@
1
1
  require "spec_helper"
2
+ require "logger"
2
3
 
3
4
  describe RDO::Connection do
4
5
  after(:each) { RDO::Connection.drivers.clear }
@@ -193,16 +194,84 @@ describe RDO::Connection do
193
194
  and_return(result)
194
195
  connection.execute("SELECT * FROM bob WHERE ?", true).should == result
195
196
  end
197
+
198
+ context "with debug logging" do
199
+ before(:each) do
200
+ connection.logger.level = Logger::DEBUG
201
+ driver.stub(:execute).and_return(result)
202
+ end
203
+
204
+ it "logs the statement" do
205
+ connection.logger.should_receive(:debug).
206
+ with(/SELECT \* FROM bob WHERE \?.*?true/)
207
+ connection.execute("SELECT * FROM bob WHERE ?", true)
208
+ end
209
+ end
210
+
211
+ context "without debug logging" do
212
+ before(:each) do
213
+ connection.logger.level = Logger::INFO
214
+ driver.stub(:execute).and_return(result)
215
+ end
216
+
217
+ it "does not log the statement" do
218
+ connection.logger.should_not_receive(:debug)
219
+ connection.execute("SELECT * FROM bob WHERE ?", true)
220
+ end
221
+ end
222
+
223
+ context "when an RDO::Exception occurs" do
224
+ before(:each) do
225
+ driver.stub(:execute).and_raise(RDO::Exception.new("some error"))
226
+ end
227
+
228
+ context "with fatal logging" do
229
+ before(:each) do
230
+ connection.logger.level = Logger::FATAL
231
+ end
232
+
233
+ it "logs the error" do
234
+ begin
235
+ connection.logger.should_receive(:fatal).
236
+ with(/some error/)
237
+ connection.execute("SELECT * FROM bob WHERE ?", true)
238
+ fail("RDO::Exception should be raised")
239
+ rescue
240
+ # expected
241
+ end
242
+ end
243
+ end
244
+
245
+ context "without debug logging" do
246
+ before(:each) do
247
+ connection.logger.level = Logger::UNKNOWN
248
+ end
249
+
250
+ it "does not log the error" do
251
+ begin
252
+ connection.logger.should_not_receive(:fatal)
253
+ connection.execute("SELECT * FROM bob WHERE ?", true)
254
+ fail("RDO::Exception should be raised")
255
+ rescue
256
+ # expected
257
+ end
258
+ end
259
+ end
260
+ end
196
261
  end
197
262
 
198
263
  describe "#prepare" do
199
- let(:stmt) { RDO::Statement.new(stub(:executor)) }
264
+ let(:command) { "SELECT * FROM bob WHERE ?" }
265
+ let(:executor) { stub(command: command) }
200
266
 
201
267
  it "delegates to the driver" do
202
- driver.should_receive(:prepare).
203
- with("SELECT * FROM bob WHERE ?").
204
- and_return(stmt)
205
- connection.prepare("SELECT * FROM bob WHERE ?").should == stmt
268
+ driver.should_receive(:prepare).with(command).and_return(executor)
269
+ connection.prepare(command).command.should == command
270
+ end
271
+
272
+ it "returns a RDO::Statement" do
273
+ driver.stub(:prepare).and_return(executor)
274
+ connection.prepare(command).should be_a_kind_of(RDO::Statement)
206
275
  end
207
276
  end
208
277
 
@@ -10,12 +10,14 @@ describe RDO::Driver do
10
10
  describe "#prepare" do
11
11
  let(:driver) { RDO::DriverWithoutStatements.new }
12
12
 
13
- it "returns a Statement" do
14
- driver.prepare("SELECT * FROM bob WHERE ?").should be_a_kind_of(RDO::Statement)
13
+ it "returns a StatementExecutor" do
14
+ driver.prepare("SELECT * FROM bob WHERE ?").
15
+ should be_a_kind_of(RDO::EmulatedStatementExecutor)
15
16
  end
16
17
 
17
18
  it "has the correct command" do
18
- driver.prepare("SELECT * FROM bob WHERE ?").command.should == "SELECT * FROM bob WHERE ?"
19
+ driver.prepare("SELECT * FROM bob WHERE ?").command.
20
+ should == "SELECT * FROM bob WHERE ?"
19
21
  end
20
22
 
21
23
  it "calls #execute on the driver" do
@@ -1,8 +1,10 @@
1
1
  require "spec_helper"
2
+ require "logger"
2
3
 
3
4
  describe RDO::Statement do
5
+ let(:logger) { Logger.new(RDO::DEV_NULL) }
4
6
  let(:executor) { double(:executor) }
5
- let(:stmt) { RDO::Statement.new(executor) }
7
+ let(:stmt) { RDO::Statement.new(executor, logger) }
6
8
 
7
9
  describe "#command" do
8
10
  let(:command) { "SELECT * FROM users" }
@@ -14,11 +16,74 @@ describe RDO::Statement do
14
16
  end
15
17
 
16
18
  describe "#execute" do
17
- let(:result) { stub(:result) }
19
+ let(:executor) { double(command: "SELECT * FROM bob WHERE ?", execute: result) }
20
+ let(:result) { stub(:result) }
18
21
 
19
22
  it "delegates to the executor" do
20
23
  executor.should_receive(:execute).with(1, 2).and_return(result)
21
24
  stmt.execute(1, 2).should == result
22
25
  end
26
+
27
+ context "with debug logging" do
28
+ before(:each) do
29
+ logger.level = Logger::DEBUG
30
+ executor.stub(:execute).and_return(result)
31
+ end
32
+
33
+ it "logs the statement" do
34
+ logger.should_receive(:debug).with(/SELECT \* FROM bob WHERE \?.*?true/)
35
+ stmt.execute(true)
36
+ end
37
+ end
38
+
39
+ context "without debug logging" do
40
+ before(:each) do
41
+ logger.level = Logger::INFO
42
+ executor.stub(:execute).and_return(result)
43
+ end
44
+
45
+ it "does not log the statement" do
46
+ logger.should_not_receive(:debug)
47
+ stmt.execute(true)
48
+ end
49
+ end
50
+
51
+ context "when an RDO::Exception occurs" do
52
+ before(:each) do
53
+ executor.stub(:execute).and_raise(RDO::Exception.new("some error"))
54
+ end
55
+
56
+ context "with fatal logging" do
57
+ before(:each) do
58
+ logger.level = Logger::FATAL
59
+ end
60
+
61
+ it "logs the error" do
62
+ begin
63
+ logger.should_receive(:fatal).with(/some error/)
64
+ stmt.execute
65
+ fail("RDO::Exception should be raised")
66
+ rescue
67
+ # expected
68
+ end
69
+ end
70
+ end
71
+
72
+ context "without debug logging" do
73
+ before(:each) do
74
+ logger.level = Logger::UNKNOWN
75
+ end
76
+
77
+ it "does not log the error" do
78
+ begin
79
+ logger.should_not_receive(:fatal)
80
+ stmt.execute
81
+ fail("RDO::Exception should be raised")
82
+ rescue
83
+ # expected
84
+ end
85
+ end
86
+ end
87
+ end
23
88
  end
24
89
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rdo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-09-30 00:00:00.000000000 Z
12
+ date: 2012-10-10 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -77,6 +77,7 @@ files:
77
77
  - ext/rdo/extconf.rb
78
78
  - ext/rdo/rdo.c
79
79
  - lib/rdo.rb
80
+ - lib/rdo/colored_logger.rb
80
81
  - lib/rdo/connection.rb
81
82
  - lib/rdo/driver.rb
82
83
  - lib/rdo/emulated_statement_executor.rb