activerecord 1.7.0 → 1.8.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 (39) hide show
  1. data/CHANGELOG +55 -0
  2. data/lib/active_record.rb +5 -7
  3. data/lib/active_record/aggregations.rb +2 -1
  4. data/lib/active_record/associations.rb +2 -1
  5. data/lib/active_record/associations/association_proxy.rb +4 -0
  6. data/lib/active_record/associations/has_many_association.rb +6 -4
  7. data/lib/active_record/associations/has_one_association.rb +7 -2
  8. data/lib/active_record/base.rb +36 -5
  9. data/lib/active_record/connection_adapters/abstract_adapter.rb +58 -10
  10. data/lib/active_record/connection_adapters/mysql_adapter.rb +30 -9
  11. data/lib/active_record/connection_adapters/postgresql_adapter.rb +3 -2
  12. data/lib/active_record/connection_adapters/sqlite_adapter.rb +17 -0
  13. data/lib/active_record/fixtures.rb +108 -31
  14. data/lib/active_record/migration.rb +94 -0
  15. data/lib/active_record/reflection.rb +10 -4
  16. data/lib/active_record/validations.rb +76 -42
  17. data/rakefile +2 -2
  18. data/test/aaa_create_tables_test.rb +4 -4
  19. data/test/aggregations_test.rb +18 -4
  20. data/test/associations_test.rb +2 -1
  21. data/test/base_test.rb +10 -0
  22. data/test/fixtures/company.rb +1 -1
  23. data/test/fixtures/customer.rb +17 -0
  24. data/test/fixtures/customers.yml +1 -0
  25. data/test/fixtures/db_definitions/db2.sql +1 -0
  26. data/test/fixtures/db_definitions/mysql.sql +1 -0
  27. data/test/fixtures/db_definitions/oci.sql +1 -0
  28. data/test/fixtures/db_definitions/postgresql.sql +1 -0
  29. data/test/fixtures/db_definitions/sqlite.sql +2 -1
  30. data/test/fixtures/db_definitions/sqlserver.sql +1 -0
  31. data/test/fixtures/fixture_database.sqlite +0 -0
  32. data/test/fixtures/fixture_database_2.sqlite +0 -0
  33. data/test/fixtures/migrations/1_people_have_last_names.rb +9 -0
  34. data/test/fixtures/migrations/2_we_need_reminders.rb +12 -0
  35. data/test/fixtures_test.rb +30 -1
  36. data/test/migration_mysql.rb +104 -0
  37. data/test/reflection_test.rb +8 -4
  38. data/test/validations_test.rb +38 -0
  39. metadata +9 -4
