activerecord 1.14.4 → 1.15.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 (159) hide show
  1. data/CHANGELOG +400 -1
  2. data/README +2 -2
  3. data/RUNNING_UNIT_TESTS +21 -3
  4. data/Rakefile +55 -10
  5. data/lib/active_record.rb +10 -4
  6. data/lib/active_record/acts/list.rb +15 -4
  7. data/lib/active_record/acts/nested_set.rb +11 -12
  8. data/lib/active_record/acts/tree.rb +13 -14
  9. data/lib/active_record/aggregations.rb +46 -22
  10. data/lib/active_record/associations.rb +213 -162
  11. data/lib/active_record/associations/association_collection.rb +45 -15
  12. data/lib/active_record/associations/association_proxy.rb +32 -13
  13. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +18 -18
  14. data/lib/active_record/associations/has_many_association.rb +37 -17
  15. data/lib/active_record/associations/has_many_through_association.rb +120 -30
  16. data/lib/active_record/associations/has_one_association.rb +1 -1
  17. data/lib/active_record/attribute_methods.rb +75 -0
  18. data/lib/active_record/base.rb +282 -203
  19. data/lib/active_record/calculations.rb +95 -54
  20. data/lib/active_record/callbacks.rb +13 -24
  21. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +12 -1
  22. data/lib/active_record/connection_adapters/abstract/connection_specification.rb.rej +21 -0
  23. data/lib/active_record/connection_adapters/abstract/database_statements.rb +30 -4
  24. data/lib/active_record/connection_adapters/abstract/quoting.rb +16 -9
  25. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +121 -37
  26. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +55 -23
  27. data/lib/active_record/connection_adapters/abstract_adapter.rb +8 -0
  28. data/lib/active_record/connection_adapters/db2_adapter.rb +1 -11
  29. data/lib/active_record/connection_adapters/firebird_adapter.rb +364 -50
  30. data/lib/active_record/connection_adapters/frontbase_adapter.rb +861 -0
  31. data/lib/active_record/connection_adapters/mysql_adapter.rb +86 -33
  32. data/lib/active_record/connection_adapters/openbase_adapter.rb +4 -3
  33. data/lib/active_record/connection_adapters/oracle_adapter.rb +151 -127
  34. data/lib/active_record/connection_adapters/postgresql_adapter.rb +125 -48
  35. data/lib/active_record/connection_adapters/sqlite_adapter.rb +38 -10
  36. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +183 -155
  37. data/lib/active_record/connection_adapters/sybase_adapter.rb +190 -212
  38. data/lib/active_record/deprecated_associations.rb +24 -10
  39. data/lib/active_record/deprecated_finders.rb +4 -1
  40. data/lib/active_record/fixtures.rb +37 -23
  41. data/lib/active_record/locking/optimistic.rb +106 -0
  42. data/lib/active_record/locking/pessimistic.rb +77 -0
  43. data/lib/active_record/migration.rb +8 -5
  44. data/lib/active_record/observer.rb +73 -34
  45. data/lib/active_record/reflection.rb +21 -7
  46. data/lib/active_record/schema_dumper.rb +33 -5
  47. data/lib/active_record/timestamp.rb +23 -34
  48. data/lib/active_record/transactions.rb +37 -30
  49. data/lib/active_record/validations.rb +46 -30
  50. data/lib/active_record/vendor/mysql.rb +20 -5
  51. data/lib/active_record/version.rb +2 -2
  52. data/lib/active_record/wrappings.rb +1 -2
  53. data/lib/active_record/xml_serialization.rb +308 -0
  54. data/test/aaa_create_tables_test.rb +5 -1
  55. data/test/abstract_unit.rb +18 -8
  56. data/test/{active_schema_mysql.rb → active_schema_test_mysql.rb} +2 -2
  57. data/test/adapter_test.rb +9 -7
  58. data/test/adapter_test_sqlserver.rb +81 -0
  59. data/test/aggregations_test.rb +29 -0
  60. data/test/{association_callbacks_test.rb → associations/callbacks_test.rb} +10 -8
  61. data/test/{associations_cascaded_eager_loading_test.rb → associations/cascaded_eager_loading_test.rb} +35 -3
  62. data/test/{associations_go_eager_test.rb → associations/eager_test.rb} +36 -2
  63. data/test/{associations_extensions_test.rb → associations/extension_test.rb} +5 -0
  64. data/test/{associations_join_model_test.rb → associations/join_model_test.rb} +118 -8
  65. data/test/associations_test.rb +339 -45
  66. data/test/attribute_methods_test.rb +49 -0
  67. data/test/base_test.rb +321 -67
  68. data/test/calculations_test.rb +48 -10
  69. data/test/callbacks_test.rb +13 -0
  70. data/test/connection_test_firebird.rb +8 -0
  71. data/test/connections/native_db2/connection.rb +18 -17
  72. data/test/connections/native_firebird/connection.rb +19 -17
  73. data/test/connections/native_frontbase/connection.rb +27 -0
  74. data/test/connections/native_mysql/connection.rb +18 -15
  75. data/test/connections/native_openbase/connection.rb +14 -15
  76. data/test/connections/native_oracle/connection.rb +16 -12
  77. data/test/connections/native_postgresql/connection.rb +16 -17
  78. data/test/connections/native_sqlite/connection.rb +3 -6
  79. data/test/connections/native_sqlite3/connection.rb +3 -6
  80. data/test/connections/native_sqlserver/connection.rb +16 -17
  81. data/test/connections/native_sqlserver_odbc/connection.rb +18 -19
  82. data/test/connections/native_sybase/connection.rb +16 -17
  83. data/test/datatype_test_postgresql.rb +52 -0
  84. data/test/defaults_test.rb +52 -10
  85. data/test/deprecated_associations_test.rb +151 -107
  86. data/test/deprecated_finder_test.rb +83 -66
  87. data/test/empty_date_time_test.rb +25 -0
  88. data/test/finder_test.rb +118 -11
  89. data/test/fixtures/accounts.yml +6 -1
  90. data/test/fixtures/author.rb +27 -4
  91. data/test/fixtures/categorizations.yml +8 -2
  92. data/test/fixtures/category.rb +1 -2
  93. data/test/fixtures/comments.yml +0 -6
  94. data/test/fixtures/companies.yml +6 -1
  95. data/test/fixtures/company.rb +23 -1
  96. data/test/fixtures/company_in_module.rb +8 -10
  97. data/test/fixtures/customer.rb +2 -2
  98. data/test/fixtures/customers.yml +9 -0
  99. data/test/fixtures/db_definitions/db2.drop.sql +1 -0
  100. data/test/fixtures/db_definitions/db2.sql +9 -0
  101. data/test/fixtures/db_definitions/firebird.drop.sql +3 -0
  102. data/test/fixtures/db_definitions/firebird.sql +13 -1
  103. data/test/fixtures/db_definitions/frontbase.drop.sql +31 -0
  104. data/test/fixtures/db_definitions/frontbase.sql +262 -0
  105. data/test/fixtures/db_definitions/frontbase2.drop.sql +1 -0
  106. data/test/fixtures/db_definitions/frontbase2.sql +4 -0
  107. data/test/fixtures/db_definitions/mysql.drop.sql +1 -0
  108. data/test/fixtures/db_definitions/mysql.sql +23 -14
  109. data/test/fixtures/db_definitions/openbase.sql +13 -1
  110. data/test/fixtures/db_definitions/oracle.drop.sql +2 -0
  111. data/test/fixtures/db_definitions/oracle.sql +29 -2
  112. data/test/fixtures/db_definitions/postgresql.drop.sql +3 -1
  113. data/test/fixtures/db_definitions/postgresql.sql +13 -3
  114. data/test/fixtures/db_definitions/schema.rb +29 -1
  115. data/test/fixtures/db_definitions/sqlite.drop.sql +1 -0
  116. data/test/fixtures/db_definitions/sqlite.sql +12 -3
  117. data/test/fixtures/db_definitions/sqlserver.drop.sql +3 -0
  118. data/test/fixtures/db_definitions/sqlserver.sql +35 -0
  119. data/test/fixtures/db_definitions/sybase.drop.sql +2 -0
  120. data/test/fixtures/db_definitions/sybase.sql +13 -4
  121. data/test/fixtures/developer.rb +12 -0
  122. data/test/fixtures/edge.rb +5 -0
  123. data/test/fixtures/edges.yml +6 -0
  124. data/test/fixtures/funny_jokes.yml +3 -7
  125. data/test/fixtures/migrations_with_decimal/1_give_me_big_numbers.rb +15 -0
  126. data/test/fixtures/migrations_with_missing_versions/1000_people_have_middle_names.rb +9 -0
  127. data/test/fixtures/migrations_with_missing_versions/1_people_have_last_names.rb +9 -0
  128. data/test/fixtures/migrations_with_missing_versions/3_we_need_reminders.rb +12 -0
  129. data/test/fixtures/migrations_with_missing_versions/4_innocent_jointable.rb +12 -0
  130. data/test/fixtures/mixin.rb +15 -0
  131. data/test/fixtures/mixins.yml +38 -0
  132. data/test/fixtures/post.rb +3 -2
  133. data/test/fixtures/project.rb +3 -1
  134. data/test/fixtures/topic.rb +6 -1
  135. data/test/fixtures/topics.yml +4 -4
  136. data/test/fixtures/vertex.rb +9 -0
  137. data/test/fixtures/vertices.yml +4 -0
  138. data/test/fixtures_test.rb +45 -0
  139. data/test/inheritance_test.rb +67 -6
  140. data/test/lifecycle_test.rb +40 -19
  141. data/test/locking_test.rb +170 -26
  142. data/test/method_scoping_test.rb +2 -2
  143. data/test/migration_test.rb +387 -110
  144. data/test/migration_test_firebird.rb +124 -0
  145. data/test/mixin_nested_set_test.rb +14 -2
  146. data/test/mixin_test.rb +56 -18
  147. data/test/modules_test.rb +8 -2
  148. data/test/multiple_db_test.rb +2 -2
  149. data/test/pk_test.rb +1 -0
  150. data/test/reflection_test.rb +8 -2
  151. data/test/schema_authorization_test_postgresql.rb +75 -0
  152. data/test/schema_dumper_test.rb +40 -4
  153. data/test/table_name_test_sqlserver.rb +23 -0
  154. data/test/threaded_connections_test.rb +19 -16
  155. data/test/transactions_test.rb +86 -72
  156. data/test/validations_test.rb +126 -56
  157. data/test/xml_serialization_test.rb +125 -0
  158. metadata +45 -11
  159. data/lib/active_record/locking.rb +0 -79
