aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--activerecord/CHANGELOG.md13
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb10
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb4
-rw-r--r--activerecord/lib/active_record/persistence.rb4
-rw-r--r--activerecord/test/cases/dirty_test.rb25
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?