data/CHANGELOG CHANGED
@@ -1,3 +1,58 @@
1
+ *1.8.0* (7th March, 2005)
2
+
3
+ * Added ActiveRecord::Base.colorize_logging to control whether to use colors in logs or not (on by default)
4
+
5
+ * Added support for timestamp with time zone in PostgreSQL #560 [Scott Barron]
6
+
7
+ * Added MultiparameterAssignmentErrors and AttributeAssignmentError exceptions #777 [demetrius]. Documentation:
8
+
9
+ * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the
10
+ +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+
11
+ objects that should be inspected to determine which attributes triggered the errors.
12
+ * +AttributeAssignmentError+ -- an error occurred while doing a mass assignment through the +attributes=+ method.
13
+ You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error.
14
+
15
+ * Fixed that postgresql adapter would fails when reading bytea fields with null value #771 [rodrigo k]
16
+
17
+ * Added transactional fixtures that uses rollback to undo changes to fixtures instead of DELETE/INSERT -- it's much faster. See documentation under Fixtures #760 [bitsweat]
18
+
19
+ * Added destruction of dependent objects in has_one associations when a new assignment happens #742 [mindel]. Example:
20
+
21
+ class Account < ActiveRecord::Base
22
+ has_one :credit_card, :dependent => true
23
+ end
24
+ class CreditCard < ActiveRecord::Base
25
+ belongs_to :account
26
+ end
27
+
28
+ account.credit_card # => returns existing credit card, lets say id = 12
29
+ account.credit_card = CreditCard.create("number" => "123")
30
+ account.save # => CC with id = 12 is destroyed
31
+
32
+
33
+ * Added validates_numericality_of #716 [skanthak/c.r.mcgrath]. Docuemntation:
34
+
35
+ Validates whether the value of the specified attribute is numeric by trying to convert it to
36
+ a float with Kernel.Float (if <tt>integer</tt> is false) or applying it to the regular expression
37
+ <tt>/^[\+\-]?\d+$/</tt> (if <tt>integer</tt> is set to true).
38
+
39
+ class Person < ActiveRecord::Base
40
+ validates_numericality_of :value, :on => :create
41
+ end
42
+
43
+ Configuration options:
44
+ * <tt>message</tt> - A custom error message (default is: "is not a number")
45
+ * <tt>on</tt> Specifies when this validation is active (default is :save, other options :create, :update)
46
+ * <tt>only_integer</tt> Specifies whether the value has to be an integer, e.g. an integral value (default is false)
47
+
48
+
49
+ * Fixed that HasManyAssociation#count was using :finder_sql rather than :counter_sql if it was available #445 [Scott Barron]
50
+
51
+ * Added better defaults for composed_of, so statements like composed_of :time_zone, :mapping => %w( time_zone time_zone ) can be written without the mapping part (it's now assumed)
52
+
53
+ * Added MacroReflection#macro which will return a symbol describing the macro used (like :composed_of or :has_many) #718, #248 [james@slashetc.com]
54
+
55
+
1
56
  *1.7.0* (24th February, 2005)
2
57
 
3
58
  * Changed the auto-timestamping feature to use ActiveRecord::Base.default_timezone instead of entertaining the parallel ActiveRecord::Base.timestamps_gmt method. The latter is now deprecated and will throw a warning on use (but still work) #710 [Jamis Buck]
@@ -23,16 +23,13 @@
23
23
 
24
24
 
25
25
  $:.unshift(File.dirname(__FILE__))
26
+ $:.unshift(File.dirname(__FILE__) + "/../../activesupport/lib")
26
27
 
27
28
  begin
28
- require 'active_support'
29
+ require 'active_support'
29
30
  rescue LoadError
30
- begin
31
- require File.dirname(__FILE__) + '/../../activesupport/lib/active_support'
32
- rescue LoadError
33
- require 'rubygems'
34
- require_gem 'activesupport'
35
- end
31
+ require 'rubygems'
32
+ require_gem 'activesupport'
36
33
  end
37
34
 
38
35
  require 'active_record/base'
@@ -47,6 +44,7 @@ require 'active_record/timestamp'
47
44
  require 'active_record/acts/list'
48
45
  require 'active_record/acts/tree'
49
46
  require 'active_record/locking'
47
+ require 'active_record/migration'
50
48
 
51
49
  ActiveRecord::Base.class_eval do
52
50
  include ActiveRecord::Validations
@@ -118,12 +118,13 @@ module ActiveRecord
118
118
  # composed_of :temperature, :mapping => %w(reading celsius)
119
119
  # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
120
120
  # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
121
+ # composed_of :gps_location
121
122
  def composed_of(part_id, options = {})
122
123
  validate_options([ :class_name, :mapping ], options.keys)
123
124
 
124
125
  name = part_id.id2name
125
126
  class_name = options[:class_name] || name_to_class_name(name)
126
- mapping = options[:mapping]
127
+ mapping = options[:mapping] || [ name, name ]
127
128
 
128
129
  reader_method(name, class_name, mapping)
129
130
  writer_method(name, class_name, mapping)
@@ -270,7 +270,8 @@ module ActiveRecord
270
270
  # sql fragment, such as "rank = 5".
271
271
  # * <tt>:order</tt> - specify the order from which the associated object will be picked at the top. Specified as
272
272
  # an "ORDER BY" sql fragment, such as "last_name, first_name DESC"
273
- # * <tt>:dependent</tt> - if set to true the associated object is destroyed alongside this object
273
+ # * <tt>:dependent</tt> - if set to true, the associated object is destroyed when this object is. It's also destroyed if another
274
+ # association is assigned.
274
275
  # * <tt>:foreign_key</tt> - specify the foreign key used for the association. By default this is guessed to be the name
275
276
  # of this class in lower-case and "_id" suffixed. So a +Person+ class that makes a has_one association will use "person_id"
276
277
  # as the default foreign_key.
@@ -34,6 +34,10 @@ module ActiveRecord
34
34
  end
35
35
 
36
36
  protected
37
+ def dependent?
38
+ @options[:dependent] || false
39
+ end
40
+
37
41
  def quoted_record_ids(records)
38
42
  records.map { |record| record.quoted_id }.join(',')
39
43
  end
@@ -24,8 +24,8 @@ module ActiveRecord
24
24
  if @options[:finder_sql]
25
25
  records = @association_class.find_by_sql(@finder_sql)
26
26
  else
27
- sql = @finder_sql.dup
28
- sql << " AND #{sanitize_sql(runtime_conditions)}" if runtime_conditions
27
+ sql = @finder_sql
28
+ sql += " AND #{sanitize_sql(runtime_conditions)}" if runtime_conditions
29
29
  orderings ||= @options[:order]
30
30
  records = @association_class.find_all(sql, orderings, limit, joins)
31
31
  end
@@ -33,11 +33,13 @@ module ActiveRecord
33
33
 
34
34
  # Count the number of associated records. All arguments are optional.
35
35
  def count(runtime_conditions = nil)
36
- if @options[:finder_sql]
36
+ if @options[:counter_sql]
37
+ @association_class.count_by_sql(@counter_sql)
38
+ elsif @options[:finder_sql]
37
39
  @association_class.count_by_sql(@finder_sql)
38
40
  else
39
41
  sql = @finder_sql
40
- sql << " AND #{sanitize_sql(runtime_conditions)}" if runtime_conditions
42
+ sql += " AND #{sanitize_sql(runtime_conditions)}" if runtime_conditions
41
43
  @association_class.count(sql)
42
44
  end
43
45
  end
@@ -10,8 +10,13 @@ module ActiveRecord
10
10
  def replace(obj, dont_save = false)
11
11
  load_target
12
12
  unless @target.nil?
13
- @target[@association_class_primary_key_name] = nil
14
- @target.save unless @owner.new_record?
13
+ if dependent? && !dont_save
14
+ @target.destroy unless @target.new_record?
15
+ @owner.clear_association_cache
16
+ else
17
+ @target[@association_class_primary_key_name] = nil
18
+ @target.save unless @owner.new_record?
19
+ end
15
20
  end
16
21
 
17
22
  if obj.nil?
@@ -1,6 +1,3 @@
1
- require 'active_support/class_attribute_accessors'
2
- require 'active_support/class_inheritable_attributes'
3
- require 'active_support/inflector'
4
1
  require 'yaml'
5
2
 
6
3
  module ActiveRecord #:nodoc:
@@ -29,6 +26,22 @@ module ActiveRecord #:nodoc:
29
26
  class StaleObjectError < ActiveRecordError #:nodoc:
30
27
  end
31
28
 
29
+ class AttributeAssignmentError < ActiveRecordError #:nodoc:
30
+ attr_reader :exception, :attribute
31
+ def initialize(message, exception, attribute)
32
+ @exception = exception
33
+ @attribute = attribute
34
+ @message = message
35
+ end
36
+ end
37
+
38
+ class MultiparameterAssignmentErrors < ActiveRecordError #:nodoc:
39
+ attr_reader :errors
40
+ def initialize(errors)
41
+ @errors = errors
42
+ end
43
+ end
44
+
32
45
  # Active Record objects doesn't specify their attributes directly, but rather infer them from the table definition with
33
46
  # which they're linked. Adding, removing, and changing attributes and their type is done directly in the database. Any change
34
47
  # is instantly reflected in the Active Record objects. The mapping that binds a given Active Record class to a certain
@@ -187,7 +200,11 @@ module ActiveRecord #:nodoc:
187
200
  # Either the row with the given ID doesn't exist or the row didn't meet the additional restrictions.
188
201
  # * +StatementInvalid+ -- the database server rejected the SQL statement. The precise error is added in the message.
189
202
  # Either the record with the given ID doesn't exist or the record didn't meet the additional restrictions.
190
- #
203
+ # * +MultiparameterAssignmentErrors+ -- collection of errors that occurred during a mass assignment using the
204
+ # +attributes=+ method. The +errors+ property of this exception contains an array of +AttributeAssignmentError+
205
+ # objects that should be inspected to determine which attributes triggered the errors.
206
+ # * +AttributeAssignmentError+ -- an error occurred while doing a mass assignment through the +attributes=+ method.
207
+ # You can inspect the +attribute+ property of the exception object to determine which attribute triggered the error.
191
208
  # *Note*: The attributes listed are class-level attributes (accessible from both the class and instance level).
192
209
  # So it's possible to assign a logger to the class through Base.logger= which will then be used by all
193
210
  # instances in the current object space.
@@ -247,6 +264,12 @@ module ActiveRecord #:nodoc:
247
264
  cattr_accessor :pluralize_table_names
248
265
  @@pluralize_table_names = true
249
266
 
267
+ # Determines whether or not to use ANSI codes to colorize the logging statements committed by the connection adapter. These colors
268
+ # makes it much easier to overview things during debugging (when used through a reader like +tail+ and on a black background), but
269
+ # may complicate matters if you use software like syslog. This is true, by default.
270
+ cattr_accessor :colorize_logging
271
+ @@colorize_logging = true
272
+
250
273
  # Determines whether to use Time.local (using :local) or Time.utc (using :utc) when pulling dates and times from the database.
251
274
  # This is set to :local by default.
252
275
  cattr_accessor :default_timezone
@@ -1254,14 +1277,22 @@ module ActiveRecord #:nodoc:
1254
1277
 
1255
1278
  # Includes an ugly hack for Time.local instead of Time.new because the latter is reserved by Time itself.
1256
1279
  def execute_callstack_for_multiparameter_attributes(callstack)
1280
+ errors = []
1257
1281
  callstack.each do |name, values|
1258
1282
  klass = (self.class.reflect_on_aggregation(name) || column_for_attribute(name)).klass
1259
1283
  if values.empty?
1260
1284
  send(name + "=", nil)
1261
1285
  else
1262
- send(name + "=", Time == klass ? klass.local(*values) : klass.new(*values))
1286
+ begin
1287
+ send(name + "=", Time == klass ? klass.local(*values) : klass.new(*values))
1288
+ rescue => ex
1289
+ errors << AttributeAssignmentError.new("error on assignment #{values.inspect} to #{name}", ex, name)
1290
+ end
1263
1291
  end
1264
1292
  end
1293
+ unless errors.empty?
1294
+ raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes"
1295
+ end
1265
1296
  end
1266
1297
 
1267
1298
  def extract_callstack_for_multiparameter_attributes(pairs)
@@ -356,6 +356,38 @@ module ActiveRecord
356
356
  sql << " LIMIT #{limit}"
357
357
  end
358
358
 
359
+
360
+ def initialize_schema_information
361
+ begin
362
+ execute "CREATE TABLE schema_info (version #{native_database_types[:integer]})"
363
+ insert "INSERT INTO schema_info (version) VALUES(0)"
364
+ rescue ActiveRecord::StatementInvalid
365
+ # Schema has been intialized
366
+ end
367
+ end
368
+
369
+ def create_table(name)
370
+ execute "CREATE TABLE #{name} (id #{native_database_types[:primary_key]})"
371
+ table_definition = yield TableDefinition.new
372
+ table_definition.columns.each { |column_name, type, options| add_column(name, column_name, type, options) }
373
+ end
374
+
375
+ def drop_table(name)
376
+ execute "DROP TABLE #{name}"
377
+ end
378
+
379
+ def add_column(table_name, column_name, type, options = {})
380
+ add_column_sql = "ALTER TABLE #{table_name} ADD #{column_name} #{native_database_types[type]}"
381
+ add_column_sql << "(#{limit})" if options[:limit]
382
+ add_column_sql << " DEFAULT '#{options[:default]}'" if options[:default]
383
+ execute(add_column_sql)
384
+ end
385
+
386
+ def remove_column(table_name, column_name)
387
+ execute "ALTER TABLE #{table_name} DROP #{column_name}"
388
+ end
389
+
390
+
359
391
  protected
360
392
  def log(sql, name, connection = nil)
361
393
  connection ||= @connection
@@ -390,18 +422,34 @@ module ActiveRecord
390
422
  end
391
423
 
392
424
  def format_log_entry(message, dump = nil)
393
- if @@row_even then
394
- @@row_even = false; caller_color = "1;32"; message_color = "4;33"; dump_color = "1;37"
425
+ if ActiveRecord::Base.colorize_logging
426
+ if @@row_even then
427
+ @@row_even = false; caller_color = "1;32"; message_color = "4;33"; dump_color = "1;37"
428
+ else
429
+ @@row_even = true; caller_color = "1;36"; message_color = "4;35"; dump_color = "0;37"
430
+ end
431
+
432
+ log_entry = " \e[#{message_color}m#{message}\e[m"
433
+ log_entry << " \e[#{dump_color}m%s\e[m" % dump if dump.kind_of?(String) && !dump.nil?
434
+ log_entry << " \e[#{dump_color}m%p\e[m" % dump if !dump.kind_of?(String) && !dump.nil?
435
+ log_entry
395
436
  else
396
- @@row_even = true; caller_color = "1;36"; message_color = "4;35"; dump_color = "0;37"
437
+ "%s %s" % [message, dump]
397
438
  end
398
-
399
- log_entry = " \e[#{message_color}m#{message}\e[m"
400
- log_entry << " \e[#{dump_color}m%s\e[m" % dump if dump.kind_of?(String) && !dump.nil?
401
- log_entry << " \e[#{dump_color}m%p\e[m" % dump if !dump.kind_of?(String) && !dump.nil?
402
- log_entry
403
439
  end
404
440
  end
405
-
441
+
442
+ class TableDefinition
443
+ attr_accessor :columns
444
+
445
+ def initialize
446
+ @columns = []
447
+ end
448
+
449
+ def column(name, type, options = {})
450
+ @columns << [ name, type, options ]
451
+ self
452
+ end
453
+ end
406
454
  end
407
- end
455
+ end
@@ -63,12 +63,33 @@ module ActiveRecord
63
63
  "Lost connection to MySQL server during query",
64
64
  "MySQL server has gone away"
65
65
  ]
