Skip to content

Commit 144e869

Browse files
committed
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).
1 parent f9c63ad commit 144e869

File tree

9 files changed

+75
-15
lines changed

9 files changed

+75
-15
lines changed

activerecord/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
## Rails 4.0.0 (unreleased) ##
22

3+
* Support for partial inserts.
4+
5+
When inserting new records, only the fields which have been changed
6+
from the defaults will actually be included in the INSERT statement.
7+
The other fields will be populated by the database.
8+
9+
This is more efficient, and also means that it will be safe to
10+
remove database columns without getting subsequent errors in running
11+
app processes (so long as the code in those processes doesn't
12+
contain any references to the removed column).
13+
14+
*Jon Leighton*
15+
316
* Added `#update_columns` method which updates the attributes from
417
the passed-in hash without calling save, hence skipping validations and
518
callbacks. `ActiveRecordError` will be raised when called on new objects

activerecord/lib/active_record/attribute_methods.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,8 @@ def clone_attribute_value(reader_method, attribute_name)
207207
value
208208
end
209209

210-
def arel_attributes_with_values_for_create(pk_attribute_allowed)
211-
arel_attributes_with_values(attributes_for_create(pk_attribute_allowed))
210+
def arel_attributes_with_values_for_create(attribute_names)
211+
arel_attributes_with_values(attributes_for_create(attribute_names))
212212
end
213213

214214
def arel_attributes_with_values_for_update(attribute_names)
@@ -242,9 +242,9 @@ def attributes_for_update(attribute_names)
242242

243243
# Filters out the primary keys, from the attribute names, when the primary
244244
# key is to be generated (e.g. the id attribute has no value).
245-
def attributes_for_create(pk_attribute_allowed)
246-
@attributes.keys.select do |name|
247-
column_for_attribute(name) && (pk_attribute_allowed || !pk_attribute?(name))
245+
def attributes_for_create(attribute_names)
246+
attribute_names.select do |name|
247+
column_for_attribute(name) && !(pk_attribute?(name) && id.nil?)
248248
end
249249
end
250250

activerecord/lib/active_record/attribute_methods/dirty.rb

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,29 @@ def write_attribute(attr, value)
6464
end
6565

6666
def update(*)
67+
partial_updates? ? super(keys_for_partial_update) : super
68+
end
69+
70+
def create(*)
6771
if partial_updates?
68-
# Serialized attributes should always be written in case they've been
69-
# changed in place.
70-
super(changed | (attributes.keys & self.class.serialized_attributes.keys))
72+
keys = keys_for_partial_update
73+
74+
# This is an extremely bloody annoying necessity to work around mysql being crap.
75+
# See test_mysql_text_not_null_defaults
76+
keys.concat self.class.columns.select(&:explicit_default?).map(&:name)
77+
78+
super keys
7179
else
7280
super
7381
end
7482
end
7583

84+
# Serialized attributes should always be written in case they've been
85+
# changed in place.
86+
def keys_for_partial_update
87+
changed | (attributes.keys & self.class.serialized_attributes.keys)
88+
end
89+
7690
def _field_changed?(attr, old, value)
7791
if column = column_for_attribute(attr)
7892
if column.number? && (changes_from_nil_to_empty_string?(column, old, value) ||

activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def insert_fixture(fixture, table_name)
299299
end
300300

301301
def empty_insert_statement_value
302-
"VALUES(DEFAULT)"
302+
"DEFAULT VALUES"
303303
end
304304

305305
def case_sensitive_equality_operator

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ def has_default?
3030
super
3131
end
3232

33+
def explicit_default?
34+
!null && (sql_type =~ /blob/i || type == :text)
35+
end
36+
3337
# Must return the relevant concrete adapter
3438
def adapter
3539
raise NotImplementedError
@@ -320,6 +324,10 @@ def join_to_update(update, select) #:nodoc:
320324
end
321325
end
322326

327+
def empty_insert_statement_value
328+
"VALUES ()"
329+
end
330+
323331
# SCHEMA STATEMENTS ========================================
324332

325333
def structure_dump #:nodoc:

activerecord/lib/active_record/connection_adapters/column.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ def has_default?
5353
!default.nil?
5454
end
5555

56+
def explicit_default?
57+
false
58+
end
59+
5660
# Returns the Ruby class that corresponds to the abstract data type.
5761
def klass
5862
case type

activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,10 +490,6 @@ def rename_column(table_name, column_name, new_column_name) #:nodoc:
490490
alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
491491
end
492492

493-
def empty_insert_statement_value
494-
"VALUES(NULL)"
495-
end
496-
497493
protected
498494
def select(sql, name = nil, binds = []) #:nodoc:
499495
exec_query(sql, name, binds)

activerecord/lib/active_record/persistence.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,8 @@ def update(attribute_names = @attributes.keys)
385385

386386
# Creates a record with values matching those of the instance attributes
387387
# and returns its id.
388-
def create
389-
attributes_values = arel_attributes_with_values_for_create(!id.nil?)
388+
def create(attribute_names = @attributes.keys)
389+
attributes_values = arel_attributes_with_values_for_create(attribute_names)
390390

391391
new_id = self.class.unscoped.insert attributes_values
392392
self.id ||= new_id if self.class.primary_key

activerecord/test/cases/dirty_test.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require 'models/pirate' # For timestamps
44
require 'models/parrot'
55
require 'models/person' # For optimistic locking
6+
require 'models/aircraft'
67

78
class Pirate # Just reopening it, not defining it
89
attr_accessor :detected_changes_in_after_update # Boolean for if changes are detected
@@ -550,6 +551,30 @@ def test_setting_time_attributes_with_time_zone_field_to_same_time_should_not_be
550551
end
551552
end
552553

554+
test "partial insert" do
555+
with_partial_updates Person do
556+
jon = nil
557+
assert_sql(/first_name/) do
558+
jon = Person.create! first_name: 'Jon'
559+
end
560+
561+
assert ActiveRecord::SQLCounter.log_all.none? { |sql| sql =~ /followers_count/ }
562+
563+
jon.reload
564+
assert_equal 'Jon', jon.first_name
565+
assert_equal 0, jon.followers_count
566+
assert_not_nil jon.id
567+
end
568+
end
569+
570+
test "partial insert with empty values" do
571+
with_partial_updates Aircraft do
572+
a = Aircraft.create!
573+
a.reload
574+
assert_not_nil a.id
575+
end
576+
end
577+
553578
private
554579
def with_partial_updates(klass, on = true)
555580
old = klass.partial_updates?

0 commit comments

Comments
 (0)