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.

Files changed (75) hide show
  1. data/CHANGELOG +77 -2
  2. data/install.rb +5 -0
  3. data/lib/active_record.rb +6 -2
  4. data/lib/active_record/acts/list.rb +56 -45
  5. data/lib/active_record/acts/tree.rb +3 -2
  6. data/lib/active_record/associations.rb +10 -62
  7. data/lib/active_record/associations/association_collection.rb +20 -23
  8. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +36 -10
  9. data/lib/active_record/associations/has_many_association.rb +50 -25
  10. data/lib/active_record/base.rb +118 -80
  11. data/lib/active_record/callbacks.rb +51 -50
  12. data/lib/active_record/connection_adapters/abstract_adapter.rb +33 -12
  13. data/lib/active_record/connection_adapters/db2_adapter.rb +129 -0
  14. data/lib/active_record/connection_adapters/sqlite_adapter.rb +23 -1
  15. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +80 -90
  16. data/lib/active_record/fixtures.rb +1 -1
  17. data/lib/active_record/locking.rb +57 -0
  18. data/lib/active_record/support/class_attribute_accessors.rb +19 -5
  19. data/lib/active_record/support/class_inheritable_attributes.rb +4 -3
  20. data/lib/active_record/support/dependencies.rb +71 -0
  21. data/lib/active_record/support/inflector.rb +1 -0
  22. data/lib/active_record/support/misc.rb +29 -3
  23. data/lib/active_record/support/module_attribute_accessors.rb +57 -0
  24. data/lib/active_record/transactions.rb +18 -11
  25. data/lib/active_record/validations.rb +3 -3
  26. data/lib/active_record/vendor/db2.rb +357 -0
  27. data/lib/active_record/vendor/mysql.rb +1 -1
  28. data/rakefile +17 -5
  29. data/test/associations_test.rb +39 -4
  30. data/test/base_test.rb +13 -4
  31. data/test/binary_test.rb +43 -0
  32. data/test/callbacks_test.rb +230 -0
  33. data/test/connections/native_db2/connection.rb +24 -0
  34. data/test/connections/native_sqlserver/connection.rb +9 -3
  35. data/test/deprecated_associations_test.rb +16 -10
  36. data/test/finder_test.rb +65 -13
  37. data/test/fixtures/associations.png +0 -0
  38. data/test/fixtures/binary.rb +2 -0
  39. data/test/fixtures/companies.yml +21 -0
  40. data/test/fixtures/courses.yml +7 -0
  41. data/test/fixtures/customers.yml +7 -0
  42. data/test/fixtures/db_definitions/db2.sql +124 -0
  43. data/test/fixtures/db_definitions/db22.sql +4 -0
  44. data/test/fixtures/db_definitions/mysql.sql +12 -0
  45. data/test/fixtures/db_definitions/postgresql.sql +13 -0
  46. data/test/fixtures/db_definitions/sqlite.sql +9 -0
  47. data/test/fixtures/db_definitions/sqlserver.sql +13 -0
  48. data/test/fixtures/developers_projects.yml +13 -0
  49. data/test/fixtures/entrants.yml +14 -0
  50. data/test/fixtures/fixture_database.sqlite +0 -0
  51. data/test/fixtures/movies.yml +7 -0
  52. data/test/fixtures/people.yml +3 -0
  53. data/test/fixtures/person.rb +1 -0
  54. data/test/fixtures/projects.yml +7 -0
  55. data/test/fixtures/topics.yml +21 -0
  56. data/test/locking_test.rb +34 -0
  57. data/test/mixin_test.rb +6 -0
  58. data/test/validations_test.rb +1 -1
  59. metadata +33 -29
  60. data/test/fixtures/companies/first_client +0 -6
  61. data/test/fixtures/companies/first_firm +0 -4
  62. data/test/fixtures/companies/second_client +0 -6
  63. data/test/fixtures/courses/java +0 -2
  64. data/test/fixtures/courses/ruby +0 -2
  65. data/test/fixtures/customers/david +0 -6
  66. data/test/fixtures/entrants/first +0 -3
  67. data/test/fixtures/entrants/second +0 -3
  68. data/test/fixtures/entrants/third +0 -3
  69. data/test/fixtures/movies/first +0 -2
  70. data/test/fixtures/movies/second +0 -2
  71. data/test/fixtures/projects/action_controller +0 -2
  72. data/test/fixtures/projects/active_record +0 -2
  73. data/test/fixtures/topics/first +0 -10
  74. data/test/fixtures/topics/second +0 -8
  75. data/test/inflector_test.rb +0 -122
@@ -364,4 +364,4 @@ module Test#:nodoc:
364
364
  end
365
365
  end
366
366
  end
367
- end
367
+ end
@@ -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
- # attr_* style accessors for class-variables that can accessed both on an instance and class level.
2
- class Class #:nodoc:
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
- self.class.#{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
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
- self.class.#{sym.id2name}=(obj)
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 descentent gets a copy of
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
- result = yield
4
- $VERBOSE = old_verbose
5
- return result
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
- TRANSACTION_MUTEX.synchronize do
81
- Thread.current['open_transactions'] ||= 0
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
- TRANSACTION_MUTEX.synchronize do
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 id <> ? AND #{scope} = ?", #{attr_name}, id, #{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 \\\#{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 id <> ?", #{attr_name}, id])}))
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