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
@@ -3,16 +3,20 @@
3
3
  require 'active_record/connection_adapters/abstract_adapter'
4
4
 
5
5
  module FireRuby # :nodoc: all
6
+ NON_EXISTENT_DOMAIN_ERROR = "335544569"
6
7
  class Database
7
- def self.new_from_params(database, host, port, service)
8
- db_string = ""
9
- if host
10
- db_string << host
11
- db_string << "/#{service || port}" if service || port
12
- db_string << ":"
13
- end
14
- db_string << database
15
- new(db_string)
8
+ def self.db_string_for(config)
9
+ unless config.has_key?(:database)
10
+ raise ArgumentError, "No database specified. Missing argument: database."
11
+ end
12
+ host_string = config.values_at(:host, :service, :port).compact.first(2).join("/") if config[:host]
13
+ [host_string, config[:database]].join(":")
14
+ end
15
+
16
+ def self.new_from_config(config)
17
+ db = new db_string_for(config)
18
+ db.character_set = config[:charset]
19
+ return db
16
20
  end
17
21
  end
18
22
  end
@@ -26,13 +30,9 @@ module ActiveRecord
26
30
  'The Firebird adapter requires FireRuby version 0.4.0 or greater; you appear ' <<
27
31
  'to be running an older version -- please update FireRuby (gem install fireruby).'
28
32
  end
29
- config = config.symbolize_keys
30
- unless config.has_key?(:database)
31
- raise ArgumentError, "No database specified. Missing argument: database."
32
- end
33
- options = config[:charset] ? { CHARACTER_SET => config[:charset] } : {}
34
- connection_params = [config[:username], config[:password], options]
35
- db = FireRuby::Database.new_from_params(*config.values_at(:database, :host, :port, :service))
33
+ config.symbolize_keys!
34
+ db = FireRuby::Database.new_from_config(config)
35
+ connection_params = config.values_at(:username, :password)
36
36
  connection = db.connect(*connection_params)
37
37
  ConnectionAdapters::FirebirdAdapter.new(connection, logger, connection_params)
38
38
  end
@@ -45,10 +45,12 @@ module ActiveRecord
45
45
 
46
46
  def initialize(name, domain, type, sub_type, length, precision, scale, default_source, null_flag)
47
47
  @firebird_type = FireRuby::SQLType.to_base_type(type, sub_type).to_s
48
+
48
49
  super(name.downcase, nil, @firebird_type, !null_flag)
50
+
49
51
  @default = parse_default(default_source) if default_source
50
- @limit = type == 'BLOB' ? BLOB_MAX_LENGTH : length
51
- @domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale
52
+ @limit = decide_limit(length)
53
+ @domain, @sub_type, @precision, @scale = domain, sub_type, precision, scale.abs
52
54
  end
53
55
 
54
56
  def type
@@ -61,27 +63,12 @@ module ActiveRecord
61
63
  end
62
64
  end
63
65
 
64
- # Submits a _CAST_ query to the database, casting the default value to the specified SQL type.
65
- # This enables Firebird to provide an actual value when context variables are used as column
66
- # defaults (such as CURRENT_TIMESTAMP).
67
66
  def default
68
- if @default
69
- sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE"
70
- connection = ActiveRecord::Base.active_connections.values.detect { |conn| conn && conn.adapter_name == 'Firebird' }
71
- if connection
72
- type_cast connection.execute(sql).to_a.first['CAST']
73
- else
74
- raise ConnectionNotEstablished, "No Firebird connections established."
75
- end
76
- end
67
+ type_cast(decide_default) if @default
77
68
  end
78
69
 
