activerecord 1.3.0 → 1.4.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- data/CHANGELOG +77 -2
- data/install.rb +5 -0
- data/lib/active_record.rb +6 -2
- data/lib/active_record/acts/list.rb +56 -45
- data/lib/active_record/acts/tree.rb +3 -2
- data/lib/active_record/associations.rb +10 -62
- data/lib/active_record/associations/association_collection.rb +20 -23
- data/lib/active_record/associations/has_and_belongs_to_many_association.rb +36 -10
- data/lib/active_record/associations/has_many_association.rb +50 -25
- data/lib/active_record/base.rb +118 -80
- data/lib/active_record/callbacks.rb +51 -50
- data/lib/active_record/connection_adapters/abstract_adapter.rb +33 -12
- data/lib/active_record/connection_adapters/db2_adapter.rb +129 -0
- data/lib/active_record/connection_adapters/sqlite_adapter.rb +23 -1
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +80 -90
- data/lib/active_record/fixtures.rb +1 -1
- data/lib/active_record/locking.rb +57 -0
- data/lib/active_record/support/class_attribute_accessors.rb +19 -5
- data/lib/active_record/support/class_inheritable_attributes.rb +4 -3
- data/lib/active_record/support/dependencies.rb +71 -0
- data/lib/active_record/support/inflector.rb +1 -0
- data/lib/active_record/support/misc.rb +29 -3
- data/lib/active_record/support/module_attribute_accessors.rb +57 -0
- data/lib/active_record/transactions.rb +18 -11
- data/lib/active_record/validations.rb +3 -3
- data/lib/active_record/vendor/db2.rb +357 -0
- data/lib/active_record/vendor/mysql.rb +1 -1
- data/rakefile +17 -5
- data/test/associations_test.rb +39 -4
- data/test/base_test.rb +13 -4
- data/test/binary_test.rb +43 -0
- data/test/callbacks_test.rb +230 -0
- data/test/connections/native_db2/connection.rb +24 -0
- data/test/connections/native_sqlserver/connection.rb +9 -3
- data/test/deprecated_associations_test.rb +16 -10
- data/test/finder_test.rb +65 -13
- data/test/fixtures/associations.png +0 -0
- data/test/fixtures/binary.rb +2 -0
- data/test/fixtures/companies.yml +21 -0
- data/test/fixtures/courses.yml +7 -0
- data/test/fixtures/customers.yml +7 -0
- data/test/fixtures/db_definitions/db2.sql +124 -0
- data/test/fixtures/db_definitions/db22.sql +4 -0
- data/test/fixtures/db_definitions/mysql.sql +12 -0
- data/test/fixtures/db_definitions/postgresql.sql +13 -0
- data/test/fixtures/db_definitions/sqlite.sql +9 -0
- data/test/fixtures/db_definitions/sqlserver.sql +13 -0
- data/test/fixtures/developers_projects.yml +13 -0
- data/test/fixtures/entrants.yml +14 -0
- data/test/fixtures/fixture_database.sqlite +0 -0
- data/test/fixtures/movies.yml +7 -0
- data/test/fixtures/people.yml +3 -0
- data/test/fixtures/person.rb +1 -0
- data/test/fixtures/projects.yml +7 -0
- data/test/fixtures/topics.yml +21 -0
- data/test/locking_test.rb +34 -0
- data/test/mixin_test.rb +6 -0
- data/test/validations_test.rb +1 -1
- metadata +33 -29
- data/test/fixtures/companies/first_client +0 -6
- data/test/fixtures/companies/first_firm +0 -4
- data/test/fixtures/companies/second_client +0 -6
- data/test/fixtures/courses/java +0 -2
- data/test/fixtures/courses/ruby +0 -2
- data/test/fixtures/customers/david +0 -6
- data/test/fixtures/entrants/first +0 -3
- data/test/fixtures/entrants/second +0 -3
- data/test/fixtures/entrants/third +0 -3
- data/test/fixtures/movies/first +0 -2
- data/test/fixtures/movies/second +0 -2
- data/test/fixtures/projects/action_controller +0 -2
- data/test/fixtures/projects/active_record +0 -2
- data/test/fixtures/topics/first +0 -10
- data/test/fixtures/topics/second +0 -8
- data/test/inflector_test.rb +0 -122
@@ -0,0 +1,57 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Locking
|
3
|
+
# Active Records support optimistic locking if the field <tt>lock_version</tt> is present. Each update to the
|
4
|
+
# record increments the lock_version column and the locking facilities ensure that records instantiated twice
|
5
|
+
# will let the last one saved raise a StaleObjectError if the first was also updated. Example:
|
6
|
+
#
|
7
|
+
# p1 = Person.find(1)
|
8
|
+
# p2 = Person.find(1)
|
9
|
+
#
|
10
|
+
# p1.first_name = "Michael"
|
11
|
+
# p1.save
|
12
|
+
#
|
13
|
+
# p2.first_name = "should fail"
|
14
|
+
# p2.save # Raises a ActiveRecord::StaleObjectError
|
15
|
+
#
|
16
|
+
# You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
|
17
|
+
# or otherwise apply the business logic needed to resolve the conflict.
|
18
|
+
#
|
19
|
+
# You must ensure that your database schema defaults the lock_version column to 0.
|
20
|
+
#
|
21
|
+
# This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>.
|
22
|
+
def self.append_features(base)
|
23
|
+
super
|
24
|
+
base.class_eval do
|
25
|
+
alias_method :update_without_lock, :update
|
26
|
+
alias_method :update, :update_with_lock
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def update_with_lock
|
31
|
+
if locking_enabled?
|
32
|
+
previous_value = self.lock_version
|
33
|
+
self.lock_version = previous_value + 1
|
34
|
+
|
35
|
+
affected_rows = connection.update(
|
36
|
+
"UPDATE #{self.class.table_name} "+
|
37
|
+
"SET #{quoted_comma_pair_list(connection, attributes_with_quotes(false))} " +
|
38
|
+
"WHERE #{self.class.primary_key} = #{quote(id)} AND lock_version = #{quote(previous_value)}",
|
39
|
+
"#{self.class.name} Update with optimistic locking"
|
40
|
+
)
|
41
|
+
|
42
|
+
raise(ActiveRecord::StaleObjectError, "Attempted to update a stale object") unless affected_rows == 1
|
43
|
+
else
|
44
|
+
update_without_lock
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Base
|
50
|
+
@@lock_optimistically = true
|
51
|
+
cattr_accessor :lock_optimistically
|
52
|
+
|
53
|
+
def locking_enabled?
|
54
|
+
lock_optimistically && respond_to?(:lock_version)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,5 +1,6 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# Extends the class object with class and instance accessors for class attributes,
|
2
|
+
# just like the native attr* accessors for instance attributes.
|
3
|
+
class Class # :nodoc:
|
3
4
|
def cattr_reader(*syms)
|
4
5
|
syms.each do |sym|
|
5
6
|
class_eval <<-EOS
|
@@ -12,7 +13,16 @@ class Class #:nodoc:
|
|
12
13
|
end
|
13
14
|
|
14
15
|
def #{sym.id2name}
|
15
|
-
|
16
|
+
@@#{sym}
|
17
|
+
end
|
18
|
+
|
19
|
+
def call_#{sym.id2name}
|
20
|
+
case @@#{sym.id2name}
|
21
|
+
when Symbol then send(@@#{sym})
|
22
|
+
when Proc then @@#{sym}.call(self)
|
23
|
+
when String then @@#{sym}
|
24
|
+
else nil
|
25
|
+
end
|
16
26
|
end
|
17
27
|
EOS
|
18
28
|
end
|
@@ -29,8 +39,12 @@ class Class #:nodoc:
|
|
29
39
|
@@#{sym.id2name} = obj
|
30
40
|
end
|
31
41
|
|
42
|
+
def self.set_#{sym.id2name}(obj)
|
43
|
+
@@#{sym.id2name} = obj
|
44
|
+
end
|
45
|
+
|
32
46
|
def #{sym.id2name}=(obj)
|
33
|
-
|
47
|
+
@@#{sym} = obj
|
34
48
|
end
|
35
49
|
EOS
|
36
50
|
end
|
@@ -39,5 +53,5 @@ class Class #:nodoc:
|
|
39
53
|
def cattr_accessor(*syms)
|
40
54
|
cattr_reader(*syms)
|
41
55
|
cattr_writer(*syms)
|
42
|
-
end
|
56
|
+
end
|
43
57
|
end
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Allows attributes to be shared within an inheritance hierarchy, but where each
|
1
|
+
# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
|
2
2
|
# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
|
3
3
|
# to, for example, an array without those additions being shared with either their parent, siblings, or
|
4
4
|
# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
|
@@ -31,10 +31,11 @@ module ClassInheritableAttributes # :nodoc:
|
|
31
31
|
def reset_inheritable_attributes
|
32
32
|
inheritable_attributes.clear
|
33
33
|
end
|
34
|
-
|
34
|
+
|
35
35
|
private
|
36
36
|
def inherited(child)
|
37
37
|
@@classes[child] = inheritable_attributes.dup
|
38
|
-
end
|
38
|
+
end
|
39
|
+
|
39
40
|
end
|
40
41
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/module_attribute_accessors'
|
2
|
+
|
3
|
+
module Dependencies
|
4
|
+
extend self
|
5
|
+
|
6
|
+
@@loaded = [ ]
|
7
|
+
mattr_accessor :loaded
|
8
|
+
|
9
|
+
@@mechanism = :load
|
10
|
+
mattr_accessor :mechanism
|
11
|
+
|
12
|
+
def depend_on(file_name, swallow_load_errors = false)
|
13
|
+
if !loaded.include?(file_name)
|
14
|
+
loaded << file_name
|
15
|
+
|
16
|
+
begin
|
17
|
+
require_or_load(file_name)
|
18
|
+
rescue LoadError
|
19
|
+
raise unless swallow_load_errors
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def associate_with(file_name)
|
25
|
+
depend_on(file_name, true)
|
26
|
+
end
|
27
|
+
|
28
|
+
def reload
|
29
|
+
old_loaded = loaded
|
30
|
+
clear
|
31
|
+
|
32
|
+
old_loaded.each do |file_name|
|
33
|
+
begin
|
34
|
+
silence_warnings { load("#{file_name}.rb") }
|
35
|
+
rescue LoadError
|
36
|
+
# The association didn't reside in its own file, so we assume it was required by other means
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def clear
|
42
|
+
self.loaded = [ ]
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def require_or_load(file_name)
|
47
|
+
mechanism == :load ? silence_warnings { load("#{file_name}.rb") } : require(file_name)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
Object.send(:define_method, :require_dependency) { |file_name| Dependencies.depend_on(file_name) } unless Object.respond_to?(:require_dependency)
|
52
|
+
Object.send(:define_method, :require_association) { |file_name| Dependencies.associate_with(file_name) } unless Object.respond_to?(:require_association)
|
53
|
+
|
54
|
+
class Object
|
55
|
+
class << self
|
56
|
+
# Use const_missing to autoload associations so we don't have to
|
57
|
+
# require_association when using single-table inheritance.
|
58
|
+
unless respond_to?(:pre_dependency_const_missing)
|
59
|
+
alias_method :pre_dependency_const_missing, :const_missing
|
60
|
+
|
61
|
+
def const_missing(class_id)
|
62
|
+
begin
|
63
|
+
require_dependency(Inflector.underscore(Inflector.demodulize(class_id.to_s)))
|
64
|
+
return Object.const_get(class_id) if Object.const_defined?(class_id)
|
65
|
+
rescue LoadError
|
66
|
+
pre_dependency_const_missing(class_id)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -48,6 +48,7 @@ module Inflector
|
|
48
48
|
def plural_rules #:doc:
|
49
49
|
[
|
50
50
|
[/(x|ch|ss)$/, '\1es'], # search, switch, fix, box, process, address
|
51
|
+
[/([^aeiouy]|qu)ies$/, '\1y'],
|
51
52
|
[/([^aeiouy]|qu)y$/, '\1ies'], # query, ability, agency
|
52
53
|
[/(?:([^f])fe|([lr])f)$/, '\1\2ves'], # half, safe, wife
|
53
54
|
[/sis$/, 'ses'], # basis, diagnosis
|
@@ -1,6 +1,32 @@
|
|
1
1
|
def silence_warnings
|
2
2
|
old_verbose, $VERBOSE = $VERBOSE, nil
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
begin
|
4
|
+
yield
|
5
|
+
ensure
|
6
|
+
$VERBOSE = old_verbose
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Hash
|
11
|
+
# Return a new hash with all keys converted to symbols.
|
12
|
+
def symbolize_keys
|
13
|
+
inject({}) do |options, (key, value)|
|
14
|
+
options[key.to_sym] = value
|
15
|
+
options
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Destructively convert all keys to symbols.
|
20
|
+
def symbolize_keys!
|
21
|
+
keys.each do |key|
|
22
|
+
unless key.is_a?(Symbol)
|
23
|
+
self[key.to_sym] = self[key]
|
24
|
+
delete(key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
alias_method :to_options, :symbolize_keys
|
31
|
+
alias_method :to_options!, :symbolize_keys!
|
6
32
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Extends the module object with module and instance accessors for class attributes,
|
2
|
+
# just like the native attr* accessors for instance attributes.
|
3
|
+
class Module # :nodoc:
|
4
|
+
def mattr_reader(*syms)
|
5
|
+
syms.each do |sym|
|
6
|
+
class_eval <<-EOS
|
7
|
+
if ! defined? @@#{sym.id2name}
|
8
|
+
@@#{sym.id2name} = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.#{sym.id2name}
|
12
|
+
@@#{sym}
|
13
|
+
end
|
14
|
+
|
15
|
+
def #{sym.id2name}
|
16
|
+
@@#{sym}
|
17
|
+
end
|
18
|
+
|
19
|
+
def call_#{sym.id2name}
|
20
|
+
case @@#{sym.id2name}
|
21
|
+
when Symbol then send(@@#{sym})
|
22
|
+
when Proc then @@#{sym}.call(self)
|
23
|
+
when String then @@#{sym}
|
24
|
+
else nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
EOS
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def mattr_writer(*syms)
|
32
|
+
syms.each do |sym|
|
33
|
+
class_eval <<-EOS
|
34
|
+
if ! defined? @@#{sym.id2name}
|
35
|
+
@@#{sym.id2name} = nil
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.#{sym.id2name}=(obj)
|
39
|
+
@@#{sym.id2name} = obj
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.set_#{sym.id2name}(obj)
|
43
|
+
@@#{sym.id2name} = obj
|
44
|
+
end
|
45
|
+
|
46
|
+
def #{sym.id2name}=(obj)
|
47
|
+
@@#{sym} = obj
|
48
|
+
end
|
49
|
+
EOS
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def mattr_accessor(*syms)
|
54
|
+
mattr_reader(*syms)
|
55
|
+
mattr_writer(*syms)
|
56
|
+
end
|
57
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'active_record/vendor/simple.rb'
|
2
|
+
Transaction::Simple.send(:remove_method, :transaction)
|
2
3
|
require 'thread'
|
3
4
|
|
4
5
|
module ActiveRecord
|
@@ -75,14 +76,10 @@ module ActiveRecord
|
|
75
76
|
# should be ready to catch those in your application code.
|
76
77
|
#
|
77
78
|
# Tribute: Object-level transactions are implemented by Transaction::Simple by Austin Ziegler.
|
78
|
-
module ClassMethods
|
79
|
+
module ClassMethods
|
79
80
|
def transaction(*objects, &block)
|
80
|
-
|
81
|
-
|
82
|
-
Thread.current['start_db_transaction'] = (Thread.current['open_transactions'] == 0)
|
83
|
-
Thread.current['open_transactions'] += 1
|
84
|
-
end
|
85
|
-
|
81
|
+
lock_mutex
|
82
|
+
|
86
83
|
begin
|
87
84
|
objects.each { |o| o.extend(Transaction::Simple) }
|
88
85
|
objects.each { |o| o.start_transaction }
|
@@ -95,11 +92,21 @@ module ActiveRecord
|
|
95
92
|
objects.each { |o| o.abort_transaction }
|
96
93
|
raise
|
97
94
|
ensure
|
98
|
-
|
99
|
-
Thread.current['open_transactions'] -= 1
|
100
|
-
end
|
95
|
+
unlock_mutex
|
101
96
|
end
|
102
97
|
end
|
98
|
+
|
99
|
+
def lock_mutex
|
100
|
+
Thread.current['open_transactions'] ||= 0
|
101
|
+
TRANSACTION_MUTEX.lock if Thread.current['open_transactions'] == 0
|
102
|
+
Thread.current['start_db_transaction'] = (Thread.current['open_transactions'] == 0)
|
103
|
+
Thread.current['open_transactions'] += 1
|
104
|
+
end
|
105
|
+
|
106
|
+
def unlock_mutex
|
107
|
+
Thread.current['open_transactions'] -= 1
|
108
|
+
TRANSACTION_MUTEX.unlock if Thread.current['open_transactions'] == 0
|
109
|
+
end
|
103
110
|
end
|
104
111
|
|
105
112
|
def transaction(*objects, &block)
|
@@ -114,4 +121,4 @@ module ActiveRecord
|
|
114
121
|
transaction { save_without_transactions(perform_validation) }
|
115
122
|
end
|
116
123
|
end
|
117
|
-
end
|
124
|
+
end
|
@@ -213,9 +213,9 @@ module ActiveRecord
|
|
213
213
|
|
214
214
|
for attr_name in attr_names
|
215
215
|
if scope = configuration[:scope]
|
216
|
-
class_eval(%(validate %{errors.add('#{attr_name}', '#{configuration[:message]}') if self.class.find_first(new_record? ? ['#{attr_name} = ? AND #{scope} = ?', #{attr_name}, #{scope}] : ["#{attr_name} = ? AND
|
216
|
+
class_eval(%(validate %{errors.add('#{attr_name}', '#{configuration[:message]}') if self.class.find_first(new_record? ? ['#{attr_name} = ? AND #{scope} = ?', #{attr_name}, #{scope}] : ["#{attr_name} = ? AND \\\#{self.class.primary_key} <> ? AND #{scope} = ?", #{attr_name}, id, #{scope}])}))
|
217
217
|
else
|
218
|
-
class_eval(%(validate %{errors.add('#{attr_name}', '#{configuration[:message]}') if self.class.find_first(new_record? ? ['#{attr_name} = ?', #{attr_name}] : ["#{attr_name} = ? AND
|
218
|
+
class_eval(%(validate %{errors.add('#{attr_name}', '#{configuration[:message]}') if self.class.find_first(new_record? ? ['#{attr_name} = ?', #{attr_name}] : ["#{attr_name} = ? AND \\\#{self.class.primary_key} <> ?", #{attr_name}, id])}))
|
219
219
|
end
|
220
220
|
end
|
221
221
|
end
|
@@ -240,7 +240,7 @@ module ActiveRecord
|
|
240
240
|
raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp)
|
241
241
|
|
242
242
|
for attr_name in attr_names
|
243
|
-
class_eval(%(#{validation_method(configuration[:on])} %{errors.add("#{attr_name}", "#{configuration[:message]}") unless #{attr_name} and #{attr_name}.to_s.match(/#{configuration[:with]}/)}))
|
243
|
+
class_eval(%(#{validation_method(configuration[:on])} %{errors.add("#{attr_name}", "#{configuration[:message]}") unless #{attr_name} and #{attr_name}.to_s.match(/#{Regexp.quote(configuration[:with].source)}/)}))
|
244
244
|
end
|
245
245
|
end
|
246
246
|
|
@@ -0,0 +1,357 @@
|
|
1
|
+
require 'db2/db2cli.rb'
|
2
|
+
|
3
|
+
module DB2
|
4
|
+
module DB2Util
|
5
|
+
include DB2CLI
|
6
|
+
|
7
|
+
def free() SQLFreeHandle(@handle_type, @handle); end
|
8
|
+
def handle() @handle; end
|
9
|
+
|
10
|
+
def check_rc(rc)
|
11
|
+
if ![SQL_SUCCESS, SQL_SUCCESS_WITH_INFO, SQL_NO_DATA_FOUND].include?(rc)
|
12
|
+
rec = 1
|
13
|
+
msg = ''
|
14
|
+
loop do
|
15
|
+
a = SQLGetDiagRec(@handle_type, @handle, rec, 500)
|
16
|
+
break if a[0] != SQL_SUCCESS
|
17
|
+
msg << a[3] if !a[3].nil? and a[3] != '' # Create message.
|
18
|
+
rec += 1
|
19
|
+
end
|
20
|
+
raise "DB2 error: #{msg}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class Environment
|
26
|
+
include DB2Util
|
27
|
+
|
28
|
+
def initialize
|
29
|
+
@handle_type = SQL_HANDLE_ENV
|
30
|
+
rc, @handle = SQLAllocHandle(@handle_type, SQL_NULL_HANDLE)
|
31
|
+
check_rc(rc)
|
32
|
+
end
|
33
|
+
|
34
|
+
def data_sources(buffer_length = 1024)
|
35
|
+
retval = []
|
36
|
+
max_buffer_length = buffer_length
|
37
|
+
|
38
|
+
a = SQLDataSources(@handle, SQL_FETCH_FIRST, SQL_MAX_DSN_LENGTH + 1, buffer_length)
|
39
|
+
retval << [a[1], a[3]]
|
40
|
+
max_buffer_length = [max_buffer_length, a[4]].max
|
41
|
+
|
42
|
+
loop do
|
43
|
+
a = SQLDataSources(@handle, SQL_FETCH_NEXT, SQL_MAX_DSN_LENGTH + 1, buffer_length)
|
44
|
+
break if a[0] == SQL_NO_DATA_FOUND
|
45
|
+
|
46
|
+
retval << [a[1], a[3]]
|
47
|
+
max_buffer_length = [max_buffer_length, a[4]].max
|
48
|
+
end
|
49
|
+
|
50
|
+
if max_buffer_length > buffer_length
|
51
|
+
get_data_sources(max_buffer_length)
|
52
|
+
else
|
53
|
+
retval
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Connection
|
59
|
+
include DB2Util
|
60
|
+
|
61
|
+
def initialize(environment)
|
62
|
+
@env = environment
|
63
|
+
@handle_type = SQL_HANDLE_DBC
|
64
|
+
rc, @handle = SQLAllocHandle(@handle_type, @env.handle)
|
65
|
+
check_rc(rc)
|
66
|
+
end
|
67
|
+
|
68
|
+
def connect(server_name, user_name = '', auth = '')
|
69
|
+
check_rc(SQLConnect(@handle, server_name, user_name, auth))
|
70
|
+
end
|
71
|
+
|
72
|
+
def set_connect_attr(attr, value)
|
73
|
+
value += "\0" if value.class == String
|
74
|
+
check_rc(SQLSetConnectAttr(@handle, attr, value))
|
75
|
+
end
|
76
|
+
|
77
|
+
def set_auto_commit_on
|
78
|
+
set_connect_attr(SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_ON)
|
79
|
+
end
|
80
|
+
|
81
|
+
def set_auto_commit_off
|
82
|
+
set_connect_attr(SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF)
|
83
|
+
end
|
84
|
+
|
85
|
+
def disconnect
|
86
|
+
check_rc(SQLDisconnect(@handle))
|
87
|
+
end
|
88
|
+
|
89
|
+
def rollback
|
90
|
+
check_rc(SQLEndTran(@handle_type, @handle, SQL_ROLLBACK))
|
91
|
+
end
|
92
|
+
|
93
|
+
def commit
|
94
|
+
check_rc(SQLEndTran(@handle_type, @handle, SQL_COMMIT))
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class Statement
|
99
|
+
include DB2Util
|
100
|
+
|
101
|
+
def initialize(connection)
|
102
|
+
@conn = connection
|
103
|
+
@handle_type = SQL_HANDLE_STMT
|
104
|
+
@parms = [] #yun
|
105
|
+
@sql = '' #yun
|
106
|
+
@numParms = 0 #yun
|
107
|
+
@prepared = false #yun
|
108
|
+
@parmArray = [] #yun. attributes of the parameter markers
|
109
|
+
rc, @handle = SQLAllocHandle(@handle_type, @conn.handle)
|
110
|
+
check_rc(rc)
|
111
|
+
end
|
112
|
+
|
113
|
+
def columns(table_name)
|
114
|
+
check_rc(SQLColumns(@handle, "", "%", table_name, "%"))
|
115
|
+
fetch_all
|
116
|
+
end
|
117
|
+
|
118
|
+
def tables
|
119
|
+
check_rc(SQLTables(@handle, "", "%", "%", "TABLE"))
|
120
|
+
fetch_all
|
121
|
+
end
|
122
|
+
|
123
|
+
def prepare(sql)
|
124
|
+
@sql = sql
|
125
|
+
check_rc(SQLPrepare(@handle, sql))
|
126
|
+
rc, @numParms = SQLNumParams(@handle) #number of question marks
|
127
|
+
check_rc(rc)
|
128
|
+
#--------------------------------------------------------------------------
|
129
|
+
# parameter attributes are stored in instance variable @parmArray so that
|
130
|
+
# they are available when execute method is called.
|
131
|
+
#--------------------------------------------------------------------------
|
132
|
+
if @numParms > 0 # get parameter marker attributes
|
133
|
+
1.upto(@numParms) do |i| # parameter number starts from 1
|
134
|
+
rc, type, size, decimalDigits = SQLDescribeParam(@handle, i)
|
135
|
+
check_rc(rc)
|
136
|
+
@parmArray << Parameter.new(type, size, decimalDigits)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
@prepared = true
|
140
|
+
self
|
141
|
+
end
|
142
|
+
|
143
|
+
def execute(*parms)
|
144
|
+
raise "The statement was not prepared" if @prepared == false
|
145
|
+
|
146
|
+
if parms.size == 1 and parms[0].class == Array
|
147
|
+
parms = parms[0]
|
148
|
+
end
|
149
|
+
|
150
|
+
if @numParms != parms.size
|
151
|
+
raise "Number of parameters supplied does not match with the SQL statement"
|
152
|
+
end
|
153
|
+
|
154
|
+
if @numParms > 0 #need to bind parameters
|
155
|
+
#--------------------------------------------------------------------
|
156
|
+
#calling bindParms may not be safe. Look comment below.
|
157
|
+
#--------------------------------------------------------------------
|
158
|
+
#bindParms(parms)
|
159
|
+
|
160
|
+
valueArray = []
|
161
|
+
1.upto(@numParms) do |i| # parameter number starts from 1
|
162
|
+
type = @parmArray[i - 1].class
|
163
|
+
size = @parmArray[i - 1].size
|
164
|
+
decimalDigits = @parmArray[i - 1].decimalDigits
|
165
|
+
|
166
|
+
if parms[i - 1].class == String
|
167
|
+
valueArray << parms[i - 1]
|
168
|
+
else
|
169
|
+
valueArray << parms[i - 1].to_s
|
170
|
+
end
|
171
|
+
|
172
|
+
rc = SQLBindParameter(@handle, i, type, size, decimalDigits, valueArray[i - 1])
|
173
|
+
check_rc(rc)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
check_rc(SQLExecute(@handle))
|
178
|
+
|
179
|
+
if @numParms != 0
|
180
|
+
check_rc(SQLFreeStmt(@handle, SQL_RESET_PARAMS)) # Reset parameters
|
181
|
+
end
|
182
|
+
|
183
|
+
self
|
184
|
+
end
|
185
|
+
|
186
|
+
#-------------------------------------------------------------------------------
|
187
|
+
# The last argument(value) to SQLBindParameter is a deferred argument, that is,
|
188
|
+
# it should be available when SQLExecute is called. Even though "value" is
|
189
|
+
# local to bindParms method, it seems that it is available when SQLExecute
|
190
|
+
# is called. I am not sure whether it would still work if garbage collection
|
191
|
+
# is done between bindParms call and SQLExecute call inside the execute method
|
192
|
+
# above.
|
193
|
+
#-------------------------------------------------------------------------------
|
194
|
+
def bindParms(parms) # This is the real thing. It uses SQLBindParms
|
195
|
+
1.upto(@numParms) do |i| # parameter number starts from 1
|
196
|
+
rc, dataType, parmSize, decimalDigits = SQLDescribeParam(@handle, i)
|
197
|
+
check_rc(rc)
|
198
|
+
if parms[i - 1].class == String
|
199
|
+
value = parms[i - 1]
|
200
|
+
else
|
201
|
+
value = parms[i - 1].to_s
|
202
|
+
end
|
203
|
+
rc = SQLBindParameter(@handle, i, dataType, parmSize, decimalDigits, value)
|
204
|
+
check_rc(rc)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
#------------------------------------------------------------------------------
|
209
|
+
# bind method does not use DB2's SQLBindParams, but replaces "?" in the
|
210
|
+
# SQL statement with the value before passing the SQL statement to DB2.
|
211
|
+
# It is not efficient and can handle only strings since it puts everything in
|
212
|
+
# quotes.
|
213
|
+
#------------------------------------------------------------------------------
|
214
|
+
def bind(sql, args) #does not use SQLBindParams
|
215
|
+
arg_index = 0
|
216
|
+
result = ""
|
217
|
+
tokens(sql).each do |part|
|
218
|
+
case part
|
219
|
+
when '?'
|
220
|
+
result << "'" + (args[arg_index]) + "'" #put it into quotes
|
221
|
+
arg_index += 1
|
222
|
+
when '??'
|
223
|
+
result << "?"
|
224
|
+
else
|
225
|
+
result << part
|
226
|
+
end
|
227
|
+
end
|
228
|
+
if arg_index < args.size
|
229
|
+
raise "Too many SQL parameters"
|
230
|
+
elsif arg_index > args.size
|
231
|
+
raise "Not enough SQL parameters"
|
232
|
+
end
|
233
|
+
result
|
234
|
+
end
|
235
|
+
|
236
|
+
## Break the sql string into parts.
|
237
|
+
#
|
238
|
+
# This is NOT a full lexer for SQL. It just breaks up the SQL
|
239
|
+
# string enough so that question marks, double question marks and
|
240
|
+
# quoted strings are separated. This is used when binding
|
241
|
+
# arguments to "?" in the SQL string. Note: comments are not
|
242
|
+
# handled.
|
243
|
+
#
|
244
|
+
def tokens(sql)
|
245
|
+
toks = sql.scan(/('([^'\\]|''|\\.)*'|"([^"\\]|""|\\.)*"|\?\??|[^'"?]+)/)
|
246
|
+
toks.collect { |t| t[0] }
|
247
|
+
end
|
248
|
+
|
249
|
+
def exec_direct(sql)
|
250
|
+
check_rc(SQLExecDirect(@handle, sql))
|
251
|
+
self
|
252
|
+
end
|
253
|
+
|
254
|
+
def set_cursor_name(name)
|
255
|
+
check_rc(SQLSetCursorName(@handle, name))
|
256
|
+
self
|
257
|
+
end
|
258
|
+
|
259
|
+
def get_cursor_name
|
260
|
+
rc, name = SQLGetCursorName(@handle)
|
261
|
+
check_rc(rc)
|
262
|
+
name
|
263
|
+
end
|
264
|
+
|
265
|
+
def row_count
|
266
|
+
rc, rowcount = SQLRowCount(@handle)
|
267
|
+
check_rc(rc)
|
268
|
+
rowcount
|
269
|
+
end
|
270
|
+
|
271
|
+
def num_result_cols
|
272
|
+
rc, cols = SQLNumResultCols(@handle)
|
273
|
+
check_rc(rc)
|
274
|
+
cols
|
275
|
+
end
|
276
|
+
|
277
|
+
def fetch_all
|
278
|
+
if block_given?
|
279
|
+
while row = fetch do
|
280
|
+
yield row
|
281
|
+
end
|
282
|
+
else
|
283
|
+
res = []
|
284
|
+
while row = fetch do
|
285
|
+
res << row
|
286
|
+
end
|
287
|
+
res
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def fetch
|
292
|
+
cols = get_col_desc
|
293
|
+
rc = SQLFetch(@handle)
|
294
|
+
if rc == SQL_NO_DATA_FOUND
|
295
|
+
SQLFreeStmt(@handle, SQL_CLOSE) # Close cursor
|
296
|
+
SQLFreeStmt(@handle, SQL_RESET_PARAMS) # Reset parameters
|
297
|
+
return nil
|
298
|
+
end
|
299
|
+
raise "ERROR" unless rc == SQL_SUCCESS
|
300
|
+
|
301
|
+
retval = []
|
302
|
+
cols.each_with_index do |c, i|
|
303
|
+
rc, content = SQLGetData(@handle, i + 1, c[1], c[2] + 1) #yun added 1 to c[2]
|
304
|
+
retval << adjust_content(content)
|
305
|
+
end
|
306
|
+
retval
|
307
|
+
end
|
308
|
+
|
309
|
+
def fetch_as_hash
|
310
|
+
cols = get_col_desc
|
311
|
+
rc = SQLFetch(@handle)
|
312
|
+
if rc == SQL_NO_DATA_FOUND
|
313
|
+
SQLFreeStmt(@handle, SQL_CLOSE) # Close cursor
|
314
|
+
SQLFreeStmt(@handle, SQL_RESET_PARAMS) # Reset parameters
|
315
|
+
return nil
|
316
|
+
end
|
317
|
+
raise "ERROR" unless rc == SQL_SUCCESS
|
318
|
+
|
319
|
+
retval = {}
|
320
|
+
cols.each_with_index do |c, i|
|
321
|
+
rc, content = SQLGetData(@handle, i + 1, c[1], c[2] + 1) #yun added 1 to c[2]
|
322
|
+
retval[c[0]] = adjust_content(content)
|
323
|
+
end
|
324
|
+
retval
|
325
|
+
end
|
326
|
+
|
327
|
+
def get_col_desc
|
328
|
+
rc, nr_cols = SQLNumResultCols(@handle)
|
329
|
+
cols = (1..nr_cols).collect do |c|
|
330
|
+
rc, name, bl, type, col_sz = SQLDescribeCol(@handle, c, 1024)
|
331
|
+
[name.downcase, type, col_sz]
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def adjust_content(c)
|
336
|
+
case c.class.to_s
|
337
|
+
when 'DB2CLI::NullClass'
|
338
|
+
return nil
|
339
|
+
when 'DB2CLI::Time'
|
340
|
+
"%02d:%02d:%02d" % [c.hour, c.minute, c.second]
|
341
|
+
when 'DB2CLI::Date'
|
342
|
+
"%04d-%02d-%02d" % [c.year, c.month, c.day]
|
343
|
+
when 'DB2CLI::Timestamp'
|
344
|
+
"%04d-%02d-%02d %02d:%02d:%02d" % [c.year, c.month, c.day, c.hour, c.minute, c.second]
|
345
|
+
else
|
346
|
+
return c
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
class Parameter
|
352
|
+
attr_reader :type, :size, :decimalDigits
|
353
|
+
def initialize(type, size, decimalDigits)
|
354
|
+
@type, @size, @decimalDigits = type, size, decimalDigits
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|