activerecord 1.11.1 → 1.12.1

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 (102) hide show
  1. data/CHANGELOG +198 -0
  2. data/lib/active_record.rb +19 -14
  3. data/lib/active_record/acts/list.rb +8 -6
  4. data/lib/active_record/acts/tree.rb +33 -10
  5. data/lib/active_record/aggregations.rb +1 -7
  6. data/lib/active_record/associations.rb +151 -82
  7. data/lib/active_record/associations/association_collection.rb +25 -0
  8. data/lib/active_record/associations/association_proxy.rb +9 -8
  9. data/lib/active_record/associations/belongs_to_association.rb +19 -5
  10. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +44 -69
  11. data/lib/active_record/associations/has_many_association.rb +6 -14
  12. data/lib/active_record/associations/has_one_association.rb +5 -3
  13. data/lib/active_record/base.rb +344 -130
  14. data/lib/active_record/callbacks.rb +2 -2
  15. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +128 -0
  16. data/lib/active_record/connection_adapters/abstract/database_statements.rb +104 -0
  17. data/lib/active_record/connection_adapters/abstract/quoting.rb +51 -0
  18. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +249 -0
  19. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +245 -0
  20. data/lib/active_record/connection_adapters/abstract_adapter.rb +29 -464
  21. data/lib/active_record/connection_adapters/db2_adapter.rb +40 -10
  22. data/lib/active_record/connection_adapters/mysql_adapter.rb +131 -60
  23. data/lib/active_record/connection_adapters/oci_adapter.rb +106 -26
  24. data/lib/active_record/connection_adapters/postgresql_adapter.rb +211 -62
  25. data/lib/active_record/connection_adapters/sqlite_adapter.rb +193 -44
  26. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +24 -15
  27. data/lib/active_record/fixtures.rb +47 -24
  28. data/lib/active_record/migration.rb +34 -5
  29. data/lib/active_record/observer.rb +32 -2
  30. data/lib/active_record/query_cache.rb +12 -11
  31. data/lib/active_record/schema.rb +58 -0
  32. data/lib/active_record/schema_dumper.rb +84 -0
  33. data/lib/active_record/transactions.rb +1 -3
  34. data/lib/active_record/validations.rb +40 -26
  35. data/lib/active_record/vendor/mysql.rb +6 -0
  36. data/lib/active_record/version.rb +9 -0
  37. data/rakefile +5 -16
  38. data/test/abstract_unit.rb +6 -11
  39. data/test/adapter_test.rb +58 -0
  40. data/test/ar_schema_test.rb +33 -0
  41. data/test/association_callbacks_test.rb +14 -0
  42. data/test/associations_go_eager_test.rb +56 -14
  43. data/test/associations_test.rb +245 -25
  44. data/test/base_test.rb +205 -34
  45. data/test/binary_test.rb +25 -42
  46. data/test/callbacks_test.rb +75 -0
  47. data/test/conditions_scoping_test.rb +136 -0
  48. data/test/connections/native_mysql/connection.rb +0 -4
  49. data/test/connections/native_sqlite3/in_memory_connection.rb +17 -0
  50. data/test/copy_table_sqlite.rb +64 -0
  51. data/test/deprecated_associations_test.rb +7 -6
  52. data/test/deprecated_finder_test.rb +3 -3
  53. data/test/finder_test.rb +33 -3
  54. data/test/fixtures/accounts.yml +5 -0
  55. data/test/fixtures/categories_ordered.yml +7 -0
  56. data/test/fixtures/category.rb +11 -1
  57. data/test/fixtures/comment.rb +22 -2
  58. data/test/fixtures/comments.yml +6 -0
  59. data/test/fixtures/companies.yml +15 -0
  60. data/test/fixtures/company.rb +24 -1
  61. data/test/fixtures/db_definitions/db2.drop.sql +5 -1
  62. data/test/fixtures/db_definitions/db2.sql +15 -1
  63. data/test/fixtures/db_definitions/mysql.drop.sql +2 -0
  64. data/test/fixtures/db_definitions/mysql.sql +17 -2
  65. data/test/fixtures/db_definitions/oci.drop.sql +37 -5
  66. data/test/fixtures/db_definitions/oci.sql +47 -4
  67. data/test/fixtures/db_definitions/oci2.drop.sql +1 -1
  68. data/test/fixtures/db_definitions/oci2.sql +2 -2
  69. data/test/fixtures/db_definitions/postgresql.drop.sql +4 -0
  70. data/test/fixtures/db_definitions/postgresql.sql +33 -4
  71. data/test/fixtures/db_definitions/sqlite.drop.sql +2 -0
  72. data/test/fixtures/db_definitions/sqlite.sql +16 -2
  73. data/test/fixtures/db_definitions/sqlserver.drop.sql +2 -0
  74. data/test/fixtures/db_definitions/sqlserver.sql +16 -2
  75. data/test/fixtures/developer.rb +1 -1
  76. data/test/fixtures/flowers.jpg +0 -0
  77. data/test/fixtures/keyboard.rb +3 -0
  78. data/test/fixtures/mixins.yml +11 -1
  79. data/test/fixtures/order.rb +4 -0
  80. data/test/fixtures/post.rb +4 -0
  81. data/test/fixtures/posts.yml +7 -0
  82. data/test/fixtures/project.rb +1 -0
  83. data/test/fixtures/subject.rb +4 -0
  84. data/test/fixtures/subscriber.rb +2 -4
  85. data/test/fixtures/topics.yml +2 -2
  86. data/test/fixtures_test.rb +79 -7
  87. data/test/inheritance_test.rb +2 -2
  88. data/test/lifecycle_test.rb +14 -6
  89. data/test/migration_test.rb +164 -6
  90. data/test/mixin_test.rb +78 -2
  91. data/test/pk_test.rb +25 -1
  92. data/test/readonly_test.rb +31 -0
  93. data/test/reflection_test.rb +4 -1
  94. data/test/schema_dumper_test.rb +19 -0
  95. data/test/schema_test_postgresql.rb +3 -2
  96. data/test/synonym_test_oci.rb +17 -0
  97. data/test/threaded_connections_test.rb +2 -1
  98. data/test/transactions_test.rb +109 -10
  99. data/test/validations_test.rb +70 -42
  100. metadata +25 -5
  101. data/test/fixtures/associations.png +0 -0
  102. data/test/thread_safety_test.rb +0 -36
