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