activerecord 3.2.8 → 3.2.9.rc1

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 (28) hide show
  1. data/CHANGELOG.md +154 -6558
  2. data/examples/performance.rb +32 -37
  3. data/lib/active_record/associations.rb +8 -5
  4. data/lib/active_record/associations/collection_association.rb +5 -3
  5. data/lib/active_record/associations/has_many_through_association.rb +14 -0
  6. data/lib/active_record/associations/has_one_association.rb +16 -14
  7. data/lib/active_record/associations/preloader.rb +14 -10
  8. data/lib/active_record/associations/preloader/association.rb +1 -3
  9. data/lib/active_record/attribute_methods/dirty.rb +3 -3
  10. data/lib/active_record/attribute_methods/time_zone_conversion.rb +14 -5
  11. data/lib/active_record/base.rb +4 -5
  12. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +10 -4
  13. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +1 -0
  14. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +1 -1
  15. data/lib/active_record/connection_adapters/column.rb +24 -9
  16. data/lib/active_record/connection_adapters/postgresql_adapter.rb +29 -33
  17. data/lib/active_record/connection_adapters/sqlite_adapter.rb +3 -3
  18. data/lib/active_record/counter_cache.rb +5 -1
  19. data/lib/active_record/explain_subscriber.rb +2 -1
  20. data/lib/active_record/migration.rb +2 -2
  21. data/lib/active_record/persistence.rb +9 -8
  22. data/lib/active_record/railties/databases.rake +6 -6
  23. data/lib/active_record/scoping/named.rb +2 -2
  24. data/lib/active_record/store.rb +2 -0
  25. data/lib/active_record/timestamp.rb +1 -0
  26. data/lib/active_record/transactions.rb +22 -3
  27. data/lib/active_record/version.rb +2 -2
  28. metadata +9 -12
@@ -1,7 +1,9 @@
1
- TIMES = (ENV['N'] || 10000).to_i
2
-
3
1
  require File.expand_path('../../../load_paths', __FILE__)
4
2
  require "active_record"
3
+ require 'benchmark/ips'
4
+
5
+ TIME = (ENV['BENCHMARK_TIME'] || 20).to_i
6
+ RECORDS = (ENV['BENCHMARK_RECORDS'] || TIME*1000).to_i
5
7
 
6
8
  conn = { :adapter => 'sqlite3', :database => ':memory:' }
7
9
 
@@ -72,8 +74,8 @@ end
72
74
  notes = ActiveRecord::Faker::LOREM.join ' '
73
75
  today = Date.today
74
76
 
75
- puts 'Inserting 10,000 users and exhibits...'
76
- 10_000.times do
77
+ puts "Inserting #{RECORDS} users and exhibits..."
78
+ RECORDS.times do
77
79
  user = User.create(
78
80
  :created_at => today,
79
81
  :name => ActiveRecord::Faker.name,
@@ -88,9 +90,7 @@ puts 'Inserting 10,000 users and exhibits...'
88
90
  )
89
91
  end
90
92
 
91
- require 'benchmark'
92
-
93
- Benchmark.bm(46) do |x|
93
+ Benchmark.ips(TIME) do |x|
94
94
  ar_obj = Exhibit.find(1)
95
95
  attrs = { :name => 'sam' }
96
96
  attrs_first = { :name => 'sam' }
@@ -101,77 +101,72 @@ Benchmark.bm(46) do |x|
101
101
  :created_at => Date.today
102
102
  }
103
103
 
104
- x.report("Model#id (x#{(TIMES * 100).ceil})") do
105
- (TIMES * 100).ceil.times { ar_obj.id }
104
+ x.report("Model#id") do
105
+ ar_obj.id
106
106
  end
107
107
 
108
108
  x.report 'Model.new (instantiation)' do
109
- TIMES.times { Exhibit.new }
109
+ Exhibit.new
110
110
  end
111
111
 
112
112
  x.report 'Model.new (setting attributes)' do
113
- TIMES.times { Exhibit.new(attrs) }
113
+ Exhibit.new(attrs)
114
114
  end