@@ -1,15 +1,53 @@
1
1
  require 'active_record/connection_adapters/abstract_adapter'
2
+ require 'set'
3
+
4
+ module MysqlCompat #:nodoc:
5
+ # add all_hashes method to standard mysql-c bindings or pure ruby version
6
+ def self.define_all_hashes_method!
7
+ raise 'Mysql not loaded' unless defined?(::Mysql)
8
+
9
+ target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
10
+ return if target.instance_methods.include?('all_hashes')
11
+
12
+ # Ruby driver has a version string and returns null values in each_hash
13
+ # C driver >= 2.7 returns null values in each_hash
14
+ if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
15
+ target.class_eval <<-'end_eval'
16
+ def all_hashes
17
+ rows = []
18
+ each_hash { |row| rows << row }
19
+ rows
20
+ end
21
+ end_eval
22
+
23
+ # adapters before 2.7 don't have a version constant
24
+ # and don't return null values in each_hash
25
+ else
26
+ target.class_eval <<-'end_eval'
27
+ def all_hashes
28
+ rows = []
29
+ all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
30
+ each_hash { |row| rows << all_fields.dup.update(row) }
31
+ rows
32
+ end
33
+ end_eval
34
+ end
35
+
36
+ unless target.instance_methods.include?('all_hashes')
37
+ raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
38
+ end
39
+ end
40
+ end
2
41
 
