diff options
9 files changed, 75 insertions, 15 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d52d6a7fde..b6ad6f82d8 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,5 +1,18 @@ ## Rails 4.0.0 (unreleased) ## +* Support for partial inserts. + + When inserting new records, only the fields which have been changed + from the defaults will actually be included in the INSERT statement. + The other fields will be populated by the database. + + This is more efficient, and also means that it will be safe to + remove database columns without getting subsequent errors in running + app processes (so long as the code in those processes doesn't + contain any references to the removed column). + + *Jon Leighton* + * Added `#update_columns` method which updates the attributes from the passed-in hash without calling save, hence skipping validations and callbacks. `ActiveRecordError` will be raised when called on new objects diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ced15bc330..0aff2562b8 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -207,8 +207,8 @@ module ActiveRecord value end - def arel_attributes_with_values_for_create(pk_attribute_allowed) - arel_attributes_with_values(attributes_for_create(pk_attribute_allowed)) + def arel_attributes_with_values_for_create(attribute_names) + arel_attributes_with_values(attributes_for_create(attribute_names)) end def arel_attributes_with_values_for_update(attribute_names) @@ -242,9 +242,9 @@ module ActiveRecord # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). - def attributes_for_create(pk_attribute_allowed) - @attributes.keys.select do |name| - column_for_attribute(name) && (pk_attribute_allowed || !pk_attribute?(name)) + def attributes_for_create(attribute_names) + attribute_names.select do |name| + column_for_attribute(name) && !(pk_attribute?(name) && id.nil?) end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 60e5b0e2bb..6204e4172d 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -64,15 +64,29 @@ module ActiveRecord end def update(*) + partial_updates? ? super(keys_for_partial_update) : super + end + + def create(*) if partial_updates? - # Serialized attributes should always be written in case they've been - # changed in place. - super(changed | (attributes.keys & self.class.serialized_attributes.keys)) + keys = keys_for_partial_update + + # This is an extremely bloody annoying necessity to work around mysql being crap. + # See test_mysql_text_not_null_defaults + keys.concat self.class.columns.select(&:explicit_default?).map(&:name) + + super keys else super end end + # Serialized attributes should always be written in case they've been + # changed in place. + def keys_for_partial_update + changed | (attributes.keys & self.class.serialized_attributes.keys) + end + def _field_changed?(attr, old, value) if column = column_for_attribute(attr) if column.number? && (changes_from_nil_to_empty_string?(column, old, value) || diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 793f58d4d3..0d7046a705 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -299,7 +299,7 @@ module ActiveRecord end def empty_insert_statement_value - "VALUES(DEFAULT)" + "DEFAULT VALUES" end def case_sensitive_equality_operator diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 1783b036a2..8c83c4f5db 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -30,6 +30,10 @@ module ActiveRecord super end + def explicit_default? + !null && (sql_type =~ /blob/i || type == :text) + end + # Must return the relevant concrete adapter def adapter raise NotImplementedError @@ -320,6 +324,10 @@ module ActiveRecord end end + def empty_insert_statement_value + "VALUES ()" + end + # SCHEMA STATEMENTS ======================================== def structure_dump #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 816b5e17c1..2028abf6f0 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -53,6 +53,10 @@ module ActiveRecord !default.nil? end + def explicit_default? + false + end + # Returns the Ruby class that corresponds to the abstract data type. def klass case type diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 4a48812807..4d5cb72c67 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -490,10 +490,6 @@ module ActiveRecord alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s}) end - def empty_insert_statement_value - "VALUES(NULL)" - end - protected def select(sql, name = nil, binds = []) #:nodoc: exec_query(sql, name, binds) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 2eaad1d469..f81eb5f5d1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -385,8 +385,8 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. - def create - attributes_values = arel_attributes_with_values_for_create(!id.nil?) + def create(attribute_names = @attributes.keys) + attributes_values = arel_attributes_with_values_for_create(attribute_names) new_id = self.class.unscoped.insert attributes_values self.id ||= new_id if self.class.primary_key diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 9a2a5a4e3c..75a52544d3 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -3,6 +3,7 @@ require 'models/topic' # For booleans require 'models/pirate' # For timestamps require 'models/parrot' require 'models/person' # For optimistic locking +require 'models/aircraft' class Pirate # Just reopening it, not defining it attr_accessor :detected_changes_in_after_update # Boolean for if changes are detected @@ -550,6 +551,30 @@ class DirtyTest < ActiveRecord::TestCase end end + test "partial insert" do + with_partial_updates Person do + jon = nil + assert_sql(/first_name/) do + jon = Person.create! first_name: 'Jon' + end + + assert ActiveRecord::SQLCounter.log_all.none? { |sql| sql =~ /followers_count/ } + + jon.reload + assert_equal 'Jon', jon.first_name + assert_equal 0, jon.followers_count + assert_not_nil jon.id + end + end + + test "partial insert with empty values" do + with_partial_updates Aircraft do + a = Aircraft.create! + a.reload + assert_not_nil a.id + end + end + private def with_partial_updates(klass, on = true) old = klass.partial_updates? |