115
115
 
116
116
  x.report 'Model.first' do
117
- TIMES.times { Exhibit.first.look }
117
+ Exhibit.first.look
118
118
  end
119
119
 
120
- x.report 'Model.named_scope' do
121
- TIMES.times { Exhibit.limit(10).with_name.with_notes }
120
+ x.report("Model.all limit(100)") do
121
+ Exhibit.look Exhibit.limit(100)
122
122
  end
123
123
 
124
- x.report("Model.all limit(100) (x#{(TIMES / 10).ceil})") do
125
- (TIMES / 10).ceil.times { Exhibit.look Exhibit.limit(100) }
124
+ x.report "Model.all limit(100) with relationship" do
125
+ Exhibit.feel Exhibit.limit(100).includes(:user)
126
126
  end
127
127
 
128
- x.report "Model.all limit(100) with relationship (x#{(TIMES / 10).ceil})" do
129
- (TIMES / 10).ceil.times { Exhibit.feel Exhibit.limit(100).includes(:user) }
128
+ x.report "Model.all limit(10,000)" do
129
+ Exhibit.look Exhibit.limit(10000)
130
130
  end
131
131
 
132
- x.report "Model.all limit(10,000) x(#{(TIMES / 1000).ceil})" do
133
- (TIMES / 1000).ceil.times { Exhibit.look Exhibit.limit(10000) }
132
+ x.report 'Model.named_scope' do
133
+ Exhibit.limit(10).with_name.with_notes
134
134
  end
135
135
 
136
136
  x.report 'Model.create' do
137
- TIMES.times { Exhibit.create(exhibit) }
137
+ Exhibit.create(exhibit)
138
138
  end
139
139
 
140
140
  x.report 'Resource#attributes=' do
141
- TIMES.times {
142
- exhibit = Exhibit.new(attrs_first)
143
- exhibit.attributes = attrs_second
144
- }
141
+ e = Exhibit.new(attrs_first)
142
+ e.attributes = attrs_second
145
143
  end
146
144
 
147
145
  x.report 'Resource#update' do
148
- TIMES.times { Exhibit.first.update_attributes(:name => 'bob') }
146
+ Exhibit.first.update_attributes(:name => 'bob')
149
147
  end
150
148
 
151
149
  x.report 'Resource#destroy' do
152
- TIMES.times { Exhibit.first.destroy }
150
+ Exhibit.first.destroy
153
151
  end
154
152
 
155
153
  x.report 'Model.transaction' do
156
- TIMES.times { Exhibit.transaction { Exhibit.new } }
154
+ Exhibit.transaction { Exhibit.new }
157
155
  end
158
156
 
159
157
  x.report 'Model.find(id)' do
160
- id = Exhibit.first.id
161
- TIMES.times { Exhibit.find(id) }
158
+ User.find(1)
162
159
  end
163
160
 
164
161
  x.report 'Model.find_by_sql' do
165
- TIMES.times {
166
- Exhibit.find_by_sql("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}").first
167
- }
162
+ Exhibit.find_by_sql("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}").first
168
163
  end
169
164
 
170
- x.report "Model.log x(#{TIMES * 10})" do
171
- (TIMES * 10).times { Exhibit.connection.send(:log, "hello", "world") {} }
165
+ x.report "Model.log" do
166
+ Exhibit.connection.send(:log, "hello", "world") {}
172
167
  end
173
168
 
174
- x.report "AR.execute(query) (#{TIMES / 2})" do
175
- (TIMES / 2).times { ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}") }
169
+ x.report "AR.execute(query)" do
170
+ ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}")
176
171
  end
177
172
  end
@@ -105,6 +105,7 @@ module ActiveRecord
105
105
 
106
106
  # See ActiveRecord::Associations::ClassMethods for documentation.
107
107
  module Associations # :nodoc:
108
+ extend ActiveSupport::Autoload
108
109
  extend ActiveSupport::Concern
109
110
 
110
111
  # These classes will be loaded when associations are created.
@@ -134,11 +135,13 @@ module ActiveRecord
134
135
  autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
135
136
  end
