data_objects 0.2.0 → 0.9.2
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/README +3 -3
- data/Rakefile +42 -22
- data/TODO +0 -5
- data/lib/data_objects.rb +35 -337
- data/lib/data_objects/command.rb +30 -0
- data/lib/data_objects/connection.rb +88 -0
- data/lib/data_objects/field.rb +19 -0
- data/lib/data_objects/logger.rb +233 -0
- data/lib/data_objects/quoting.rb +98 -0
- data/lib/data_objects/reader.rb +22 -0
- data/lib/data_objects/result.rb +13 -0
- data/lib/data_objects/support/pooling.rb +236 -0
- data/lib/data_objects/transaction.rb +42 -0
- data/spec/command_spec.rb +37 -0
- data/spec/connection_spec.rb +83 -0
- data/spec/dataobjects_spec.rb +1 -0
- data/spec/do_mock.rb +31 -0
- data/spec/reader_spec.rb +18 -0
- data/spec/result_spec.rb +23 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/support/pooling_spec.rb +374 -0
- data/spec/transaction_spec.rb +39 -0
- metadata +80 -36
@@ -0,0 +1,30 @@
|
|
1
|
+
module DataObjects
|
2
|
+
class Command
|
3
|
+
|
4
|
+
attr_reader :connection
|
5
|
+
|
6
|
+
# initialize creates a new Command object
|
7
|
+
def initialize(connection, text)
|
8
|
+
raise ArgumentError.new("+connection+ must be a DataObjects::Connection") unless DataObjects::Connection === connection
|
9
|
+
@connection, @text = connection, text
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute_non_query(*args)
|
13
|
+
raise NotImplementedError.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute_reader(*args)
|
17
|
+
raise NotImplementedError.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def set_types(column_types)
|
21
|
+
raise NotImplementedError.new
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
@text
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'fastthread'
|
6
|
+
rescue LoadError
|
7
|
+
end
|
8
|
+
|
9
|
+
module DataObjects
|
10
|
+
class Connection
|
11
|
+
|
12
|
+
def self.new(uri)
|
13
|
+
uri = uri.is_a?(String) ? Addressable::URI::parse(uri) : uri
|
14
|
+
|
15
|
+
if uri.scheme == 'jdbc'
|
16
|
+
driver_name = uri.path.split(':').first
|
17
|
+
else
|
18
|
+
driver_name = uri.scheme.capitalize
|
19
|
+
end
|
20
|
+
|
21
|
+
DataObjects.const_get(driver_name.capitalize)::Connection.new(uri)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.inherited(target)
|
25
|
+
target.class_eval do
|
26
|
+
|
27
|
+
def self.new(*args)
|
28
|
+
instance = allocate
|
29
|
+
instance.send(:initialize, *args)
|
30
|
+
instance
|
31
|
+
end
|
32
|
+
|
33
|
+
include Extlib::Pooling
|
34
|
+
alias close release
|
35
|
+
end
|
36
|
+
|
37
|
+
if driver_module_name = target.name.split('::')[-2]
|
38
|
+
driver_module = DataObjects::const_get(driver_module_name)
|
39
|
+
driver_module.class_eval <<-EOS
|
40
|
+
def self.logger
|
41
|
+
@logger
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.logger=(logger)
|
45
|
+
@logger = logger
|
46
|
+
end
|
47
|
+
EOS
|
48
|
+
|
49
|
+
driver_module.logger = DataObjects::Logger.new(nil, :off)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
#####################################################
|
54
|
+
# Standard API Definition
|
55
|
+
#####################################################
|
56
|
+
def to_s
|
57
|
+
@uri.to_s
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(uri)
|
61
|
+
raise NotImplementedError.new
|
62
|
+
end
|
63
|
+
|
64
|
+
def dispose
|
65
|
+
raise NotImplementedError.new
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_command(text)
|
69
|
+
concrete_command.new(self, text)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
def concrete_command
|
74
|
+
@concrete_command || begin
|
75
|
+
|
76
|
+
class << self
|
77
|
+
private
|
78
|
+
def concrete_command
|
79
|
+
@concrete_command
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
@concrete_command = DataObjects::const_get(self.class.name.split('::')[-2]).const_get('Command')
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
require "time" # httpdate
|
2
|
+
# ==== Public DataObjects Logger API
|
3
|
+
#
|
4
|
+
# Logger taken from Merb :)
|
5
|
+
#
|
6
|
+
# To replace an existing logger with a new one:
|
7
|
+
# DataObjects::Logger.set_log(log{String, IO},level{Symbol, String})
|
8
|
+
#
|
9
|
+
# Available logging levels are
|
10
|
+
# DataObjects::Logger::{ Fatal, Error, Warn, Info, Debug }
|
11
|
+
#
|
12
|
+
# Logging via:
|
13
|
+
# DataObjects.logger.fatal(message<String>)
|
14
|
+
# DataObjects.logger.error(message<String>)
|
15
|
+
# DataObjects.logger.warn(message<String>)
|
16
|
+
# DataObjects.logger.info(message<String>)
|
17
|
+
# DataObjects.logger.debug(message<String>)
|
18
|
+
#
|
19
|
+
# Flush the buffer to
|
20
|
+
# DataObjects.logger.flush
|
21
|
+
#
|
22
|
+
# Remove the current log object
|
23
|
+
# DataObjects.logger.close
|
24
|
+
#
|
25
|
+
# ==== Private DataObjects Logger API
|
26
|
+
#
|
27
|
+
# To initialize the logger you create a new object, proxies to set_log.
|
28
|
+
# DataObjects::Logger.new(log{String, IO},level{Symbol, String})
|
29
|
+
#
|
30
|
+
# Logger will not create the file until something is actually logged
|
31
|
+
# This avoids file creation on DataObjects init when it creates the
|
32
|
+
# default logger.
|
33
|
+
module DataObjects
|
34
|
+
|
35
|
+
class << self #:nodoc:
|
36
|
+
attr_accessor :logger
|
37
|
+
end
|
38
|
+
|
39
|
+
class Logger
|
40
|
+
|
41
|
+
attr_accessor :aio
|
42
|
+
attr_accessor :delimiter
|
43
|
+
attr_reader :level
|
44
|
+
attr_reader :buffer
|
45
|
+
attr_reader :log
|
46
|
+
|
47
|
+
# @note
|
48
|
+
# Ruby (standard) logger levels:
|
49
|
+
# off: absolutely nothing
|
50
|
+
# fatal: an unhandleable error that results in a program crash
|
51
|
+
# error: a handleable error condition
|
52
|
+
# warn: a warning
|
53
|
+
# info: generic (useful) information about system operation
|
54
|
+
# debug: low-level information for developers
|
55
|
+
#
|
56
|
+
# DataObjects::Logger::LEVELS[:off, :fatal, :error, :warn, :info, :debug]
|
57
|
+
LEVELS =
|
58
|
+
{
|
59
|
+
:off => 99999,
|
60
|
+
:fatal => 7,
|
61
|
+
:error => 6,
|
62
|
+
:warn => 4,
|
63
|
+
:info => 3,
|
64
|
+
:debug => 0
|
65
|
+
}
|
66
|
+
|
67
|
+
def level=(new_level)
|
68
|
+
@level = LEVELS[new_level.to_sym]
|
69
|
+
reset_methods(:close)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# The idea here is that instead of performing an 'if' conditional check on
|
75
|
+
# each logging we do it once when the log object is setup
|
76
|
+
def set_write_method
|
77
|
+
@log.instance_eval do
|
78
|
+
|
79
|
+
# Determine if asynchronous IO can be used
|
80
|
+
def aio?
|
81
|
+
@aio = !RUBY_PLATFORM.match(/java|mswin/) &&
|
82
|
+
!(@log == STDOUT) &&
|
83
|
+
@log.respond_to?(:write_nonblock)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Define the write method based on if aio an be used
|
87
|
+
undef write_method if defined? write_method
|
88
|
+
if aio?
|
89
|
+
alias :write_method :write_nonblock
|
90
|
+
else
|
91
|
+
alias :write_method :write
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def initialize_log(log)
|
97
|
+
close if @log # be sure that we don't leave open files laying around.
|
98
|
+
@log = log || "log/dm.log"
|
99
|
+
end
|
100
|
+
|
101
|
+
def reset_methods(o_or_c)
|
102
|
+
if o_or_c == :open
|
103
|
+
alias internal_push push_opened
|
104
|
+
elsif o_or_c == :close
|
105
|
+
alias internal_push push_closed
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def push_opened(string)
|
110
|
+
message = Time.now.httpdate
|
111
|
+
message << delimiter
|
112
|
+
message << string
|
113
|
+
message << "\n" unless message[-1] == ?\n
|
114
|
+
@buffer << message
|
115
|
+
flush # Force a flush for now until we figure out where we want to use the buffering.
|
116
|
+
end
|
117
|
+
|
118
|
+
def push_closed(string)
|
119
|
+
unless @log.respond_to?(:write)
|
120
|
+
log = Pathname(@log)
|
121
|
+
log.dirname.mkpath
|
122
|
+
@log = log.open('a')
|
123
|
+
@log.sync = true
|
124
|
+
end
|
125
|
+
set_write_method
|
126
|
+
reset_methods(:open)
|
127
|
+
push(string)
|
128
|
+
end
|
129
|
+
|
130
|
+
alias internal_push push_closed
|
131
|
+
|
132
|
+
def prep_msg(message, level)
|
133
|
+
level << delimiter << message
|
134
|
+
end
|
135
|
+
|
136
|
+
public
|
137
|
+
|
138
|
+
# To initialize the logger you create a new object, proxies to set_log.
|
139
|
+
# DataObjects::Logger.new(log{String, IO},level{Symbol, String})
|
140
|
+
#
|
141
|
+
# @param log<IO,String> either an IO object or a name of a logfile.
|
142
|
+
# @param log_level<String> the message string to be logged
|
143
|
+
# @param delimiter<String> delimiter to use between message sections
|
144
|
+
# @param log_creation<Boolean> log that the file is being created
|
145
|
+
def initialize(*args)
|
146
|
+
set_log(*args)
|
147
|
+
end
|
148
|
+
|
149
|
+
# To replace an existing logger with a new one:
|
150
|
+
# DataObjects::Logger.set_log(log{String, IO},level{Symbol, String})
|
151
|
+
#
|
152
|
+
#
|
153
|
+
# @param log<IO,String> either an IO object or a name of a logfile.
|
154
|
+
# @param log_level<Symbol> a symbol representing the log level from
|
155
|
+
# {:off, :fatal, :error, :warn, :info, :debug}
|
156
|
+
# @param delimiter<String> delimiter to use between message sections
|
157
|
+
# @param log_creation<Boolean> log that the file is being created
|
158
|
+
def set_log(log, log_level = :off, delimiter = " ~ ", log_creation = false)
|
159
|
+
delimiter ||= " ~ "
|
160
|
+
|
161
|
+
if log_level && LEVELS[log_level.to_sym]
|
162
|
+
self.level = log_level.to_sym
|
163
|
+
else
|
164
|
+
self.level = :debug
|
165
|
+
end
|
166
|
+
|
167
|
+
@buffer = []
|
168
|
+
@delimiter = delimiter
|
169
|
+
|
170
|
+
initialize_log(log)
|
171
|
+
|
172
|
+
DataObjects.logger = self
|
173
|
+
|
174
|
+
self.info("Logfile created") if log_creation
|
175
|
+
end
|
176
|
+
|
177
|
+
# Flush the entire buffer to the log object.
|
178
|
+
# DataObjects.logger.flush
|
179
|
+
#
|
180
|
+
def flush
|
181
|
+
return unless @buffer.size > 0
|
182
|
+
@log.write_method(@buffer.slice!(0..-1).to_s)
|
183
|
+
end
|
184
|
+
|
185
|
+
# Close and remove the current log object.
|
186
|
+
# DataObjects.logger.close
|
187
|
+
#
|
188
|
+
def close
|
189
|
+
flush
|
190
|
+
@log.close if @log.respond_to?(:close)
|
191
|
+
@log = nil
|
192
|
+
end
|
193
|
+
|
194
|
+
# Appends a string and log level to logger's buffer.
|
195
|
+
|
196
|
+
# @note
|
197
|
+
# Note that the string is discarded if the string's log level less than the
|
198
|
+
# logger's log level.
|
199
|
+
# @note
|
200
|
+
# Note that if the logger is aio capable then the logger will use
|
201
|
+
# non-blocking asynchronous writes.
|
202
|
+
#
|
203
|
+
# @param level<Fixnum> the logging level as an integer
|
204
|
+
# @param string<String> the message string to be logged
|
205
|
+
def push(string)
|
206
|
+
internal_push(string)
|
207
|
+
end
|
208
|
+
alias << push
|
209
|
+
|
210
|
+
# Generate the following logging methods for DataObjects.logger as described
|
211
|
+
# in the API:
|
212
|
+
# :fatal, :error, :warn, :info, :debug
|
213
|
+
# :off only gets an off? method
|
214
|
+
LEVELS.each_pair do |name, number|
|
215
|
+
unless name.to_sym == :off
|
216
|
+
class_eval <<-EOS
|
217
|
+
# DOC
|
218
|
+
def #{name}(message)
|
219
|
+
self.<<( prep_msg(message, "#{name}") ) if #{name}?
|
220
|
+
end
|
221
|
+
EOS
|
222
|
+
end
|
223
|
+
|
224
|
+
class_eval <<-EOS
|
225
|
+
# DOC
|
226
|
+
def #{name}?
|
227
|
+
#{number} >= level
|
228
|
+
end
|
229
|
+
EOS
|
230
|
+
end
|
231
|
+
|
232
|
+
end # class Logger
|
233
|
+
end # module DataObjects
|
@@ -0,0 +1,98 @@
|
|
1
|
+
module DataObjects
|
2
|
+
|
3
|
+
module Quoting
|
4
|
+
# Escape a string of SQL with a set of arguments.
|
5
|
+
# The first argument is assumed to be the SQL to escape,
|
6
|
+
# the remaining arguments (if any) are assumed to be
|
7
|
+
# values to escape and interpolate.
|
8
|
+
#
|
9
|
+
# ==== Examples
|
10
|
+
# escape_sql("SELECT * FROM zoos")
|
11
|
+
# # => "SELECT * FROM zoos"
|
12
|
+
#
|
13
|
+
# escape_sql("SELECT * FROM zoos WHERE name = ?", "Dallas")
|
14
|
+
# # => "SELECT * FROM zoos WHERE name = `Dallas`"
|
15
|
+
#
|
16
|
+
# escape_sql("SELECT * FROM zoos WHERE name = ? AND acreage > ?", "Dallas", 40)
|
17
|
+
# # => "SELECT * FROM zoos WHERE name = `Dallas` AND acreage > 40"
|
18
|
+
#
|
19
|
+
# ==== Warning
|
20
|
+
# This method is meant mostly for adapters that don't support
|
21
|
+
# bind-parameters.
|
22
|
+
def escape_sql(args)
|
23
|
+
sql = @text.dup
|
24
|
+
|
25
|
+
unless args.empty?
|
26
|
+
sql.gsub!(/\?/) do |x|
|
27
|
+
quote_value(args.shift)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
sql
|
32
|
+
end
|
33
|
+
|
34
|
+
def quote_value(value)
|
35
|
+
return 'NULL' if value.nil?
|
36
|
+
|
37
|
+
case value
|
38
|
+
when Numeric then quote_numeric(value)
|
39
|
+
when String then quote_string(value)
|
40
|
+
when Class then quote_class(value)
|
41
|
+
when Time then quote_time(value)
|
42
|
+
when DateTime then quote_datetime(value)
|
43
|
+
when Date then quote_date(value)
|
44
|
+
when TrueClass, FalseClass then quote_boolean(value)
|
45
|
+
when Array then quote_array(value)
|
46
|
+
when Range then quote_range(value)
|
47
|
+
when Symbol then quote_symbol(value)
|
48
|
+
else
|
49
|
+
if value.respond_to?(:to_sql)
|
50
|
+
value.to_sql
|
51
|
+
else
|
52
|
+
raise "Don't know how to quote #{value.inspect}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def quote_symbol(value)
|
58
|
+
quote_string(value.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
def quote_numeric(value)
|
62
|
+
value.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
def quote_string(value)
|
66
|
+
"'#{value.gsub("'", "''")}'"
|
67
|
+
end
|
68
|
+
|
69
|
+
def quote_class(value)
|
70
|
+
quote_string(value.name)
|
71
|
+
end
|
72
|
+
|
73
|
+
def quote_time(value)
|
74
|
+
"'#{value.strftime('%Y-%m-%d %H:%M:%S')}" + (value.usec > 0 ? ".#{value.usec.to_s.rjust(6, '0')}'" : "'")
|
75
|
+
end
|
76
|
+
|
77
|
+
def quote_datetime(value)
|
78
|
+
"'#{value.dup}'"
|
79
|
+
end
|
80
|
+
|
81
|
+
def quote_date(value)
|
82
|
+
"'#{value.strftime("%Y-%m-%d")}'"
|
83
|
+
end
|
84
|
+
|
85
|
+
def quote_boolean(value)
|
86
|
+
value.to_s.upcase
|
87
|
+
end
|
88
|
+
|
89
|
+
def quote_array(value)
|
90
|
+
"(#{value.map { |entry| quote_value(entry) }.join(', ')})"
|
91
|
+
end
|
92
|
+
|
93
|
+
def quote_range(value)
|
94
|
+
"#{quote_value(value.first)} AND #{quote_value(value.last)}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|