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/lib/rdo/driver.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
##
|
2
|
+
# RDO: Ruby Data Objects.
|
3
|
+
# Copyright © 2012 Chris Corbyn.
|
4
|
+
#
|
5
|
+
# See LICENSE file for details.
|
6
|
+
##
|
7
|
+
|
8
|
+
module RDO
|
9
|
+
# Abstract class that is subclassed by each specific driver.
|
10
|
+
#
|
11
|
+
# Driver developers should be able to subclass this, then write specs and
|
12
|
+
# override the behaviours they need to change.
|
13
|
+
#
|
14
|
+
# Ideally all instance method will be overridden by really robust drivers.
|
15
|
+
class Driver
|
16
|
+
# Options passed to initialize.
|
17
|
+
attr_reader :options
|
18
|
+
|
19
|
+
# Initialize the Driver with the given options.
|
20
|
+
#
|
21
|
+
# Drivers SHOULD call super if overriding.
|
22
|
+
#
|
23
|
+
# @param [Hash] options
|
24
|
+
# all options passed to the Driver, as a Symbol-keyed Hash.
|
25
|
+
def initialize(options = {})
|
26
|
+
@options = options.dup
|
27
|
+
end
|
28
|
+
|
29
|
+
# Open a connection to the RDBMS, if it is not already open.
|
30
|
+
#
|
31
|
+
# If it is not possible to open a connection, an RDO::Exception is raised.
|
32
|
+
#
|
33
|
+
# This is a no-op: subclasses MUST override this.
|
34
|
+
#
|
35
|
+
# @return [Boolean]
|
36
|
+
# true if a connection was opened or was already open, false if not.
|
37
|
+
def open
|
38
|
+
false
|
39
|
+
end
|
40
|
+
|
41
|
+
# Check if the connection is currently open or not.
|
42
|
+
#
|
43
|
+
# Drivers MUST override this.
|
44
|
+
#
|
45
|
+
# @return [Boolean]
|
46
|
+
# true if the connection is open, false otherwise
|
47
|
+
def open?
|
48
|
+
false
|
49
|
+
end
|
50
|
+
|
51
|
+
# Close the current connection, if it is open.
|
52
|
+
#
|
53
|
+
# Drivers MUST override this.
|
54
|
+
#
|
55
|
+
# @return [Boolean]
|
56
|
+
# true if the connection was closed or was already closed, false if not
|
57
|
+
def close
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
# Create a prepared statement to later be executed with some inputs.
|
62
|
+
#
|
63
|
+
# Not all drivers support this natively, but it is emulated by default.
|
64
|
+
#
|
65
|
+
# This is a default implementation for emulated prepared statements:
|
66
|
+
# drivers SHOULD override it if possible.
|
67
|
+
#
|
68
|
+
# @param [String] statement
|
69
|
+
# a string of SQL or DDL, with '?' placeholders for bind parameters
|
70
|
+
#
|
71
|
+
# @return [Statement]
|
72
|
+
# a prepared statement to later be executed
|
73
|
+
def prepare(statement)
|
74
|
+
Statement.new(emulated_statement_executor(statement))
|
75
|
+
end
|
76
|
+
|
77
|
+
# Execute a statement against the RDBMS.
|
78
|
+
#
|
79
|
+
# The statement can either by a read, or a write operation.
|
80
|
+
# Placeholders marked by '?' may be interpolated in the statement, so
|
81
|
+
# that bind parameters can be safely provided.
|
82
|
+
#
|
83
|
+
# Where the RDBMS natively support bind parameters, this functionality is
|
84
|
+
# used; otherwise, the values are quoted using #quote.
|
85
|
+
#
|
86
|
+
# Drivers MUST override this.
|
87
|
+
#
|
88
|
+
# @param [String] statement
|
89
|
+
# a string of SQL or DDL to be executed
|
90
|
+
#
|
91
|
+
# @param [Array] *bind_values
|
92
|
+
# a list of parameters to substitute in the statement
|
93
|
+
#
|
94
|
+
# @return [Result]
|
95
|
+
# the result of the query
|
96
|
+
def execute(statement, *bind_values)
|
97
|
+
Result.new([])
|
98
|
+
end
|
99
|
+
|
100
|
+
# Escape a given value for safe interpolation into a statement.
|
101
|
+
#
|
102
|
+
# This should be avoided where the driver natively supports bind parameters.
|
103
|
+
#
|
104
|
+
# Drivers MUST override this with a RDBMS-specific solution.
|
105
|
+
#
|
106
|
+
# @param [Object] value
|
107
|
+
# the value to quote
|
108
|
+
#
|
109
|
+
# @return [String]
|
110
|
+
# a safely escaped value
|
111
|
+
def quote(value)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def emulated_statement_executor(stmt)
|
117
|
+
EmulatedStatementExecutor.new(self, stmt)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
##
|
2
|
+
# RDO: Ruby Data Objects.
|
3
|
+
# Copyright © 2012 Chris Corbyn.
|
4
|
+
#
|
5
|
+
# See LICENSE file for details.
|
6
|
+
##
|
7
|
+
|
8
|
+
module RDO
|
9
|
+
# This StatementExecutor is used as a fallback for prepared statements.
|
10
|
+
#
|
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.
|
13
|
+
class EmulatedStatementExecutor
|
14
|
+
attr_reader :command
|
15
|
+
|
16
|
+
# Initialize a new statement executor for the given connection & command.
|
17
|
+
#
|
18
|
+
# @param [RDO::Connection] connection
|
19
|
+
# the connection on which #prepare was invoked
|
20
|
+
#
|
21
|
+
# @param [String] command
|
22
|
+
# a string of SQL/DDL to execute
|
23
|
+
def initialize(connection, command)
|
24
|
+
@connection = connection
|
25
|
+
@command = command
|
26
|
+
end
|
27
|
+
|
28
|
+
# Execute the command using the given bind values.
|
29
|
+
#
|
30
|
+
# @param [Object...] args
|
31
|
+
# bind parameters to use in place of '?'
|
32
|
+
def execute(*args)
|
33
|
+
@connection.execute(command, *args)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/rdo/result.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
##
|
2
|
+
# RDO: Ruby Data Objects.
|
3
|
+
# Copyright © 2012 Chris Corbyn.
|
4
|
+
#
|
5
|
+
# See LICENSE file for details.
|
6
|
+
##
|
7
|
+
|
8
|
+
module RDO
|
9
|
+
# The standard Result class returned by Connection#execute.
|
10
|
+
#
|
11
|
+
# Both read and write queries receive results in this format.
|
12
|
+
class Result
|
13
|
+
include Enumerable
|
14
|
+
|
15
|
+
# Initialize a new Result.
|
16
|
+
#
|
17
|
+
# @param [Enumerable] tuples
|
18
|
+
# a list of tuples, provided by the driver
|
19
|
+
#
|
20
|
+
# @param [Hash] info
|
21
|
+
# information about the result, including:
|
22
|
+
# - count
|
23
|
+
# - rows_affected
|
24
|
+
# - insert_id
|
25
|
+
# - execution_time
|
26
|
+
def initialize(tuples, info = {})
|
27
|
+
@info = info.dup
|
28
|
+
@tuples = tuples
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get raw result info provided by the driver.
|
32
|
+
#
|
33
|
+
# @return [Hash]
|
34
|
+
# aribitrary information provided about the result
|
35
|
+
def info
|
36
|
+
@info
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return the inserted row ID.
|
40
|
+
#
|
41
|
+
# For some drivers this requires that a RETURNING clause by used in SQL.
|
42
|
+
# It may be more desirable to simply check the rows in the result.
|
43
|
+
#
|
44
|
+
# @return [Object]
|
45
|
+
# the ID of the record just inserted, or nil
|
46
|
+
def insert_id
|
47
|
+
if info.key?(:insert_id)
|
48
|
+
info[:insert_id]
|
49
|
+
else
|
50
|
+
first_value
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# If only one column and one row is expected in the result, fetch it.
|
55
|
+
#
|
56
|
+
# If no rows were returned, this method returns nil.
|
57
|
+
#
|
58
|
+
# @return [Object]
|
59
|
+
# a single value at the first column in the first row of the result
|
60
|
+
def first_value
|
61
|
+
if row = first
|
62
|
+
row.values.first
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Return the number of rows affected by the query.
|
67
|
+
#
|
68
|
+
# @return [Fixnum]
|
69
|
+
# the number of rows affected
|
70
|
+
def affected_rows
|
71
|
+
info[:affected_rows].to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
# Get the number of rows in the result.
|
75
|
+
#
|
76
|
+
# Many drivers provide the count, otherwise it will be computed at runtime.
|
77
|
+
#
|
78
|
+
# @return Fixnum
|
79
|
+
# the number of rows in the Result
|
80
|
+
def count
|
81
|
+
if info[:count].nil? || block_given?
|
82
|
+
super
|
83
|
+
else
|
84
|
+
info[:count]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Iterate over all rows returned by the connection.
|
89
|
+
#
|
90
|
+
# For each row, a Symbol-keyed Hash is yielded into the block.
|
91
|
+
def each(&block)
|
92
|
+
tap{ @tuples.each(&block) }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
##
|
2
|
+
# RDO: Ruby Data Objects.
|
3
|
+
# Copyright © 2012 Chris Corbyn.
|
4
|
+
#
|
5
|
+
# See LICENSE file for details.
|
6
|
+
##
|
7
|
+
|
8
|
+
require "forwardable"
|
9
|
+
|
10
|
+
module RDO
|
11
|
+
# Represents a prepared statement.
|
12
|
+
#
|
13
|
+
# This class actually just wraps a StatementExecutor,
|
14
|
+
# which only needs to conform to a duck-type
|
15
|
+
class Statement
|
16
|
+
extend Forwardable
|
17
|
+
|
18
|
+
def_delegators :@executor, :command, :execute
|
19
|
+
|
20
|
+
# Initialize a new Statement wrapping the given StatementExecutor.
|
21
|
+
#
|
22
|
+
# @param [Object] executor
|
23
|
+
# any object that responds to #execute, #connection and #command
|
24
|
+
def initialize(executor)
|
25
|
+
@executor = executor
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/rdo/util.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
##
|
2
|
+
# RDO: Ruby Data Objects.
|
3
|
+
# Copyright © 2012 Chris Corbyn.
|
4
|
+
#
|
5
|
+
# See LICENSE file for details.
|
6
|
+
##
|
7
|
+
|
8
|
+
require "date"
|
9
|
+
require "bigdecimal"
|
10
|
+
|
11
|
+
module RDO
|
12
|
+
# This file contains methods useful for drivers to convert complex types.
|
13
|
+
#
|
14
|
+
# Performing these operations in C would not be any cheaper, since the data
|
15
|
+
# must first be converted into Ruby types anyway.
|
16
|
+
module Util
|
17
|
+
class << self
|
18
|
+
# Convert a String to a Float, taking into account Infinity and NaN.
|
19
|
+
#
|
20
|
+
# @param [String] s
|
21
|
+
# a String that is formatted for a Float;
|
22
|
+
# or Infinity, -Infinity or NaN.
|
23
|
+
#
|
24
|
+
# @return [Float]
|
25
|
+
# a Float that is the same as the input
|
26
|
+
def float(s)
|
27
|
+
case s
|
28
|
+
when "Infinity"
|
29
|
+
Float::INFINITY
|
30
|
+
when "-Infinity"
|
31
|
+
-Float::INFINITY
|
32
|
+
when "NaN"
|
33
|
+
Float::NAN
|
34
|
+
else
|
35
|
+
Float(s)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Convert a String to a BigDecimal.
|
40
|
+
#
|
41
|
+
# @param [String] s
|
42
|
+
# a String that is formatted as a decimal, or NaN
|
43
|
+
#
|
44
|
+
# @return [BigDecimal]
|
45
|
+
# the BigDecimal representation of this number
|
46
|
+
def decimal(s)
|
47
|
+
BigDecimal(s)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Convert a date & time string, without a time zone, into a DateTime.
|
51
|
+
#
|
52
|
+
# This method will parse the DateTime using the system time zone.
|
53
|
+
#
|
54
|
+
# @param [String] s
|
55
|
+
# a date & time string
|
56
|
+
#
|
57
|
+
# @return [DateTime]
|
58
|
+
# a DateTime in the system time zone
|
59
|
+
def date_time_without_zone(s)
|
60
|
+
date_time_with_zone(s + system_time_zone)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Convert a date & time string, with a time zone, into a DateTime.
|
64
|
+
#
|
65
|
+
# @param [String] s
|
66
|
+
# a date & time string, including a time zone
|
67
|
+
#
|
68
|
+
# @return [DateTime]
|
69
|
+
# a DateTime for this input
|
70
|
+
def date_time_with_zone(s)
|
71
|
+
DateTime.parse(s)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Convert a date string into a Date.
|
75
|
+
#
|
76
|
+
# This method understands AD and BC.
|
77
|
+
#
|
78
|
+
# @param [String] s
|
79
|
+
# a string representing a date, possibly BC
|
80
|
+
#
|
81
|
+
# @return [Date]
|
82
|
+
# a Date for this input
|
83
|
+
def date(s)
|
84
|
+
Date.parse(s)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Get the time zone of the local system.
|
88
|
+
#
|
89
|
+
# This is useful—in fact crucial—for ensuring times are represented
|
90
|
+
# correctly.
|
91
|
+
#
|
92
|
+
# Driver developers should use this, where possible, to notify the DBMS
|
93
|
+
# of the client's time zone.
|
94
|
+
#
|
95
|
+
# @return [String]
|
96
|
+
# a string of the form '+10:00', or '-09:30'
|
97
|
+
def system_time_zone
|
98
|
+
DateTime.now.zone
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
data/lib/rdo/version.rb
ADDED
data/rdo.gemspec
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/rdo/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["d11wtq"]
|
6
|
+
gem.email = ["chris@w3style.co.uk"]
|
7
|
+
|
8
|
+
gem.description = <<-TEXT.strip.gsub(/^ {2}/, "")
|
9
|
+
== Ruby Data Objects
|
10
|
+
|
11
|
+
If you're building something in Ruby that needs access to a database, you may
|
12
|
+
opt to use an ORM like ActiveRecord, DataMapper or Sequel. But if your needs
|
13
|
+
don't fit well with an ORM (maybe you're even writing an ORM?) then you'll
|
14
|
+
need some other way of talking to your database.
|
15
|
+
|
16
|
+
RDO provides a common interface to a number of RDBMS backends, using a clean
|
17
|
+
Ruby syntax, while supporting all the functionality you'd expect from a robust
|
18
|
+
database connection library:
|
19
|
+
|
20
|
+
- Connect to different types of RDBMS in a consistent way
|
21
|
+
- Type casting
|
22
|
+
- Safe parameterization of queries
|
23
|
+
- Buffered query results
|
24
|
+
- Fetching meta data from executed commands
|
25
|
+
- Access RETURNING values just like any read query
|
26
|
+
- Prepared statements (emulated where no native support exists)
|
27
|
+
- Simple core ruby data types
|
28
|
+
|
29
|
+
=== RDBMS Support
|
30
|
+
|
31
|
+
Support for each RDBMS is provided in separate gems, so as to minimize the
|
32
|
+
installation requirements. Many gems are maintained by separate users who
|
33
|
+
work more closely with those RDBMS's.
|
34
|
+
|
35
|
+
Due to the nature of this gem, most of the nitty-gritty code is actually
|
36
|
+
written in C.
|
37
|
+
|
38
|
+
See the official README for full details.
|
39
|
+
TEXT
|
40
|
+
|
41
|
+
gem.summary = "RDO—Ruby Data Objects—A robust RDBMS connection layer"
|
42
|
+
gem.homepage = "https://github.com/d11wtq/rdo"
|
43
|
+
|
44
|
+
gem.files = `git ls-files`.split($\)
|
45
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
46
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
47
|
+
gem.name = "rdo"
|
48
|
+
gem.require_paths = ["lib"]
|
49
|
+
gem.version = RDO::VERSION
|
50
|
+
|
51
|
+
gem.add_development_dependency "rspec"
|
52
|
+
end
|