136
137
 
137
- autoload :Preloader, 'active_record/associations/preloader'
138
- autoload :JoinDependency, 'active_record/associations/join_dependency'
139
- autoload :AssociationScope, 'active_record/associations/association_scope'
140
- autoload :AliasTracker, 'active_record/associations/alias_tracker'
141
- autoload :JoinHelper, 'active_record/associations/join_helper'
138
+ eager_autoload do
139
+ autoload :Preloader, 'active_record/associations/preloader'
140
+ autoload :JoinDependency, 'active_record/associations/join_dependency'
141
+ autoload :AssociationScope, 'active_record/associations/association_scope'
142
+ autoload :AliasTracker, 'active_record/associations/alias_tracker'
143
+ autoload :JoinHelper, 'active_record/associations/join_helper'
144
+ end
142
145
 
143
146
  # Clears out the association cache.
144
147
  def clear_association_cache #:nodoc:
@@ -190,6 +190,8 @@ module ActiveRecord
190
190
  # association, it will be used for the query. Otherwise, construct options and pass them with
191
191
  # scope to the target class's +count+.
192
192
  def count(column_name = nil, count_options = {})
193
+ return 0 if owner.new_record?
194
+
193
195
  column_name, count_options = nil, column_name if column_name.is_a?(Hash)
194
196
 
195
197
  if options[:counter_sql] || options[:finder_sql]
@@ -362,7 +364,7 @@ module ActiveRecord
362
364
  # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
363
365
  interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
364
366
  count_with = $2.to_s
365
- count_with = '*' if count_with.blank? || count_with =~ /,/
367
+ count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
366
368
  "SELECT #{$1}COUNT(#{count_with}) FROM"
367
369
  end
368
370
  end
@@ -407,7 +409,7 @@ module ActiveRecord
407
409
  if mem_index
408
410
  mem_record = memory.delete_at(mem_index)
409
411
 
410
- (record.attribute_names - mem_record.changes.keys).each do |name|
412
+ ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name|
411
413
  mem_record[name] = record[name]
412
414
  end
413
415
 
@@ -569,7 +571,7 @@ module ActiveRecord
569
571
  args.shift if args.first.is_a?(Hash) && args.first.empty?
570
572
 
571
573
  collection = fetch_first_or_last_using_find?(args) ? scoped : load_target
572
- collection.send(type, *args)
574
+ collection.send(type, *args).tap {|it| set_inverse_instance it }
573
575
  end
574
576
  end
575
577
  end
@@ -38,6 +38,20 @@ module ActiveRecord
38
38
  super
39
39
  end
40
40
 
41
+ def concat_records(records)
42
+ ensure_not_nested
43
+
44
+ records = super
45
+
46
+ if owner.new_record? && records
47
+ records.flatten.each do |record|
48
+ build_through_record(record)
49
+ end
50
+ end
51
+
52
+ records
53
+ end
54
+
41
55
  def insert_record(record, validate = true, raise = false)
42
56
  ensure_not_nested
43
57
 
@@ -8,19 +8,21 @@ module ActiveRecord
8
8
  raise_on_type_mismatch(record) if record
9
9
  load_target
10
10
 
11
- reflection.klass.transaction do
12
- if target && target != record
13
- remove_target!(options[:dependent]) unless target.destroyed?
14
- end
15
-
16
- if record
17
- set_owner_attributes(record)
18
- set_inverse_instance(record)
19
-
20
- if owner.persisted? && save && !record.save
21
- nullify_owner_attributes(record)
22
- set_owner_attributes(target) if target
23
- raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
11
+ # If target and record are nil, or target is equal to record,
12
+ # we don't need to have transaction.
13
+ if (target || record) && target != record
14
+ reflection.klass.transaction do
15
+ remove_target!(options[:dependent]) if target && !target.destroyed?
16
+
17
+ if record
18
+ set_owner_attributes(record)
19
+ set_inverse_instance(record)
20
+
21
+ if owner.persisted? && save && !record.save
22
+ nullify_owner_attributes(record)
23
+ set_owner_attributes(target) if target
24
+ raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
25
+ end
24
26
  end