79
- def type_cast(value)
80
- if type == :boolean
81
- value == true or value == ActiveRecord::ConnectionAdapters::FirebirdAdapter.boolean_domain[:true]
82
- else
83
- super
84
- end
70
+ def self.value_to_boolean(value)
71
+ %W(#{FirebirdAdapter.boolean_domain[:true]} true t 1).include? value.to_s.downcase
85
72
  end
86
73
 
87
74
  private
@@ -90,6 +77,35 @@ module ActiveRecord
90
77
  return $1 unless $1.upcase == "NULL"
91
78
  end
92
79
 
80
+ def decide_default
81
+ if @default =~ /^'?(\d*\.?\d+)'?$/ or
82
+ @default =~ /^'(.*)'$/ && [:text, :string, :binary, :boolean].include?(type)
83
+ $1
84
+ else
85
+ firebird_cast_default
86
+ end
87
+ end
88
+
89
+ # Submits a _CAST_ query to the database, casting the default value to the specified SQL type.
90
+ # This enables Firebird to provide an actual value when context variables are used as column
91
+ # defaults (such as CURRENT_TIMESTAMP).
92
+ def firebird_cast_default
93
+ sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE"
94
+ if connection = Base.active_connections.values.detect { |conn| conn && conn.adapter_name == 'Firebird' }
95
+ connection.execute(sql).to_a.first['CAST']
96
+ else
97
+ raise ConnectionNotEstablished, "No Firebird connections established."
98
+ end
99
+ end
100
+
101
+ def decide_limit(length)
102
+ if text? or number?
103
+ length
104
+ elsif @firebird_type == 'BLOB'
105
+ BLOB_MAX_LENGTH
106
+ end
107
+ end
108
+
93
109
  def column_def
94
110
  case @firebird_type
95
111
  when 'BLOB' then "VARCHAR(#{VARCHAR_MAX_LENGTH})"
@@ -133,7 +149,7 @@ module ActiveRecord
133
149
  # Firebird 1.5 does not provide a native +BOOLEAN+ type. But you can easily
134
150
  # define a +BOOLEAN+ _domain_ for this purpose, e.g.:
135
151
  #
136
- # CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1));
152
+ # CREATE DOMAIN D_BOOLEAN AS SMALLINT CHECK (VALUE IN (0, 1) OR VALUE IS NULL);
137
153
  #
138
154
  # When the Firebird adapter encounters a column that is based on a domain
139
155
  # that includes "BOOLEAN" in the domain name, it will attempt to treat
@@ -200,8 +216,23 @@ module ActiveRecord
200
216
  # as column names as well.
201
217
  #
202
218
  # === Migrations
203
- # The Firebird adapter does not currently support Migrations. I hope to
204
- # add this feature in the near future.
219
+ # The Firebird Adapter now supports Migrations.
220
+ #
221
+ # ==== Create/Drop Table and Sequence Generators
222
+ # Creating or dropping a table will automatically create/drop a
223
+ # correpsonding sequence generator, using the default naming convension.
224
+ # You can specify a different name using the <tt>:sequence</tt> option; no
225
+ # generator is created if <tt>:sequence</tt> is set to +false+.
226
+ #
227
+ # ==== Rename Table
228
+ # The Firebird #rename_table Migration should be used with caution.
229
+ # Firebird 1.5 lacks built-in support for this feature, so it is
230
+ # implemented by making a copy of the original table (including column
231
+ # definitions, indexes and data records), and then dropping the original
232
+ # table. Constraints and Triggers are _not_ properly copied, so avoid
233
+ # this method if your original table includes constraints (other than
234
+ # the primary key) or triggers. (Consider manually copying your table
235
+ # or using a view instead.)
205
236
  #
206
237
  # == Connection Options
207
238
  # The following options are supported by the Firebird adapter. None of the
@@ -238,10 +269,12 @@ module ActiveRecord
238
269
  # Specifies the character set to be used by the connection. Refer to
239
270
  # Firebird documentation for valid options.
240
271
  class FirebirdAdapter < AbstractAdapter
241
- @@boolean_domain = { :true => 1, :false => 0 }
272
+ TEMP_COLUMN_NAME = 'AR$TEMP_COLUMN'
273
+
274
+ @@boolean_domain = { :name => "d_boolean", :type => "smallint", :true => 1, :false => 0 }
242
275
  cattr_accessor :boolean_domain
243
276
 
244
- def initialize(connection, logger, connection_params=nil)
277
+ def initialize(connection, logger, connection_params = nil)
245
278
  super(connection, logger)
246
279
  @connection_params = connection_params
247
280
  end
@@ -250,13 +283,35 @@ module ActiveRecord
250
283
  'Firebird'
251
284
  end
252
285
 
286
+ def supports_migrations? # :nodoc:
287
+ true
288
+ end
289
+
290
+ def native_database_types # :nodoc:
291
+ {
292
+ :primary_key => "BIGINT NOT NULL PRIMARY KEY",
293
+ :string => { :name => "varchar", :limit => 255 },
294
+ :text => { :name => "blob sub_type text" },
295
+ :integer => { :name => "bigint" },
296
+ :decimal => { :name => "decimal" },
297
+ :numeric => { :name => "numeric" },
298
+ :float => { :name => "float" },
299
+ :datetime => { :name => "timestamp" },
300
+ :timestamp => { :name => "timestamp" },
301
+ :time => { :name => "time" },
302
+ :date => { :name => "date" },
303
+ :binary => { :name => "blob sub_type 0" },
304
+ :boolean => boolean_domain
305
+ }
306
+ end
307
+
253
308
  # Returns true for Firebird adapter (since Firebird requires primary key
254
309
  # values to be pre-fetched before insert). See also #next_sequence_value.
255
310
  def prefetch_primary_key?(table_name = nil)
256
311
  true
257
312
  end
258
313
 
259
- def default_sequence_name(table_name, primary_key) # :nodoc:
314
+ def default_sequence_name(table_name, primary_key = nil) # :nodoc:
260
315
  "#{table_name}_seq"
261
316
  end
262
317
 
@@ -276,7 +331,7 @@ module ActiveRecord
276
331
  end
277
332
 
278
333
  def quote_column_name(column_name) # :nodoc:
279
- %Q("#{ar_to_fb_case(column_name)}")
334
+ %Q("#{ar_to_fb_case(column_name.to_s)}")
280
335
  end
281
336
 
282
337
  def quoted_true # :nodoc:
@@ -290,12 +345,16 @@ module ActiveRecord
290
345
 
291
346
  # CONNECTION MANAGEMENT ====================================
292
347
 
293
- def active?
348
+ def active? # :nodoc:
294
349
  not @connection.closed?
295
350
  end
296
351
 
297
- def reconnect!
298
- @connection.close
352
+ def disconnect! # :nodoc:
353
+ @connection.close rescue nil
354
+ end
355
+
356
+ def reconnect! # :nodoc:
357
+ disconnect!
299
358
  @connection = @connection.database.connect(*@connection_params)
300
359
  end
301
360
 
@@ -307,8 +366,7 @@ module ActiveRecord
307
366
  end
308
367
 
309
368
  def select_one(sql, name = nil) # :nodoc:
310
- result = select(sql, name)
311
- result.nil? ? nil : result.first
369
+ select(sql, name).first
312
370
  end
313
371
 
314
372
  def execute(sql, name = nil, &block) # :nodoc:
@@ -363,8 +421,37 @@ module ActiveRecord
363
421
 
364
422
  # SCHEMA STATEMENTS ========================================
365
423
 
424
+ def current_database # :nodoc:
425
+ file = @connection.database.file.split(':').last
426
+ File.basename(file, '.*')
427
+ end
428
+
429
+ def recreate_database! # :nodoc:
430
+ sql = "SELECT rdb$character_set_name FROM rdb$database"
431
+ charset = execute(sql).to_a.first[0].rstrip
432
+ disconnect!
433
+ @connection.database.drop(*@connection_params)
434
+ FireRuby::Database.create(@connection.database.file,
435
+ @connection_params[0], @connection_params[1], 4096, charset)
436
+ end
437
+
438
+ def tables(name = nil) # :nodoc:
439
+ sql = "SELECT rdb$relation_name FROM rdb$relations WHERE rdb$system_flag = 0"
440
+ execute(sql, name).collect { |row| row[0].rstrip.downcase }
441
+ end
442
+
443
+ def indexes(table_name, name = nil) # :nodoc:
444
+ index_metadata(table_name, false, name).inject([]) do |indexes, row|
445
+ if indexes.empty? or indexes.last.name != row[0]
446
+ indexes << IndexDefinition.new(table_name, row[0].rstrip.downcase, row[1] == 1, [])
447
+ end
448
+ indexes.last.columns << row[2].rstrip.downcase
449
+ indexes
450
+ end
451
+ end
452
+
366
453
  def columns(table_name, name = nil) # :nodoc:
367
- sql = <<-END_SQL
454
+ sql = <<-end_sql
368
455
  SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type,
369
456
  f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale,
370
457
  COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source,
@@ -373,7 +460,7 @@ module ActiveRecord
373
460
  JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
374
461
  WHERE r.rdb$relation_name = '#{table_name.to_s.upcase}'
375
462
  ORDER BY r.rdb$field_position
376
- END_SQL
463
+ end_sql
377
464
  execute(sql, name).collect do |field|
378
465
  field_values = field.values.collect do |value|
379
466
  case value
@@ -386,7 +473,120 @@ module ActiveRecord
386
473
  end
387
474
  end
388
475
 
476
+ def create_table(name, options = {}) # :nodoc:
477
+ begin
478
+ super
479
+ rescue StatementInvalid
480
+ raise unless non_existent_domain_error?
481
+ create_boolean_domain
482
+ super
483
+ end
484
+ unless options[:id] == false or options[:sequence] == false
485
+ sequence_name = options[:sequence] || default_sequence_name(name)
486
+ create_sequence(sequence_name)
487
+ end
488
+ end
489
+
490
+ def drop_table(name, options = {}) # :nodoc:
491
+ super(name)
492
+ unless options[:sequence] == false
493
+ sequence_name = options[:sequence] || default_sequence_name(name)
494
+ drop_sequence(sequence_name) if sequence_exists?(sequence_name)
495
+ end
496
+ end
497
+
498
+ def add_column(table_name, column_name, type, options = {}) # :nodoc:
499
+ super
500
+ rescue StatementInvalid
501
+ raise unless non_existent_domain_error?
502
+ create_boolean_domain
503
+ super
504
+ end
505
+
506
+ def change_column(table_name, column_name, type, options = {}) # :nodoc:
507
+ change_column_type(table_name, column_name, type, options)
508
+ change_column_position(table_name, column_name, options[:position]) if options.include?(:position)
509
+ change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
510
+ end
511
+
512
+ def change_column_default(table_name, column_name, default) # :nodoc:
513
+ table_name = table_name.to_s.upcase
514
+ sql = <<-end_sql
515
+ UPDATE rdb$relation_fields f1
516
+ SET f1.rdb$default_source =
517
+ (SELECT f2.rdb$default_source FROM rdb$relation_fields f2
518
+ WHERE f2.rdb$relation_name = '#{table_name}'
519
+ AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}'),
520
+ f1.rdb$default_value =
521
+ (SELECT f2.rdb$default_value FROM rdb$relation_fields f2
522
+ WHERE f2.rdb$relation_name = '#{table_name}'
523
+ AND f2.rdb$field_name = '#{TEMP_COLUMN_NAME}')
524
+ WHERE f1.rdb$relation_name = '#{table_name}'
525
+ AND f1.rdb$field_name = '#{ar_to_fb_case(column_name.to_s)}'
526
+ end_sql
527
+ transaction do
528
+ add_column(table_name, TEMP_COLUMN_NAME, :string, :default => default)
529
+ execute sql
530
+ remove_column(table_name, TEMP_COLUMN_NAME)
531
+ end
532
+ end
533
+
534
+ def rename_column(table_name, column_name, new_column_name) # :nodoc:
535
+ execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TO #{new_column_name}"
536
+ end
537
+
538
+ def remove_index(table_name, options) #:nodoc:
539
+ execute "DROP INDEX #{quote_column_name(index_name(table_name, options))}"
540
+ end
541
+
542
+ def rename_table(name, new_name) # :nodoc:
543
+ if table_has_constraints_or_dependencies?(name)
544
+ raise ActiveRecordError,
545
+ "Table #{name} includes constraints or dependencies that are not supported by " <<
546
+ "the Firebird rename_table migration. Try explicitly removing the constraints/" <<
547
+ "dependencies first, or manually renaming the table."
548
+ end
549
+
550
+ transaction do
551
+ copy_table(name, new_name)
552
+ copy_table_indexes(name, new_name)
553
+ end
554
+ begin
555
+ copy_table_data(name, new_name)
556
+ copy_sequence_value(name, new_name)
557
+ rescue
558
+ drop_table(new_name)
559
+ raise
560
+ end
561
+ drop_table(name)
562
+ end
563
+
564
+ def dump_schema_information # :nodoc:
565
+ super << ";\n"
566
+ end
567
+
568
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil) # :nodoc:
569
+ case type
570
+ when :integer then integer_sql_type(limit)
571
+ when :float then float_sql_type(limit)
572
+ when :string then super(type, limit, precision, scale)
573
+ else super(type, limit, precision, scale)
574
+ end
575
+ end
576
+
389
577
  private