66
-
66
+
67
+ def native_database_types
68
+ {
69
+ :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
70
+ :string => "varchar(255)",
71
+ :text => "text",
72
+ :integer => "int(11)",
73
+ :float => "float",
74
+ :datetime => "datetime",
75
+ :timestamp => "datetime",
76
+ :time => "datetime",
77
+ :date => "date",
78
+ :binary => "blob",
79
+ :boolean => "tinyint(1)"
80
+ }
81
+ end
82
+
67
83
  def initialize(connection, logger, connection_options=nil)
68
84
  super(connection, logger)
69
85
  @connection_options = connection_options
70
86
  end
71
87
 
88
+ def adapter_name
89
+ 'MySQL'
90
+ end
91
+
92
+
72
93
  def select_all(sql, name = nil)
73
94
  select(sql, name)
74
95
  end
@@ -111,6 +132,7 @@ module ActiveRecord
111
132
 
112
133
  alias_method :delete, :update
113
134
 
135
+
114
136
  def begin_db_transaction
115
137
  begin
116
138
  execute "BEGIN"
@@ -134,15 +156,17 @@ module ActiveRecord
134
156
  # Transactions aren't supported
135
157
  end
136
158
  end
159
+
137
160
 
138
161
  def quote_column_name(name)
139
162
  return "`#{name}`"