25
27
  end
26
28
  end
@@ -36,7 +38,7 @@ module ActiveRecord
36
38
  when :destroy
37
39
  target.destroy
38
40
  when :nullify
39
- target.update_column(reflection.foreign_key, nil)
41
+ target.update_attribute(reflection.foreign_key, nil)
40
42
  end
41
43
  end
42
44
  end
@@ -30,17 +30,21 @@ module ActiveRecord
30
30
  # option references an association's column), it will fallback to the table
31
31
  # join strategy.
32
32
  class Preloader #:nodoc:
33
- autoload :Association, 'active_record/associations/preloader/association'
34
- autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
35
- autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
36
- autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
33
+ extend ActiveSupport::Autoload
37
34
 
38
- autoload :HasMany, 'active_record/associations/preloader/has_many'
39
- autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
40
- autoload :HasOne, 'active_record/associations/preloader/has_one'
41
- autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
42
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
43
- autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
35
+ eager_autoload do
36
+ autoload :Association, 'active_record/associations/preloader/association'
37
+ autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
38
+ autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
39
+ autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
40
+
41
+ autoload :HasMany, 'active_record/associations/preloader/has_many'
42
+ autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
43
+ autoload :HasOne, 'active_record/associations/preloader/has_one'
44
+ autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
45
+ autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
46
+ autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
47
+ end
44
48
 
45
49
  attr_reader :records, :associations, :options, :model
46
50
 
@@ -117,9 +117,7 @@ module ActiveRecord
117
117
  conditions = klass.send(:instance_eval, &conditions)
118
118
  end
119
119
 
120
- if conditions
121
- klass.send(:sanitize_sql, conditions)
122
- end
120
+ conditions
123
121
  end
124
122
  end
125
123
  end
@@ -55,12 +55,12 @@ module ActiveRecord
55
55
  # The attribute already has an unsaved change.
56
56
  if attribute_changed?(attr)
57
57
  old = @changed_attributes[attr]
58
- @changed_attributes.delete(attr) unless field_changed?(attr, old, value)
58
+ @changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
59
59
  else
60
60
  old = clone_attribute_value(:read_attribute, attr)
61
61
  # Save Time objects as TimeWithZone if time_zone_aware_attributes == true
62
62
  old = old.in_time_zone if clone_with_time_zone_conversion_attribute?(attr, old)
63
- @changed_attributes[attr] = old if field_changed?(attr, old, value)
63
+ @changed_attributes[attr] = old if _field_changed?(attr, old, value)
64
64
  end
65
65
 
66
66
  # Carry on.
@@ -77,7 +77,7 @@ module ActiveRecord
77
77
  end
78
78
  end
79
79
 
80
- def field_changed?(attr, old, value)
80
+ def _field_changed?(attr, old, value)
81
81
  if column = column_for_attribute(attr)
82
82
  if column.number? && (changes_from_nil_to_empty_string?(column, old, value) ||
83
83
  changes_from_zero_to_string?(old, value))
@@ -41,11 +41,14 @@ module ActiveRecord
41
41
  unless time.acts_like?(:time)
42
42
  time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
43
43
  end