@@ -11,7 +11,7 @@ begin
11
11
  # Establishes a connection to the database that's used by
12
12
  # all Active Record objects
13
13
  def self.db2_connection(config) # :nodoc:
14
- symbolize_strings_in_hash(config)
14
+ config = config.symbolize_keys
15
15
  usr = config[:username]
16
16
  pwd = config[:password]
17
17
 
@@ -28,7 +28,7 @@ begin
28
28
  end
29
29
 
30
30
  module ConnectionAdapters
31
- # The DB2 adapter works with the C-based CLI driver (http://raa.ruby-lang.org/project/ruby-db2/).
31
+ # The DB2 adapter works with the C-based CLI driver (http://rubyforge.org/projects/ruby-dbi/)
32
32
  #
33
33
  # Options:
34
34
  #
@@ -44,7 +44,7 @@ begin
44
44
  select(sql, name).first
45
45
  end
46
46
 
47
- def insert(sql, name = nil, pk = nil, id_value = nil)
47
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
48
48
  execute(sql, name = nil)
49
49
  id_value || last_insert_id
50
50
  end
@@ -91,12 +91,26 @@ begin
91
91
  string.gsub(/'/, "''") # ' (for ruby-mode)
92
92
  end
93
93
 
94
- def add_limit_with_offset!(sql, limit, offset)
95
- raise ArgumentError, 'add_limit_with_offset! not implemented'
96
- end
97
-
98
- def add_limit_without_offset!(sql, limit)
99
- sql << " FETCH FIRST #{limit} ROWS ONLY"
94
+ def add_limit_offset!(sql, options)
95
+ if options[:limit] and !options[:limit].nil?
96
+ # "FETCH FIRST 0 ROWS ONLY" is not allowed, so we have
97
+ # to use a cheap trick.
98
+ if options[:limit] == 0
99
+ if sql =~ /WHERE/i
100
+ sql.sub!(/WHERE/i, 'WHERE 1 = 2 AND ')
101
+ elsif
102
+ sql =~ /ORDER\s+BY/i
103
+ sql.sub!(/ORDER\s+BY/i, 'WHERE 1 = 2 ORDER BY')
104
+ else
105
+ sql << 'WHERE 1 = 2'
106
+ end
107
+ else
108
+ sql << " FETCH FIRST #{options[:limit]} ROWS ONLY"
109
+ end
110
+ end
111
+ if options[:offset] and !options[:offset].nil?
112
+ raise ArgumentError, ':offset option is not yet supported!'
113
+ end
100
114
  end
101
115
 
102
116
  def columns(table_name, name = nil)
@@ -115,6 +129,22 @@ begin
115
129
  result
116
130
  end
117
131
 
132
+ def native_database_types
133
+ {
134
+ :primary_key => "int generated by default as identity primary key",
135
+ :string => { :name => "varchar", :limit => 255 },
136
+ :text => { :name => "clob", :limit => 32768 },
137
+ :integer => { :name => "int" },
138
+ :float => { :name => "float" },
139
+ :datetime => { :name => "timestamp" },
140
+ :timestamp => { :name => "timestamp" },
141
+ :time => { :name => "time" },
142
+ :date => { :name => "date" },
143
+ :binary => { :name => "blob", :limit => 32768 },
144
+ :boolean => { :name => "decimal", :limit => 1 }
145
+ }
146
+ end
147
+
118
148
  private
119
149
 
120
150
  def last_insert_id
@@ -128,7 +158,7 @@ begin
128
158
  stmt = nil
129
159
  log(sql, name) do
130
160
  stmt = DB2::Statement.new(@connection)
131
- stmt.exec_direct("#{sql} with ur")
161
+ stmt.exec_direct("#{sql.gsub(/=\s*null/i, 'IS NULL')} with ur")
132
162
  end
133
163
 
134
164
  rows = []
@@ -1,27 +1,30 @@
1
1
  require 'active_record/connection_adapters/abstract_adapter'
2
- require 'parsedate'
3
2
 
4
3
  module ActiveRecord
5
4
  class Base
6
5
  # Establishes a connection to the database that's used by all Active Record objects.
7
6
  def self.mysql_connection(config) # :nodoc:
7
+ # Only include the MySQL driver if one hasn't already been loaded
8
8
  unless self.class.const_defined?(:Mysql)
9
9
  begin
10
- # Only include the MySQL driver if one hasn't already been loaded
11
10
  require_library_or_gem 'mysql'
11
+ # The C version of mysql returns null fields in each_hash if Mysql::VERSION is defined
12
+ ConnectionAdapters::MysqlAdapter.null_values_in_each_hash = Mysql.const_defined?(:VERSION)
12
13
  rescue LoadError => cannot_require_mysql
13
14
  # Only use the supplied backup Ruby/MySQL driver if no driver is already in place
14
15
  begin
15
16
  require 'active_record/vendor/mysql'
16
17
  require 'active_record/vendor/mysql411'
18
+ # The ruby version of mysql returns null fields in each_hash
19
+ ConnectionAdapters::MysqlAdapter.null_values_in_each_hash = true
17
20
  rescue LoadError
18
21
  raise cannot_require_mysql
19
22
  end
20
23
  end
21
24
  end
25
+
22
26
 
23
- symbolize_strings_in_hash(config)
24
-
27
+ config = config.symbolize_keys
25
28
  host = config[:host]
26
29
  port = config[:port]
27
30
  socket = config[:socket]
@@ -41,6 +44,14 @@ module ActiveRecord
41
44
  end
42
45
 
43
46
  module ConnectionAdapters
47
+ class MysqlColumn < Column #:nodoc:
48
+ private
49
+ def simplified_type(field_type)
50
+ return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase == "tinyint(1)"
51
+ super
52
+ end
53
+ end
54
+
44
55
  # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
45
56
  # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
46
57
  #
@@ -56,19 +67,41 @@ module ActiveRecord
56
67
  # * <tt>:sslcert</tt> -- Necessary to use MySQL with an SSL connection
57
68
  # * <tt>:sslcapath</tt> -- Necessary to use MySQL with an SSL connection
58
69
  # * <tt>:sslcipher</tt> -- Necessary to use MySQL with an SSL connection
70
+ #
71
+ # By default, the MysqlAdapter will consider all columns of type tinyint(1)
72
+ # as boolean. If you wish to disable this emulation (which was the default
73
+ # behavior in versions 0.13.1 and earlier) you can add the following line
74
+ # to your environment.rb file:
75
+ #
76
+ # ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
59
77
  class MysqlAdapter < AbstractAdapter
78
+ @@emulate_booleans = true
79
+ cattr_accessor :emulate_booleans
80
+
81
+ cattr_accessor :null_values_in_each_hash
82
+ @@null_values_in_each_hash = false
83
+
60
84
  LOST_CONNECTION_ERROR_MESSAGES = [
61
85
  "Server shutdown in progress",
62
86
  "Broken pipe",
63
87
  "Lost connection to MySQL server during query",
64
88
  "MySQL server has gone away"
65
89
  ]
66
-
67
- def supports_migrations?
90
+
91
+ def initialize(connection, logger, connection_options=nil)
92
+ super(connection, logger)
93
+ @connection_options = connection_options
94
+ end
95
+
96
+ def adapter_name #:nodoc:
97
+ 'MySQL'
98
+ end
99
+
100
+ def supports_migrations? #:nodoc:
68
101
  true
69
102
  end
70
103
 
71
- def native_database_types
104
+ def native_database_types #:nodoc
72
105
  {
73
106
  :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
74
107
  :string => { :name => "varchar", :limit => 255 },
@@ -84,37 +117,38 @@ module ActiveRecord
84
117
  }
85
118
  end
86
119
 
87
- def initialize(connection, logger, connection_options=nil)
88
- super(connection, logger)
89
- @connection_options = connection_options
90
- end
91
120
 
92
- def adapter_name
93
- 'MySQL'
121
+ # QUOTING ==================================================
122
+
123
+ def quote_column_name(name) #:nodoc:
124
+ "`#{name}`"
94
125
  end
95
126
 
96
- def select_all(sql, name = nil)
97
- select(sql, name)
127
+ def quote_string(string) #:nodoc:
128
+ Mysql::quote(string)
98
129
  end
99
130
 
100
- def select_one(sql, name = nil)
101
- result = select(sql, name)
102
- result.nil? ? nil : result.first
131
+ def quoted_true
132
+ "1"
133
+ end
134
+
135
+ def quoted_false
136
+ "0"
103
137
  end
104
138
 
105
- def columns(table_name, name = nil)
106
- sql = "SHOW FIELDS FROM #{table_name}"
107
- columns = []
108
- execute(sql, name).each { |field| columns << Column.new(field[0], field[4], field[1]) }
109
- columns
139
+
140
+ # DATABASE STATEMENTS ======================================
141
+
142
+ def select_all(sql, name = nil) #:nodoc:
143
+ select(sql, name)
110
144
  end
111
145
 
112
- def insert(sql, name = nil, pk = nil, id_value = nil)
113
- execute(sql, name = nil)
114
- id_value || @connection.insert_id
146
+ def select_one(sql, name = nil) #:nodoc:
147
+ result = select(sql, name)
148
+ result.nil? ? nil : result.first
115
149
  end
116
150
 
117
- def execute(sql, name = nil, retries = 2)
151
+ def execute(sql, name = nil, retries = 2) #:nodoc:
118
152
  unless @logger
119
153
  @connection.query(sql)
120
154
  else
@@ -136,78 +170,114 @@ module ActiveRecord
136
170
  end
137
171
  end
138
172
 
139
- def update(sql, name = nil)
173
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
174
+ execute(sql, name = nil)
175
+ id_value || @connection.insert_id
176
+ end
177
+
178
+ def update(sql, name = nil) #:nodoc:
140
179
  execute(sql, name)
141
180
  @connection.affected_rows
142
181
  end
143
182
 
144
- alias_method :delete, :update
183
+ alias_method :delete, :update #:nodoc:
145
184
 
146
185
 
147
- def begin_db_transaction
186
+ def begin_db_transaction #:nodoc:
148
187
  execute "BEGIN"
149
188
  rescue Exception
150
189
  # Transactions aren't supported
151
190
  end
152
191
 
153
- def commit_db_transaction
192
+ def commit_db_transaction #:nodoc:
154
193
  execute "COMMIT"
155
194
  rescue Exception
156
195
  # Transactions aren't supported
157
196
  end
158
197
 
159
- def rollback_db_transaction
198
+ def rollback_db_transaction #:nodoc:
160
199
  execute "ROLLBACK"
161
200
  rescue Exception
162
201
  # Transactions aren't supported
163
202
  end
164
203
 
165
204
 
166
- def quote_column_name(name)
167
- "`#{name}`"
205
+ def add_limit_offset!(sql, options) #:nodoc
206
+ if limit = options[:limit]
207
+ unless offset = options[:offset]
208
+ sql << " LIMIT #{limit}"
209
+ else
210
+ sql << " LIMIT #{offset}, #{limit}"
211
+ end
212
+ end
168
213
  end
169
214
 
170
- def quote_string(string)
171
- Mysql::quote(string)
172
- end
173
215
 
216
+ # SCHEMA STATEMENTS ========================================
174
217
 
175
- def structure_dump
218
+ def structure_dump #:nodoc:
176
219
  select_all("SHOW TABLES").inject("") do |structure, table|
177
220
  structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
178
221
  end
179
222
  end
180
223
 
181
- def add_limit_offset!(sql, options)
182
- if options[:limit]
183
- if options[:offset].blank?
184
- sql << " LIMIT #{options[:limit]}"
185
- else
186
- sql << " LIMIT #{options[:offset]}, #{options[:limit]}"
187
- end
188
- end
189
- end
190
-
191
- def recreate_database(name)
224
+ def recreate_database(name) #:nodoc:
192
225
  drop_database(name)
193
226
  create_database(name)
194
227
  end
195
228
 
196
- def drop_database(name)
229
+ def create_database(name) #:nodoc:
230
+ execute "CREATE DATABASE #{name}"
231
+ end
232
+
233
+ def drop_database(name) #:nodoc:
197
234
  execute "DROP DATABASE IF EXISTS #{name}"
198
235
  end
199
236
 
200
- def create_database(name)
201
- execute "CREATE DATABASE #{name}"
237
+
238
+ def tables(name = nil) #:nodoc:
239
+ tables = []
240
+ execute("SHOW TABLES", name).each { |field| tables << field[0] }
241
+ tables
242
+ end
243
+
244
+ def indexes(table_name, name = nil)#:nodoc:
245
+ indexes = []
246
+ current_index = nil
247
+ execute("SHOW KEYS FROM #{table_name}", name).each do |row|
248
+ if current_index != row[2]
249
+ next if row[2] == "PRIMARY" # skip the primary key
250
+ current_index = row[2]
251
+ indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
252
+ end
253
+
254
+ indexes.last.columns << row[4]
255
+ end
256
+ indexes
257
+ end
258
+
259
+ def columns(table_name, name = nil)#:nodoc:
260
+ sql = "SHOW FIELDS FROM #{table_name}"
261
+ columns = []
262
+ execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
263
+ columns
264
+ end
265
+
266
+ def create_table(name, options = {}) #:nodoc:
267
+ super(name, {:options => "ENGINE=InnoDB"}.merge(options))
202
268
  end
203
269
 
204
- def change_column_default(table_name, column_name, default)
270
+ def rename_table(name, new_name)
271
+ execute "RENAME TABLE #{name} TO #{new_name}"
272
+ end
273
+
274
+ def change_column_default(table_name, column_name, default) #:nodoc:
205
275
  current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
206
276
 
207
277
  change_column(table_name, column_name, current_type, { :default => default })
208
278
  end
209
279
 
210
- def change_column(table_name, column_name, type, options = {})
280
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
211
281
  options[:default] ||= select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
212
282
 
213
283
  change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit])}"