3
42
  module ActiveRecord
4
43
  class Base
5
- # Establishes a connection to the database that's used by all Active Record objects.
6
- def self.mysql_connection(config) # :nodoc:
7
- # Only include the MySQL driver if one hasn't already been loaded
44
+ def self.require_mysql
45
+ # Include the MySQL driver if one hasn't already been loaded
8
46
  unless defined? Mysql
9
47
  begin
10
48
  require_library_or_gem 'mysql'
11
49
  rescue LoadError => cannot_require_mysql
12
- # Only use the supplied backup Ruby/MySQL driver if no driver is already in place
50
+ # Use the bundled Ruby/MySQL driver if no driver is already in place
13
51
  begin
14
52
  require 'active_record/vendor/mysql'
15
53
  rescue LoadError
@@ -18,6 +56,12 @@ module ActiveRecord
18
56
  end
19
57
  end
20
58
 
59
+ # Define Mysql::Result.all_hashes
60
+ MysqlCompat.define_all_hashes_method!
61
+ end
62
+
63
+ # Establishes a connection to the database that's used by all Active Record objects.
64
+ def self.mysql_connection(config) # :nodoc:
21
65
  config = config.symbolize_keys
22
66
  host = config[:host]
23
67
  port = config[:port]
@@ -31,20 +75,41 @@ module ActiveRecord
31
75
  raise ArgumentError, "No database specified. Missing argument: database."
32
76
  end
33
77
 
78
+ require_mysql
34
79
  mysql = Mysql.init
35
80
  mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
81
+
36
82
  ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
37
83
  end
38
84
  end
39
85
 
40
86
  module ConnectionAdapters
41
87
  class MysqlColumn < Column #:nodoc:
88
+ TYPES_ALLOWING_EMPTY_STRING_DEFAULT = Set.new([:binary, :string, :text])
89
+
90
+ def initialize(name, default, sql_type = nil, null = true)
91
+ @original_default = default
92
+ super
93
+ @default = nil if missing_default_forged_as_empty_string?
94
+ end
95
+
42
96
  private
43
97
  def simplified_type(field_type)
44
98
  return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
45
99
  return :string if field_type =~ /enum/i
46
100
  super
47
101
  end
102
+
103
+ # MySQL misreports NOT NULL column default when none is given.
104
+ # We can't detect this for columns which may have a legitimate ''
105
+ # default (string, text, binary) but we can for others (integer,
106
+ # datetime, boolean, and the rest).
107
+ #
108
+ # Test whether the column has default '', is not null, and is not
109
+ # a type allowing default ''.
110
+ def missing_default_forged_as_empty_string?
111
+ !null && @original_default == '' && !TYPES_ALLOWING_EMPTY_STRING_DEFAULT.include?(type)
112
+ end
48
113
  end
49
114
 
50
115
  # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
@@ -83,7 +148,7 @@ module ActiveRecord
83
148
  def initialize(connection, logger, connection_options, config)
84
149
  super(connection, logger)
85
150
  @connection_options, @config = connection_options, config
86
- @null_values_in_each_hash = Mysql.const_defined?(:VERSION)
151
+
87
152
  connect
88
153
  end
89
154
 
@@ -95,13 +160,14 @@ module ActiveRecord
95
160
  true
96
161
  end
97
162
 
98
- def native_database_types #:nodoc
163
+ def native_database_types #:nodoc:
99
164
  {
100
165
  :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
101
166
  :string => { :name => "varchar", :limit => 255 },
102
167
  :text => { :name => "text" },
103
168
  :integer => { :name => "int", :limit => 11 },
104
169
  :float => { :name => "float" },
170
+ :decimal => { :name => "decimal" },
105
171
  :datetime => { :name => "datetime" },
106
172
  :timestamp => { :name => "datetime" },
107
173
  :time => { :name => "time" },
@@ -118,6 +184,8 @@ module ActiveRecord
118
184
  if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
119
185
  s = column.class.string_to_binary(value).unpack("H*")[0]
120
186
  "x'#{s}'"
187
+ elsif value.kind_of?(BigDecimal)
188
+ "'#{value.to_s("F")}'"
121
189
  else
122
190
  super
123
191
  end
@@ -171,16 +239,7 @@ module ActiveRecord
171
239
 
172
240
  # DATABASE STATEMENTS ======================================
173
241
 
174
- def select_all(sql, name = nil) #:nodoc:
175
- select(sql, name)
176
- end
177
-
178
- def select_one(sql, name = nil) #:nodoc:
179
- result = select(sql, name)
180
- result.nil? ? nil : result.first
181
- end
182
-
183
- def execute(sql, name = nil, retries = 2) #:nodoc:
242
+ def execute(sql, name = nil) #:nodoc:
184
243
  log(sql, name) { @connection.query(sql) }
185
244
  rescue ActiveRecord::StatementInvalid => exception
186
245
  if exception.message.split(":").first =~ /Packets out of order/
@@ -200,9 +259,6 @@ module ActiveRecord
200
259
  @connection.affected_rows
201
260
  end
202
261
 
203
- alias_method :delete, :update #:nodoc:
204
-
205
-
206
262
  def begin_db_transaction #:nodoc:
207
263
  execute "BEGIN"
208
264
  rescue Exception
@@ -222,7 +278,7 @@ module ActiveRecord
222
278
  end
223
279
 
224
280
 
225
- def add_limit_offset!(sql, options) #:nodoc
281
+ def add_limit_offset!(sql, options) #:nodoc:
226
282
  if limit = options[:limit]
227
283
  unless offset = options[:offset]
228
284
  sql << " LIMIT #{limit}"
@@ -304,13 +360,15 @@ module ActiveRecord
304
360
  def change_column_default(table_name, column_name, default) #:nodoc:
305
361
  current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
306
362
 
307
- change_column(table_name, column_name, current_type, { :default => default })
363
+ execute("ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{current_type} DEFAULT #{quote(default)}")
308
364
  end
309
365
 
310
366
  def change_column(table_name, column_name, type, options = {}) #:nodoc:
311
- options[:default] ||= select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
312
-
313
- change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}"
367
+ unless options_include_default?(options)
368
+ options[:default] = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
369
+ end
370
+
371
+ change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
314
372
  add_column_options!(change_column_sql, options)