44
- time = time.in_time_zone rescue nil if time
45
- changed = read_attribute(:#{attr_name}) != time
46
- write_attribute(:#{attr_name}, original_time)
47
- #{attr_name}_will_change! if changed
48
- @attributes_cache["#{attr_name}"] = time
44
+ zoned_time = time && time.in_time_zone rescue nil
45
+ rounded_time = round_usec(zoned_time)
46
+ rounded_value = round_usec(read_attribute("#{attr_name}"))
47
+ if (rounded_value != rounded_time) || (!rounded_value && original_time)
48
+ write_attribute("#{attr_name}", original_time)
49
+ #{attr_name}_will_change!
50
+ @attributes_cache["#{attr_name}"] = zoned_time
51
+ end
49
52
  end
50
53
  EOV
51
54
  generated_attribute_methods.module_eval(method_body, __FILE__, line)
@@ -59,6 +62,12 @@ module ActiveRecord
59
62
  time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && column.type.in?([:datetime, :timestamp])
60
63
  end
61
64
  end
65
+
66
+ private
67
+ def round_usec(value)
68
+ return unless value
69
+ value.change(:usec => 0)
70
+ end
62
71
  end
63
72
  end
64
73
  end
@@ -450,12 +450,12 @@ module ActiveRecord #:nodoc:
450
450
  private
451
451
 
452
452
  def relation #:nodoc:
453
- @relation ||= Relation.new(self, arel_table)
453
+ relation = Relation.new(self, arel_table)
454
454
 
455
455
  if finder_needs_type_condition?
456
- @relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
456
+ relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
457
457
  else
458
- @relation
458
+ relation
459
459
  end
460
460
  end
461
461
  end
@@ -489,7 +489,6 @@ module ActiveRecord #:nodoc:
489
489
  @marked_for_destruction = false
490
490
  @previously_changed = {}
491
491
  @changed_attributes = {}
492
- @relation = nil
493
492
 
494
493
  ensure_proper_type
495
494
 
@@ -544,7 +543,7 @@ module ActiveRecord #:nodoc:
544
543
 
545
544
  @changed_attributes = {}
546
545
  self.class.column_defaults.each do |attr, orig_value|
547
- @changed_attributes[attr] = orig_value if field_changed?(attr, orig_value, @attributes[attr])
546
+ @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
548
547
  end
549
548
 
550
549
  @aggregation_cache = {}
@@ -54,8 +54,11 @@ module ActiveRecord
54
54
  # your database connection configuration:
55
55
  #
56
56
  # * +pool+: number indicating size of connection pool (default 5)
57
- # * +wait_timeout+: number of seconds to block and wait for a connection
58
- # before giving up and raising a timeout error (default 5 seconds).
57
+ # * +checkout _timeout+: number of seconds to block and wait for a
58
+ # connection before giving up and raising a timeout error
59
+ # (default 5 seconds). ('wait_timeout' supported for backwards
60
+ # compatibility, but conflicts with key used for different purpose
61
+ # by mysql2 adapter).
59
62
  class ConnectionPool
60
63
  include MonitorMixin
61
64
 
@@ -77,7 +80,10 @@ module ActiveRecord
77
80
  @reserved_connections = {}
78
81
 
79
82
  @queue = new_cond
80
- @timeout = spec.config[:wait_timeout] || 5
83
+ # 'wait_timeout', the backward-compatible key, conflicts with spec key
84
+ # used by mysql2 for something entirely different, checkout_timeout
85
+ # preferred to avoid conflict and allow independent values.
86
+ @timeout = spec.config[:checkout_timeout] || spec.config[:wait_timeout] || 5
81
87
 
82
88
  # default max pool size to 5
83
89
  @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
@@ -110,7 +116,7 @@ module ActiveRecord
110
116
  # #release_connection releases the connection-thread association
111
117
  # and returns the connection to the pool.
112
118
  def release_connection(with_id = current_connection_id)
113
- conn = @reserved_connections.delete(with_id)
119
+ conn = synchronize { @reserved_connections.delete(with_id) }
114
120
  checkin conn if conn
115
121
  end
116
122
 
@@ -68,6 +68,7 @@ module ActiveRecord
68
68
  :database => config.path.sub(%r{^/},""),
69
69
  :host => config.host }
70
70
  spec.reject!{ |_,value| !value }
71
+ spec.map { |key,value| spec[key] = URI.unescape(value) if value.is_a?(String) }
71
72
  if config.query
72
73
  options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
73
74
  spec.merge!(options)
@@ -391,7 +391,7 @@ module ActiveRecord
391
391
  # t.remove(:qualification)
392
392
  # t.remove(:qualification, :experience)
393
393
  def remove(*column_names)
394
- @base.remove_column(@table_name, column_names)
394
+ @base.remove_column(@table_name, *column_names)
395
395
  end
396
396
 
397
397
  # Removes the given index from the table.