@@ -215,22 +285,23 @@ module ActiveRecord
215
285
  execute(change_column_sql)
216
286
  end
217
287
 
218
- def rename_column(table_name, column_name, new_column_name)
288
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
219
289
  current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
220
290
  execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}"
221
291
  end
222
292
 
223
- def create_table(name, options = {})
224
- super(name, {:options => "ENGINE=InnoDB"}.merge(options))
225
- end
226
293
 
227
294
  private
228
295
  def select(sql, name = nil)
229
296
  @connection.query_with_result = true
230
297
  result = execute(sql, name)
231
298
  rows = []
232
- all_fields_initialized = result.fetch_fields.inject({}) { |all_fields, f| all_fields[f.name] = nil; all_fields }
233
- result.each_hash { |row| rows << all_fields_initialized.dup.update(row) }
299
+ if @@null_values_in_each_hash
300
+ result.each_hash { |row| rows << row }
301
+ else
302
+ all_fields = result.fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
303
+ result.each_hash { |row| rows << all_fields.dup.update(row) }
304
+ end
234
305
  result.free
235
306
  rows
236
307
  end
@@ -15,7 +15,7 @@
15
15
  # Do what you want with this code, at your own peril, but if any significant portion of my code
16
16
  # remains then please acknowledge my contribution.