315
373
  execute(change_column_sql)
316
374
  end
@@ -327,6 +385,7 @@ module ActiveRecord
327
385
  if encoding
328
386
  @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
329
387
  end
388
+ @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
330
389
  @connection.real_connect(*@connection_options)
331
390
  execute("SET NAMES '#{encoding}'") if encoding
332
391
  end
@@ -334,21 +393,15 @@ module ActiveRecord
334
393
  def select(sql, name = nil)
335
394
  @connection.query_with_result = true
336
395
  result = execute(sql, name)
337
- rows = []
338
- if @null_values_in_each_hash
339
- result.each_hash { |row| rows << row }
340
- else
341
- all_fields = result.fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
342
- result.each_hash { |row| rows << all_fields.dup.update(row) }
343
- end
396
+ rows = result.all_hashes
344
397
  result.free
345
398
  rows
346
399
  end
347
-
400
+
348
401
  def supports_views?
349
402
  version[0] >= 5
350
403
  end
351
-
404
+
352
405
  def version
353
406
  @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
354
407
  end
@@ -32,7 +32,7 @@ module ActiveRecord
32
32
  private
33
33
  def simplified_type(field_type)
34
34
  return :integer if field_type.downcase =~ /long/
35
- return :float if field_type.downcase == "money"
35
+ return :decimal if field_type.downcase == "money"
36
36
  return :binary if field_type.downcase == "object"
37
37
  super
38
38
  end
@@ -55,7 +55,7 @@ module ActiveRecord
55
55
  #
56
56
  # Caveat: Operations involving LIMIT and OFFSET do not yet work!
57
57
  #
58
- # Maintainer: derrickspell@cdmplus.com
58
+ # Maintainer: derrick.spell@gmail.com
59
59
  class OpenBaseAdapter < AbstractAdapter
60
60
  def adapter_name
61
61
  'OpenBase'
@@ -68,6 +68,7 @@ module ActiveRecord
68
68
  :text => { :name => "text" },
69
69
  :integer => { :name => "integer" },
70
70
  :float => { :name => "float" },
71
+ :decimal => { :name => "decimal" },
71
72
  :datetime => { :name => "datetime" },
72
73
  :timestamp => { :name => "timestamp" },
73
74
  :time => { :name => "time" },
@@ -121,7 +122,7 @@ module ActiveRecord
121
122
 
122
123
  # DATABASE STATEMENTS ======================================
123
124
 
124
- def add_limit_offset!(sql, options) #:nodoc
125
+ def add_limit_offset!(sql, options) #:nodoc:
125
126
  if limit = options[:limit]
126
127
  unless offset = options[:offset]
127
128
  sql << " RETURN RESULTS #{limit}"
@@ -41,28 +41,17 @@ begin
41
41
  self.oracle_connection(config)
42
42
  end
43
43
 
44
- # Enable the id column to be bound into the sql later, by the adapter's insert method.
45
- # This is preferable to inserting the hard-coded value here, because the insert method
46
- # needs to know the id value explicitly.
47
- alias :attributes_with_quotes_pre_oracle :attributes_with_quotes
48
- def attributes_with_quotes(include_primary_key = true) #:nodoc:
49
- aq = attributes_with_quotes_pre_oracle(include_primary_key)
50
- if connection.class == ConnectionAdapters::OracleAdapter
51
- aq[self.class.primary_key] = ":id" if include_primary_key && aq[self.class.primary_key].nil?
52
- end
53
- aq
54
- end
55
-
56
44
  # After setting large objects to empty, select the OCI8::LOB
57
45
  # and write back the data.
58
- after_save :write_lobs
46
+ after_save :write_lobs
59
47
  def write_lobs() #:nodoc:
60
48
  if connection.is_a?(ConnectionAdapters::OracleAdapter)