578
+ def integer_sql_type(limit)
579
+ case limit
580
+ when (1..2) then 'smallint'
581
+ when (3..4) then 'integer'
582
+ else 'bigint'
583
+ end
584
+ end
585
+
586
+ def float_sql_type(limit)
587
+ limit.to_i <= 4 ? 'float' : 'double precision'
588
+ end
589
+
390
590
  def select(sql, name = nil)
391
591
  execute(sql, name).collect do |row|
392
592
  hashed_row = {}
@@ -398,6 +598,120 @@ module ActiveRecord
398
598
  end
399
599
  end
400
600
 
601
+ def primary_key(table_name)
602
+ if pk_row = index_metadata(table_name, true).to_a.first
603
+ pk_row[2].rstrip.downcase
604
+ end
605
+ end
606
+
607
+ def index_metadata(table_name, pk, name = nil)
608
+ sql = <<-end_sql
609
+ SELECT i.rdb$index_name, i.rdb$unique_flag, s.rdb$field_name
610
+ FROM rdb$indices i
611
+ JOIN rdb$index_segments s ON i.rdb$index_name = s.rdb$index_name
612
+ LEFT JOIN rdb$relation_constraints c ON i.rdb$index_name = c.rdb$index_name
613
+ WHERE i.rdb$relation_name = '#{table_name.to_s.upcase}'
614
+ end_sql
615
+ if pk
616
+ sql << "AND c.rdb$constraint_type = 'PRIMARY KEY'\n"
617
+ else
618
+ sql << "AND (c.rdb$constraint_type IS NULL OR c.rdb$constraint_type != 'PRIMARY KEY')\n"
619
+ end
620
+ sql << "ORDER BY i.rdb$index_name, s.rdb$field_position\n"
621
+ execute sql, name
622
+ end
623
+
624
+ def change_column_type(table_name, column_name, type, options = {})
625
+ sql = "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} TYPE #{type_to_sql(type, options[:limit])}"
626
+ execute sql
627
+ rescue StatementInvalid
628
+ raise unless non_existent_domain_error?
629
+ create_boolean_domain
630
+ execute sql
631
+ end
632
+
633
+ def change_column_position(table_name, column_name, position)
634
+ execute "ALTER TABLE #{table_name} ALTER COLUMN #{column_name} POSITION #{position}"
635
+ end
636
+
637
+ def copy_table(from, to)
638
+ table_opts = {}
639
+ if pk = primary_key(from)
640
+ table_opts[:primary_key] = pk
641
+ else
642
+ table_opts[:id] = false
643
+ end
644
+ create_table(to, table_opts) do |table|
645
+ from_columns = columns(from).reject { |col| col.name == table_opts[:primary_key] }
646
+ from_columns.each do |column|
647
+ col_opts = [:limit, :default, :null].inject({}) { |opts, opt| opts.merge(opt => column.send(opt)) }
648
+ table.column column.name, column.type, col_opts
649
+ end
650
+ end
651
+ end
652
+
653
+ def copy_table_indexes(from, to)
654
+ indexes(from).each do |index|
655
+ unless index.name[from.to_s]
656
+ raise ActiveRecordError,
657
+ "Cannot rename index #{index.name}, because the index name does not include " <<
658
+ "the original table name (#{from}). Try explicitly removing the index on the " <<
659
+ "original table and re-adding it on the new (renamed) table."
660
+ end
661
+ options = {}
662
+ options[:name] = index.name.gsub(from.to_s, to.to_s)
663
+ options[:unique] = index.unique
664
+ add_index(to, index.columns, options)
665
+ end
666
+ end
667
+
668
+ def copy_table_data(from, to)
669
+ execute "INSERT INTO #{to} SELECT * FROM #{from}", "Copy #{from} data to #{to}"
670
+ end
671
+
672
+ def copy_sequence_value(from, to)
673
+ sequence_value = FireRuby::Generator.new(default_sequence_name(from), @connection).last
674
+ execute "SET GENERATOR #{default_sequence_name(to)} TO #{sequence_value}"
675
+ end
676
+
677
+ def sequence_exists?(sequence_name)
678
+ FireRuby::Generator.exists?(sequence_name, @connection)
679
+ end
680
+
681
+ def create_sequence(sequence_name)
682
+ FireRuby::Generator.create(sequence_name.to_s, @connection)
683
+ end
684
+
685
+ def drop_sequence(sequence_name)
686
+ FireRuby::Generator.new(sequence_name.to_s, @connection).drop
687
+ end
688
+
689
+ def create_boolean_domain
690
+ sql = <<-end_sql
691
+ CREATE DOMAIN #{boolean_domain[:name]} AS #{boolean_domain[:type]}
692
+ CHECK (VALUE IN (#{quoted_true}, #{quoted_false}) OR VALUE IS NULL)
693
+ end_sql
694
+ execute sql rescue nil
695
+ end
696
+
697
+ def table_has_constraints_or_dependencies?(table_name)
698
+ table_name = table_name.to_s.upcase
699
+ sql = <<-end_sql
700
+ SELECT 1 FROM rdb$relation_constraints
701
+ WHERE rdb$relation_name = '#{table_name}'
702
+ AND rdb$constraint_type IN ('UNIQUE', 'FOREIGN KEY', 'CHECK')
703
+ UNION
704
+ SELECT 1 FROM rdb$dependencies
705
+ WHERE rdb$depended_on_name = '#{table_name}'
706
+ AND rdb$depended_on_type = 0
707
+ end_sql
708
+ !select(sql).empty?
709
+ end
710
+
711
+ def non_existent_domain_error?
712
+ $!.message.include? FireRuby::NON_EXISTENT_DOMAIN_ERROR
713
+ end
714
+
401
715
  # Maps uppercase Firebird column names to lowercase for ActiveRecord;
402
716
  # mixed-case columns retain their original case.
403
717
  def fb_to_ar_case(column_name)