17
17
  # Copyright 2005 Graham Jenkins
18
- # $Revision: 1.2 $
18
+
19
19
  require 'active_record/connection_adapters/abstract_adapter'
20
20
 
21
21
  begin
@@ -77,8 +77,8 @@ begin
77
77
  # It has also been tested against a 9i database.
78
78
  #
79
79
  # Usage notes:
80
- # * Key generation uses a sequence "rails_sequence" for all tables. (I couldn't find a simple
81
- # and safe way of passing table-specific sequence information to the adapter.)
80
+ # * Key generation assumes a "${table_name}_seq" sequence is available for all tables; the
81
+ # sequence name can be changed using ActiveRecord::Base.set_sequence_name
82
82
  # * Oracle uses DATE or TIMESTAMP datatypes for both dates and times. Consequently I have had to
83
83
  # resort to some hacks to get data converted to Date or Time in Ruby.
84
84
  # If the column_name ends in _time it's created as a Ruby Time. Else if the
@@ -99,6 +99,10 @@ begin
99
99
  # * <tt>:password</tt> -- Defaults to nothing
100
100
  # * <tt>:host</tt> -- Defaults to localhost
101
101
  class OCIAdapter < AbstractAdapter
102
+ def default_sequence_name(table, column)
103
+ "#{table}_seq"
104
+ end
105
+
102
106
  def quote_string(string)