61
- self.class.columns.select { |c| c.type == :binary }.each { |c|
49
+ self.class.columns.select { |c| c.sql_type =~ /LOB$/i }.each { |c|
62
50
  value = self[c.name]
51
+ value = value.to_yaml if unserializable_attribute?(c.name, c)
63
52
  next if value.nil? || (value == '')
64
53
  lob = connection.select_one(
65
- "SELECT #{ c.name} FROM #{ self.class.table_name } WHERE #{ self.class.primary_key} = #{quote(id)}",
54
+ "SELECT #{c.name} FROM #{self.class.table_name} WHERE #{self.class.primary_key} = #{quote_value(id)}",
66
55
  'Writable Large Object')[c.name]
67
56
  lob.write value
68
57
  }
@@ -75,57 +64,21 @@ begin
75
64
 
76
65
  module ConnectionAdapters #:nodoc:
77
66
  class OracleColumn < Column #:nodoc:
78
- attr_reader :sql_type
79
-
80
- # overridden to add the concept of scale, required to differentiate
81
- # between integer and float fields
82
- def initialize(name, default, sql_type, limit, scale, null)
83
- @name, @limit, @sql_type, @scale, @null = name, limit, sql_type, scale, null
84
-
85
- @type = simplified_type(sql_type)
86
- @default = type_cast(default)
87
-
88
- @primary = nil
89
- @text = [:string, :text].include? @type
90
- @number = [:float, :integer].include? @type
91
- end
92
67
 
93
68
  def type_cast(value)
94
- return nil if value.nil? || value =~ /^\s*null\s*$/i
95
- case type
96
- when :string then value
97
- when :integer then defined?(value.to_i) ? value.to_i : (value ? 1 : 0)
98
- when :float then value.to_f
99
- when :datetime then cast_to_date_or_time(value)
100
- when :time then cast_to_time(value)
101
- else value
102
- end
69
+ return guess_date_or_time(value) if type == :datetime && OracleAdapter.emulate_dates
70
+ super
103
71
  end
104
72
 
105
73
  private
106
74
  def simplified_type(field_type)
75
+ return :boolean if OracleAdapter.emulate_booleans && field_type == 'NUMBER(1)'
107
76
  case field_type
108
- when /char/i : :string
109
- when /num|float|double|dec|real|int/i : @scale == 0 ? :integer : :float
110
- when /date|time/i : @name =~ /_at$/ ? :time : :datetime
111
- when /clob/i : :text
112
- when /blob/i : :binary
77
+ when /date|time/i then :datetime
78
+ else super
113
79
  end
114
80
  end
115
81
 
116
- def cast_to_date_or_time(value)
117
- return value if value.is_a? Date
118
- return nil if value.blank?
119
- guess_date_or_time (value.is_a? Time) ? value : cast_to_time(value)
120
- end
121
-
122
- def cast_to_time(value)
123
- return value if value.is_a? Time
124
- time_array = ParseDate.parsedate value
125
- time_array[0] ||= 2000; time_array[1] ||= 1; time_array[2] ||= 1;
126
- Time.send(Base.default_timezone, *time_array) rescue nil
127
- end
128
-
129
82
  def guess_date_or_time(value)
130
83
  (value.hour == 0 and value.min == 0 and value.sec == 0) ?
131
84
  Date.new(value.year, value.month, value.day) : value
@@ -167,6 +120,12 @@ begin
167
120
  # * <tt>:database</tt>
168
121
  class OracleAdapter < AbstractAdapter
169
122
 
123
+ @@emulate_booleans = true
124
+ cattr_accessor :emulate_booleans
125
+
126
+ @@emulate_dates = false
127
+ cattr_accessor :emulate_dates
128
+
170
129
  def adapter_name #:nodoc:
171
130
  'Oracle'
172
131
  end
@@ -174,14 +133,15 @@ begin
174
133
  def supports_migrations? #:nodoc:
175
134
  true
176
135
  end
177
-
178
- def native_database_types #:nodoc
136
+
137
+ def native_database_types #:nodoc:
179
138
  {
180
139
  :primary_key => "NUMBER(38) NOT NULL PRIMARY KEY",
181
140
  :string => { :name => "VARCHAR2", :limit => 255 },
182
141
  :text => { :name => "CLOB" },
183
142
  :integer => { :name => "NUMBER", :limit => 38 },
184
143
  :float => { :name => "NUMBER" },
144
+ :decimal => { :name => "DECIMAL" },
185
145
  :datetime => { :name => "DATE" },
186
146
  :timestamp => { :name => "DATE" },
187
147
  :time => { :name => "DATE" },
@@ -205,26 +165,26 @@ begin
205
165
  name =~ /[A-Z]/ ? "\"#{name}\"" : name
206
166
  end
207
167
 
208
- def quote_string(string) #:nodoc:
209
- string.gsub(/'/, "''")
168
+ def quote_string(s) #:nodoc:
169
+ s.gsub(/'/, "''")
210
170
  end
211
171
 
212
172
  def quote(value, column = nil) #:nodoc:
213
- if column && column.type == :binary
214
- %Q{empty_#{ column.sql_type rescue 'blob' }()}
173
+ if column && [:text, :binary].include?(column.type)
174
+ %Q{empty_#{ column.sql_type.downcase rescue 'blob' }()}
215
175
  else
216
- case value
217
- when String : %Q{'#{quote_string(value)}'}
218
- when NilClass : 'null'
219
- when TrueClass : '1'
220
- when FalseClass : '0'
221
- when Numeric : value.to_s
222
- when Date, Time : %Q{'#{value.strftime("%Y-%m-%d %H:%M:%S")}'}
223
- else %Q{'#{quote_string(value.to_yaml)}'}
224
- end
176
+ super
225
177
  end
226
178
  end
227
179
 
180
+ def quoted_true
181
+ "1"
182
+ end
183
+
184
+ def quoted_false
185
+ "0"
186
+ end
187
+
228
188
 
229
189
  # CONNECTION MANAGEMENT ====================================
230
190
  #
@@ -232,7 +192,7 @@ begin
232
192
  # Returns true if the connection is active.
233
193
  def active?
234
194
  # Pings the connection to check if it's still good. Note that an
235
- # #active? method is also available, but that simply returns the
195
+ # #active? method is also available, but that simply returns the
236
196
  # last known state, which isn't good enough if the connection has
237
197
  # gone stale since the last use.
238
198
  @connection.ping
@@ -258,35 +218,24 @@ begin
258
218
  #
259
219
  # see: abstract/database_statements.rb
260
220
 
261
- def select_all(sql, name = nil) #:nodoc:
262
- select(sql, name)
263
- end
264
-
265
- def select_one(sql, name = nil) #:nodoc:
266
- result = select_all(sql, name)
267
- result.size > 0 ? result.first : nil
268
- end
269
-
270
221
  def execute(sql, name = nil) #:nodoc:
271
222
  log(sql, name) { @connection.exec sql }
272
223
  end
273
224
 
274
- def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
275
- if pk.nil? # Who called us? What does the sql look like? No idea!
276
- execute sql, name
277
- elsif id_value # Pre-assigned id
278
- log(sql, name) { @connection.exec sql }
279
- else # Assume the sql contains a bind-variable for the id
280
- id_value = select_one("select #{sequence_name}.nextval id from dual")['id']
281
- log(sql, name) { @connection.exec sql, id_value }
282
- end
225
+ # Returns the next sequence value from a sequence generator. Not generally
226
+ # called directly; used by ActiveRecord to get the next primary key value
227
+ # when inserting a new database record (see #prefetch_primary_key?).
228
+ def next_sequence_value(sequence_name)
229
+ id = 0
230
+ @connection.exec("select #{sequence_name}.nextval id from dual") { |r| id = r[0].to_i }
231
+ id
232
+ end
283
233
 
234
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
235
+ execute(sql, name)
284
236
  id_value
285
237
  end
286
238
 
287
- alias :update :execute #:nodoc:
288
- alias :delete :execute #:nodoc:
289
-
290
239
  def begin_db_transaction #:nodoc:
291
240
  @connection.autocommit = false
292
241
  end
@@ -313,6 +262,12 @@ begin
313
262
  end
314
263
  end
315
264
 
265
+ # Returns true for Oracle adapter (since Oracle requires primary key
266
+ # values to be pre-fetched before insert). See also #next_sequence_value.
267
+ def prefetch_primary_key?(table_name = nil)
268
+ true
269
+ end
270
+
316
271
  def default_sequence_name(table, column) #:nodoc:
317
272
  "#{table}_seq"
318
273
  end
@@ -338,7 +293,7 @@ begin
338
293
  FROM user_indexes i, user_ind_columns c
339
294
  WHERE i.table_name = '#{table_name.to_s.upcase}'
340
295
  AND c.index_name = i.index_name
341
- AND i.index_name NOT IN (SELECT index_name FROM user_constraints WHERE constraint_type = 'P')
296
+ AND i.index_name NOT IN (SELECT uc.index_name FROM user_constraints uc WHERE uc.constraint_type = 'P')
342
297
  ORDER BY i.index_name, c.column_position
343
298
  SQL
344
299
 
@@ -360,47 +315,54 @@ begin
360
315
  def columns(table_name, name = nil) #:nodoc:
361
316
  (owner, table_name) = @connection.describe(table_name)
362
317
 
363
- table_cols = %Q{
364
- select column_name, data_type, data_default, nullable,
318
+ table_cols = <<-SQL
319
+ select column_name as name, data_type as sql_type, data_default, nullable,
365
320
  decode(data_type, 'NUMBER', data_precision,
321
+ 'FLOAT', data_precision,
366
322
  'VARCHAR2', data_length,
367
- null) as length,
323
+ null) as limit,
368
324
  decode(data_type, 'NUMBER', data_scale, null) as scale
369
325
  from all_tab_columns
370
326
  where owner = '#{owner}'
371
327
  and table_name = '#{table_name}'
372
328
  order by column_id
373
- }
329
+ SQL
374
330
 
375
331
  select_all(table_cols, name).map do |row|
332
+ limit, scale = row['limit'], row['scale']
333
+ if limit || scale
334
+ row['sql_type'] << "(#{(limit || 38).to_i}" + ((scale = scale.to_i) > 0 ? ",#{scale})" : ")")
335
+ end
336
+
337
+ # clean up odd default spacing from Oracle
376
338
  if row['data_default']
377
339
  row['data_default'].sub!(/^(.*?)\s*$/, '\1')
378
340
  row['data_default'].sub!(/^'(.*)'$/, '\1')
341
+ row['data_default'] = nil if row['data_default'] =~ /^null$/i
379
342
  end
380
- OracleColumn.new(
381
- oracle_downcase(row['column_name']),
382
- row['data_default'],
383
- row['data_type'],
384
- (l = row['length']).nil? ? nil : l.to_i,
385
- (s = row['scale']).nil? ? nil : s.to_i,
386
- row['nullable'] == 'Y'
387
- )
343
+
344
+ OracleColumn.new(oracle_downcase(row['name']),
345
+ row['data_default'],
346
+ row['sql_type'],
347
+ row['nullable'] == 'Y')
388
348
  end
389
349
  end
390
350
 
391
351
  def create_table(name, options = {}) #:nodoc:
392
352
  super(name, options)
393
- execute "CREATE SEQUENCE #{name}_seq START WITH 10000" unless options[:id] == false
353
+ seq_name = options[:sequence_name] || "#{name}_seq"
354
+ execute "CREATE SEQUENCE #{seq_name} START WITH 10000" unless options[:id] == false
394
355
  end
395
356
 
396
357
  def rename_table(name, new_name) #:nodoc:
397
358
  execute "RENAME #{name} TO #{new_name}"
398
359
  execute "RENAME #{name}_seq TO #{new_name}_seq" rescue nil
399
- end
360
+ end
400
361
 
401
- def drop_table(name) #:nodoc:
362
+ def drop_table(name, options = {}) #:nodoc:
402
363
  super(name)
403
- execute "DROP SEQUENCE #{name}_seq" rescue nil
364
+ seq_name = options[:sequence_name] || "#{name}_seq"
365
+ execute "DROP SEQUENCE #{seq_name}" rescue nil
404
366
  end
405
367
 
406
368
  def remove_index(table_name, options = {}) #:nodoc:
@@ -412,7 +374,7 @@ begin
412
374
  end
413
375
 
414
376
  def change_column(table_name, column_name, type, options = {}) #:nodoc:
415
- change_column_sql = "ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit])}"
377
+ change_column_sql = "ALTER TABLE #{table_name} MODIFY #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
416
378
  add_column_options!(change_column_sql, options)
417
379
  execute(change_column_sql)
418
380
  end
@@ -425,26 +387,45 @@ begin
425
387
  execute "ALTER TABLE #{table_name} DROP COLUMN #{column_name}"
426
388
  end
427
389
 
390
+ # Find a table's primary key and sequence.
391
+ # *Note*: Only primary key is implemented - sequence will be nil.
392
+ def pk_and_sequence_for(table_name)
393
+ (owner, table_name) = @connection.describe(table_name)
394
+
395
+ pks = select_values(<<-SQL, 'Primary Key')
396
+ select cc.column_name
397
+ from all_constraints c, all_cons_columns cc
398
+ where c.owner = '#{owner}'
399
+ and c.table_name = '#{table_name}'
400
+ and c.constraint_type = 'P'
401
+ and cc.owner = c.owner
402
+ and cc.constraint_name = c.constraint_name
403
+ SQL
404
+
405
+ # only support single column keys
406
+ pks.size == 1 ? [oracle_downcase(pks.first), nil] : nil
407
+ end
408
+
428
409
  def structure_dump #:nodoc:
429
410
  s = select_all("select sequence_name from user_sequences").inject("") do |structure, seq|
430
411
  structure << "create sequence #{seq.to_a.first.last};\n\n"
431
412
  end
432
413
 
433
414
  select_all("select table_name from user_tables").inject(s) do |structure, table|
434
- ddl = "create table #{table.to_a.first.last} (\n "
415
+ ddl = "create table #{table.to_a.first.last} (\n "
435
416
  cols = select_all(%Q{
436
417
  select column_name, data_type, data_length, data_precision, data_scale, data_default, nullable
437
418
  from user_tab_columns
438
419
  where table_name = '#{table.to_a.first.last}'
439
420
  order by column_id
440
- }).map do |row|
441
- col = "#{row['column_name'].downcase} #{row['data_type'].downcase}"
421
+ }).map do |row|
422
+ col = "#{row['column_name'].downcase} #{row['data_type'].downcase}"
442
423
  if row['data_type'] =='NUMBER' and !row['data_precision'].nil?
443
424
  col << "(#{row['data_precision'].to_i}"
444
425
  col << ",#{row['data_scale'].to_i}" if !row['data_scale'].nil?
445
426
  col << ')'
446
427
  elsif row['data_type'].include?('CHAR')
447
- col << "(#{row['data_length'].to_i})"
428
+ col << "(#{row['data_length'].to_i})"
448
429
  end
449
430
  col << " default #{row['data_default']}" if !row['data_default'].nil?
450
431
  col << ' not null' if row['nullable'] == 'N'
@@ -466,6 +447,41 @@ begin
466
447
  end
467
448
  end
468
449
 
450
+ # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
451
+ #
452
+ # Oracle requires the ORDER BY columns to be in the SELECT list for DISTINCT
453
+ # queries. However, with those columns included in the SELECT DISTINCT list, you
454
+ # won't actually get a distinct list of the column you want (presuming the column
455
+ # has duplicates with multiple values for the ordered-by columns. So we use the
456
+ # FIRST_VALUE function to get a single (first) value for each column, effectively
457
+ # making every row the same.
458
+ #
459
+ # distinct("posts.id", "posts.created_at desc")
460
+ def distinct(columns, order_by)
461
+ return "DISTINCT #{columns}" if order_by.blank?
462
+
463
+ # construct a valid DISTINCT clause, ie. one that includes the ORDER BY columns, using
464
+ # FIRST_VALUE such that the inclusion of these columns doesn't invalidate the DISTINCT
465
+ order_columns = order_by.split(',').map { |s| s.strip }.reject(&:blank?)
466
+ order_columns = order_columns.zip((0...order_columns.size).to_a).map do |c, i|
467
+ "FIRST_VALUE(#{c.split.first}) OVER (PARTITION BY #{columns} ORDER BY #{c}) AS alias_#{i}__"
468
+ end
469
+ sql = "DISTINCT #{columns}, "
470
+ sql << order_columns * ", "
471
+ end
472
+
473
+ # ORDER BY clause for the passed order option.
474
+ #
475
+ # Uses column aliases as defined by #distinct.
476
+ def add_order_by_for_association_limiting!(sql, options)
477
+ return sql if options[:order].blank?
478
+
479
+ order = options[:order].split(',').collect { |s| s.strip }.reject(&:blank?)
480
+ order.map! {|s| $1 if s =~ / (.*)/}
481
+ order = order.zip((0...order.size).to_a).map { |s,i| "alias_#{i}__ #{s}" }.join(', ')
482
+
483
+ sql << "ORDER BY #{order}"
484
+ end
469
485
 
470
486
  private
471
487
 
@@ -542,7 +558,7 @@ begin
542
558
  def describe(name)
543
559
  @desc ||= @@env.alloc(OCIDescribe)
544
560
  @desc.attrSet(OCI_ATTR_DESC_PUBLIC, -1) if VERSION >= '0.1.14'
545
- @desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK)
561
+ @desc.describeAny(@svc, name.to_s, OCI_PTYPE_UNK) rescue raise %Q{"DESC #{name}" failed; does it exist?}
546
562
  info = @desc.attrGet(OCI_ATTR_PARAM)
547
563
 
548
564
  case info.attrGet(OCI_ATTR_PTYPE)
@@ -554,6 +570,7 @@ begin
554
570
  schema = info.attrGet(OCI_ATTR_SCHEMA_NAME)
555
571
  name = info.attrGet(OCI_ATTR_NAME)
556
572
  describe(schema + '.' + name)
573
+ else raise %Q{"DESC #{name}" failed; not a table or view.}
557
574
  end
558
575
  end
559
576
 
@@ -563,11 +580,14 @@ begin
563
580
  # The OracleConnectionFactory factors out the code necessary to connect and
564
581
  # configure an Oracle/OCI connection.
565
582
  class OracleConnectionFactory #:nodoc:
566
- def new_connection(username, password, database)
583
+ def new_connection(username, password, database, async, prefetch_rows, cursor_sharing)
567
584
  conn = OCI8.new username, password, database
568
585
  conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
569
586
  conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'} rescue nil
570
587
  conn.autocommit = true
588
+ conn.non_blocking = true if async
589
+ conn.prefetch_rows = prefetch_rows
590
+ conn.exec "alter session set cursor_sharing = #{cursor_sharing}" rescue nil
571
591
  conn
572
592
  end
573
593
  end
@@ -575,10 +595,10 @@ begin
575
595
 
576
596
  # The OCI8AutoRecover class enhances the OCI8 driver with auto-recover and
577
597
  # reset functionality. If a call to #exec fails, and autocommit is turned on
578
- # (ie., we're not in the middle of a longer transaction), it will
598
+ # (ie., we're not in the middle of a longer transaction), it will
579
599
  # automatically reconnect and try again. If autocommit is turned off,
580
600
  # this would be dangerous (as the earlier part of the implied transaction
581
- # may have failed silently if the connection died) -- so instead the
601
+ # may have failed silently if the connection died) -- so instead the
582
602
  # connection is marked as dead, to be reconnected on it's next use.
583
603
  class OCI8AutoRecover < DelegateClass(OCI8) #:nodoc:
584
604
  attr_accessor :active
@@ -592,9 +612,12 @@ begin
592
612
 
593
613
  def initialize(config, factory = OracleConnectionFactory.new)
594
614
  @active = true
595
- @username, @password, @database = config[:username], config[:password], config[:database]
615
+ @username, @password, @database, = config[:username], config[:password], config[:database]
616
+ @async = config[:allow_concurrency]
617
+ @prefetch_rows = config[:prefetch_rows] || 100
618
+ @cursor_sharing = config[:cursor_sharing] || 'similar'
596
619
  @factory = factory
597
- @connection = @factory.new_connection @username, @password, @database
620
+ @connection = @factory.new_connection @username, @password, @database, @async, @prefetch_rows, @cursor_sharing
598
621
  super @connection
599
622
  end
600
623
 
@@ -613,7 +636,7 @@ begin
613
636
  def reset!
614
637
  logoff rescue nil
615
638
  begin
616
- @connection = @factory.new_connection @username, @password, @database
639
+ @connection = @factory.new_connection @username, @password, @database, @async, @prefetch_rows, @cursor_sharing
617
640
  __setobj__ @connection
618
641
  @active = true
619
642
  rescue
@@ -623,7 +646,7 @@ begin
623
646
  end
624
647
 
625
648
  # ORA-00028: your session has been killed
626
- # ORA-01012: not logged on
649
+ # ORA-01012: not logged on
627
650
  # ORA-03113: end-of-file on communication channel
628
651
  # ORA-03114: not connected to ORACLE
629
652
  LOST_CONNECTION_ERROR_CODES = [ 28, 1012, 3113, 3114 ]
@@ -631,11 +654,11 @@ begin
631
654
  # Adds auto-recovery functionality.
632
655
  #
633
656
  # See: http://www.jiubao.org/ruby-oci8/api.en.html#label-11
634
- def exec(sql, *bindvars)
657
+ def exec(sql, *bindvars, &block)
635
658
  should_retry = self.class.auto_retry? && autocommit?
636
659
 
637
660
  begin
638
- @connection.exec(sql, *bindvars)
661
+ @connection.exec(sql, *bindvars, &block)
639
662
  rescue OCIException => e
640
663
  raise unless LOST_CONNECTION_ERROR_CODES.include?(e.code)
641
664
  @active = false
@@ -652,13 +675,14 @@ rescue LoadError
652
675
  # OCI8 driver is unavailable.
653
676
  module ActiveRecord # :nodoc:
654
677
  class Base
678
+ @@oracle_error_message = "Oracle/OCI libraries could not be loaded: #{$!.to_s}"
655
679
  def self.oracle_connection(config) # :nodoc:
656
680
  # Set up a reasonable error message
657
- raise LoadError, "Oracle/OCI libraries could not be loaded."
681
+ raise LoadError, @@oracle_error_message
658
682
  end
659
683
  def self.oci_connection(config) # :nodoc:
660
684
  # Set up a reasonable error message
661
- raise LoadError, "Oracle/OCI libraries could not be loaded."
685
+ raise LoadError, @@oracle_error_message
662
686
  end
663
687
  end
664
688
  end