140
163
  end
141
164
 
142
- def adapter_name()
143
- 'MySQL'
165
+ def quote_string(s)
166
+ Mysql::quote(s)
144
167
  end
145
-
168
+
169
+
146
170
  def structure_dump
147
171
  select_all("SHOW TABLES").inject("") do |structure, table|
148
172
  structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
@@ -161,11 +185,8 @@ module ActiveRecord
161
185
  def create_database(name)
162
186
  execute "CREATE DATABASE #{name}"
163
187
  end
164
-
165
- def quote_string(s)
166
- Mysql::quote(s)
167
- end
168
-
188
+
189
+
169
190
  private
170
191
  def select(sql, name = nil)
171
192
  result = nil
@@ -143,11 +143,11 @@ module ActiveRecord
143
143
  end
144
144
 
145
145
  def escape_bytea(s)
146
- s.gsub(/\\/) { '\\\\\\\\' }.gsub(/[^\\]/) { |c| sprintf('\\\\%03o', c[0].to_i) }
146
+ s.gsub(/\\/) { '\\\\\\\\' }.gsub(/[^\\]/) { |c| sprintf('\\\\%03o', c[0].to_i) } unless s.nil?
147
147
  end
148
148
 
149
149
  def unescape_bytea(s)
150
- s.gsub(/\\([0-9][0-9][0-9])/) { $1.oct.chr }.gsub(/\\\\/) { '\\' }
150
+ s.gsub(/\\([0-9][0-9][0-9])/) { $1.oct.chr }.gsub(/\\\\/) { '\\' } unless s.nil?
151
151
  end