103
107
  string.gsub(/'/, "''")
104
108
  end
@@ -106,31 +110,82 @@ begin
106
110
  def quote(value, column = nil)
107
111
  if column and column.type == :binary then %Q{empty_#{ column.sql_type }()}
108
112
  else case value
109
- when String then %Q{'#{quote_string(value)}'}
113
+ when String then %Q{'#{quote_string(value)}'}
110
114
  when NilClass then 'null'
111
115
  when TrueClass then '1'
112
116
  when FalseClass then '0'
113
- when Numeric then value.to_s
117
+ when Numeric then value.to_s
114
118
  when Date, Time then %Q{'#{value.strftime("%Y-%m-%d %H:%M:%S")}'}
115
- else %Q{'#{quote_string(value.to_yaml)}'}
119
+ else %Q{'#{quote_string(value.to_yaml)}'}
116
120
  end
117
121
  end
118
122
  end
119
123
 
124
+ # camelCase column names need to be quoted; not that anyone using Oracle
125
+ # would really do this, but handling this case means we pass the test...
126
+ def quote_column_name(name)
127
+ name =~ /[A-Z]/ ? "\"#{name}\"" : name
128
+ end
129
+
130
+ def structure_dump
131
+ s = select_all("select sequence_name from user_sequences").inject("") do |structure, seq|
132
+ structure << "create sequence #{seq.to_a.first.last};\n\n"
133
+ end
134
+
135
+ select_all("select table_name from user_tables").inject(s) do |structure, table|
136
+ ddl = "create table #{table.to_a.first.last} (\n "
137
+ cols = select_all(%Q{
138
+ select column_name, data_type, data_length, data_precision, data_scale, data_default, nullable
139
+ from user_tab_columns
140
+ where table_name = '#{table.to_a.first.last}'
141
+ order by column_id
142
+ }).map do |row|
143
+ col = "#{row['column_name'].downcase} #{row['data_type'].downcase}"
144
+ if row['data_type'] =='NUMBER' and !row['data_precision'].nil?
145
+ col << "(#{row['data_precision'].to_i}"
146
+ col << ",#{row['data_scale'].to_i}" if !row['data_scale'].nil?
147
+ col << ')'
148
+ elsif row['data_type'].include?('CHAR')
149
+ col << "(#{row['data_length'].to_i})"
150
+ end
151
+ col << " default #{row['data_default']}" if !row['data_default'].nil?
152
+ col << ' not null' if row['nullable'] == 'N'
153
+ col
154
+ end
155
+ ddl << cols.join(",\n ")
156
+ ddl << ");\n\n"
157
+ structure << ddl
158
+ end
159
+ end
160
+
161
+ def structure_drop
162
+ s = select_all("select sequence_name from user_sequences").inject("") do |drop, seq|
163
+ drop << "drop sequence #{seq.to_a.first.last};\n\n"
164
+ end
165
+
166
+ select_all("select table_name from user_tables").inject(s) do |drop, table|
167
+ drop << "drop table #{table.to_a.first.last} cascade constraints;\n\n"
168
+ end
169
+ end
170
+
120
171
  def select_all(sql, name = nil)
121
172
  offset = sql =~ /OFFSET (\d+)$/ ? $1.to_i : 0
122
173
  sql, limit = $1, $2.to_i if sql =~ /(.*)(?: LIMIT[= ](\d+))(\s*OFFSET \d+)?$/
174
+
123
175
  if limit
124
176
  sql = "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_ where rownum <= #{offset+limit}) where raw_rnum_ > #{offset}"
125
177
  elsif offset > 0
126
178
  sql = "select * from (select raw_sql_.*, rownum raw_rnum_ from (#{sql}) raw_sql_) where raw_rnum_ > #{offset}"
127
179
  end
180
+
128
181
  cursor = log(sql, name) { @connection.exec sql }
129
- cols = cursor.get_col_names.map { |x| x.downcase }
182
+ cols = cursor.get_col_names.map { |x| oci_downcase(x) }
130
183
  rows = []
184
+
131
185
  while row = cursor.fetch
132
186
  hash = Hash.new
133
- cols.each_with_index { |col, i|
187
+
188
+ cols.each_with_index do |col, i|
134
189
  hash[col] = case row[i]
135
190
  when OCI8::LOB
136
191
  name == 'Writable Large Object' ? row[i]: row[i].read
@@ -139,9 +194,11 @@ begin
139
194
  row[i].to_date : row[i].to_time
140
195
  else row[i]
141
196
  end unless col == 'raw_rnum_'
142
- }
197
+ end
198
+
143
199
  rows << hash
144
200
  end
201
+
145
202
  rows
146
203
  ensure
147
204
  cursor.close if cursor
@@ -153,25 +210,34 @@ begin
153
210
  end
154
211
 
155
212
  def columns(table_name, name = nil)
156
- cols = select_all(%Q{
213
+ select_all(%Q{
157
214
  select column_name, data_type, data_default, data_length, data_scale
158
- from user_tab_columns where table_name = '#{table_name.upcase}'}
159
- ).map { |row|
160
- OCIColumn.new row['column_name'].downcase, row['data_default'],
161
- row['data_length'], row['data_type'], row['data_scale']
162
- }
163
- cols
215
+ from user_catalog cat, user_synonyms syn, all_tab_columns col
216
+ where cat.table_name = '#{table_name.upcase}'
217
+ and syn.synonym_name (+)= cat.table_name
218
+ and col.owner = nvl(syn.table_owner, user)
219
+ and col.table_name = nvl(syn.table_name, cat.table_name)}
220
+ ).map do |row|
221
+ OCIColumn.new(
222
+ oci_downcase(row['column_name']),
223
+ row['data_default'],
224
+ row['data_length'],
225
+ row['data_type'],
226
+ row['data_scale']
227
+ )
228
+ end
164
229
  end
165
230
 
166
- def insert(sql, name = nil, pk = nil, id_value = nil)
231
+ def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
167
232
  if pk.nil? # Who called us? What does the sql look like? No idea!
168
233
  execute sql, name
169
234
  elsif id_value # Pre-assigned id
170
235
  log(sql, name) { @connection.exec sql }
171
236
  else # Assume the sql contains a bind-variable for the id
172
- id_value = select_one("select rails_sequence.nextval id from dual")['id']
237
+ id_value = select_one("select #{sequence_name}.nextval id from dual")['id']
173
238
  log(sql, name) { @connection.exec sql, id_value }
174
239
  end
240
+
175
241
  id_value
176
242
  end
177
243
 
@@ -201,18 +267,31 @@ begin
201
267
  def adapter_name()
202
268
  'OCI'
203
269
  end
270
+
271
+ private
272
+ # Oracle column names by default are case-insensitive, but treated as upcase;
273
+ # for neatness, we'll downcase within Rails. EXCEPT that folks CAN quote
274
+ # their column names when creating Oracle tables, which makes then case-sensitive.
275
+ # I don't know anybody who does this, but we'll handle the theoretical case of a
276
+ # camelCase column name. I imagine other dbs handle this different, since there's a
277
+ # unit test that's currently failing test_oci.
278
+ def oci_downcase(column_name)
279
+ column_name =~ /[a-z]/ ? column_name : column_name.downcase
280
+ end
204
281
  end
205
282
  end
206
283
  end
207
284
 
208
285
  module ActiveRecord
209
286
  class Base
210
- def self.oci_connection(config) #:nodoc:
211
- conn = OCI8.new config[:username], config[:password], config[:host]
212
- conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
213
- conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'}
214
- conn.autocommit = true
215
- ConnectionAdapters::OCIAdapter.new conn, logger
287
+ class << self
288
+ def oci_connection(config) #:nodoc:
289
+ conn = OCI8.new config[:username], config[:password], config[:host]
290
+ conn.exec %q{alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'}
291
+ conn.exec %q{alter session set nls_timestamp_format = 'YYYY-MM-DD HH24:MI:SS'}
292
+ conn.autocommit = true
293
+ ConnectionAdapters::OCIAdapter.new conn, logger
294
+ end
216
295
  end
217
296
 
218
297
  alias :attributes_with_quotes_pre_oci :attributes_with_quotes #:nodoc:
@@ -231,9 +310,10 @@ begin
231
310
 
232
311
  # After setting large objects to empty, select the OCI8::LOB and write back the data
233
312
  def write_lobs() #:nodoc:
234
- if connection.class == ConnectionAdapters::OCIAdapter
313
+ if connection.is_a?(ConnectionAdapters::OCIAdapter)
235
314
  self.class.columns.select { |c| c.type == :binary }.each { |c|
236
- break unless value = self[c.name]
315
+ value = self[c.name]
316
+ next if value.nil? || (value == '')
237
317
  lob = connection.select_one(
238
318
  "select #{ c.name} from #{ self.class.table_name } WHERE #{ self.class.primary_key} = #{quote(id)}",
239
319
  'Writable Large Object'