152
152
 
153
153
  def split_table_schema(table_name)
@@ -188,6 +188,7 @@ module ActiveRecord
188
188
  when 'numeric', 'real', 'money' then 'float'
189
189
  when 'character varying', 'interval' then 'string'
190
190
  when 'timestamp without time zone' then 'datetime'
191
+ when 'timestamp with time zone' then 'datetime'
191
192
  when 'bytea' then 'binary'
192
193
  else field_type
193
194
  end
@@ -87,6 +87,22 @@ module ActiveRecord
87
87
  #
88
88
  # * <tt>:dbfile</tt> -- Path to the database file.
89
89
  class SQLiteAdapter < AbstractAdapter
90
+ def native_database_types
91
+ {
92
+ :primary_key => "INTEGER PRIMARY KEY NOT NULL",
93
+ :string => "VARCHAR(255)",
94
+ :text => "TEXT",
95
+ :integer => "INTEGER",
96
+ :float => "float",
97
+ :datetime => "DATETIME",
98
+ :timestamp => "DATETIME",
99
+ :time => "DATETIME",
100
+ :date => "DATE",
101
+ :binary => "BLOB",
102
+ :boolean => "INTEGER"
103
+ }
104
+ end
105
+
90
106
  def execute(sql, name = nil)
91
107
  log(sql, name) { @connection.execute(sql) }
92
108
  end
@@ -150,6 +166,7 @@ module ActiveRecord
150
166
  'SQLite'
151
167
  end
152
168
 
169
+
153
170
  protected
154
171
  def table_structure(table_name)
155
172
  execute "PRAGMA table_info(#{table_name})"