diff options
Diffstat (limited to 'activerecord')
17 files changed, 180 insertions, 42 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 3bd85976a0..2e4f653ea6 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,14 @@ +* Support for Postgres `citext` data type enabling case-insensitive where + values without needing to wrap in UPPER/LOWER sql functions. + + *Troy Kruthoff*, *Lachlan Sylvester* + +* Only save has_one associations if record has changes. + Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one + object did not get saved to the db. + + *Alan Kennedy* + * Allow strings to specify the `#order` value. Example: diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 53f7591226..f725356cd9 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -530,8 +530,8 @@ module ActiveRecord # end # # @firm = Firm.first - # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm - # @firm.invoices # selects all invoices by going through the Client join model + # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm + # @firm.invoices # selects all invoices by going through the Client join model # # Similarly you can go through a +has_one+ association on the join model: # diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 2a8530af62..70e97432e4 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -23,7 +23,7 @@ module ActiveRecord reset_association owners, through_reflection.name - middle_records = through_records.map { |(_,rec)| rec }.flatten + middle_records = through_records.flat_map { |(_,rec)| rec } preloaders = preloader.preload(middle_records, source_reflection.name, diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 57ceec2fc5..ea48a13ea8 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -105,9 +105,9 @@ module ActiveRecord super else # If B < A and A defines its own attribute method, then we don't want to overwrite that. - defined = method_defined_within?(method_name, superclass) && - superclass.instance_method(method_name).owner != superclass.generated_attribute_methods - defined || super + defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) + base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name) + defined && !base_defined || super end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index e9622ca0c1..213b72b933 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -377,15 +377,16 @@ module ActiveRecord def save_has_one_association(reflection) association = association_instance_get(reflection.name) record = association && association.load_target + if record && !record.destroyed? autosave = reflection.options[:autosave] if autosave && record.marked_for_destruction? record.destroy - else + elsif autosave != false key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id - if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key)) + if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key end 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 23edc8b955..baac9522b6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -459,7 +459,7 @@ module ActiveRecord end def bulk_change_table(table_name, operations) #:nodoc: - sqls = operations.map do |command, args| + sqls = operations.flat_map do |command, args| table, arguments = args.shift, args method = :"#{command}_sql" @@ -468,7 +468,7 @@ module ActiveRecord else raise "Unknown method called : #{method}(#{arguments.inspect})" end - end.flatten.join(", ") + end.join(", ") execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 697915f3e9..5d32aaed50 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -375,6 +375,7 @@ This is not reliable and will be removed in the future. register_type 'circle', OID::Text.new register_type 'hstore', OID::Hstore.new register_type 'json', OID::Json.new + register_type 'citext', OID::Text.new register_type 'ltree', OID::Text.new register_type 'cidr', OID::Cidr.new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index a56ef91d07..e5f7913c70 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -215,6 +215,8 @@ module ActiveRecord # Character types when /^(?:character varying|bpchar)(?:\(\d+\))?$/ :string + when /^citext(?:\(\d+\))?$/ + :citext # Binary data types when 'bytea' :binary @@ -353,6 +355,10 @@ module ActiveRecord def json(name, options = {}) column(name, 'json', options) end + + def citext(name, options = {}) + column(name, 'citext', options) + end end class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition @@ -393,6 +399,10 @@ module ActiveRecord column name, type, options end + def citext(name, options = {}) + column(name, 'citext', options) + end + def column(name, type = nil, options = {}) super column = self[name] @@ -441,7 +451,8 @@ module ActiveRecord macaddr: { name: "macaddr" }, uuid: { name: "uuid" }, json: { name: "json" }, - ltree: { name: "ltree" } + ltree: { name: "ltree" }, + citext: { name: "citext" } } include Quoting diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 01fec31544..73ab3b39aa 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -11,7 +11,7 @@ module ActiveRecord end module ClassMethods - # Returns a scope for the model without the +default_scope+. + # Returns a scope for the model without the previously set scopes. # # class Post < ActiveRecord::Base # def self.default_scope @@ -19,11 +19,12 @@ module ActiveRecord # end # end # - # Post.all # Fires "SELECT * FROM posts WHERE published = true" - # Post.unscoped.all # Fires "SELECT * FROM posts" + # Post.all # Fires "SELECT * FROM posts WHERE published = true" + # Post.unscoped.all # Fires "SELECT * FROM posts" + # Post.where(published: false).unscoped.all # Fires "SELECT * FROM posts" # # This method also accepts a block. All queries inside the block will - # not use the +default_scope+: + # not use the previously set scopes. # # Post.unscoped { # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 49b0f73219..82c2b9439b 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -13,7 +13,7 @@ module ActiveRecord def validate_each(record, attribute, value) finder_class = find_finder_class_for(record) table = finder_class.arel_table - value = map_enum_attribute(finder_class,attribute,value) + value = map_enum_attribute(finder_class, attribute, value) value = deserialize_attribute(record, attribute, value) relation = build_relation(finder_class, table, attribute, value) @@ -93,7 +93,7 @@ module ActiveRecord value end - def map_enum_attribute(klass,attribute,value) + def map_enum_attribute(klass, attribute, value) mapping = klass.enum_mapping_for(attribute.to_s) value = mapping[value] if value && mapping value diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb new file mode 100644 index 0000000000..ebb9ff98b1 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 + +require 'cases/helper' +require 'active_record/base' +require 'active_record/connection_adapters/postgresql_adapter' + +class PostgresqlCitextTest < ActiveRecord::TestCase + class Citext < ActiveRecord::Base + self.table_name = 'citexts' + end + + def setup + @connection = ActiveRecord::Base.connection + + unless @connection.extension_enabled?('citext') + @connection.enable_extension 'citext' + @connection.commit_db_transaction + end + + @connection.reconnect! + + @connection.create_table('citexts') do |t| + t.citext 'cival' + end + @column = Citext.columns_hash['cival'] + end + + def teardown + @connection.execute 'DROP TABLE IF EXISTS citexts;' + @connection.execute 'DROP EXTENSION IF EXISTS citext CASCADE;' + end + + def test_citext_enabled + assert @connection.extension_enabled?('citext') + end + + def test_column_type + assert_equal :citext, @column.type + end + + def test_column_sql_type + assert_equal 'citext', @column.sql_type + end + + def test_change_table_supports_json + @connection.transaction do + @connection.change_table('citexts') do |t| + t.citext 'username' + end + Citext.reset_column_information + column = Citext.columns.find { |c| c.name == 'username' } + assert_equal :citext, column.type + + raise ActiveRecord::Rollback # reset the schema change + end + ensure + Citext.reset_column_information + end + + def test_write + x = Citext.new(cival: 'Some CI Text') + x.save! + citext = Citext.first + assert_equal "Some CI Text", citext.cival + + citext.cival = "Some NEW CI Text" + citext.save! + + assert_equal "Some NEW CI Text", citext.reload.cival + end + + def test_select_case_insensitive + @connection.execute "insert into citexts (cival) values('Cased Text')" + x = Citext.where(cival: 'cased text').first + assert_equal 'Cased Text', x.cival + end +end diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index e555c52281..cf71bc1597 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -159,7 +159,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase activerecord.reload assert activerecord.developers_with_callbacks.size == 2 end - log_array = activerecord.developers_with_callbacks.collect {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.flatten.sort + log_array = activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort assert activerecord.developers_with_callbacks.clear assert_equal log_array, activerecord.developers_log.sort end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 1a1e442df0..173081cb28 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -746,6 +746,19 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert "unknown attribute: hello", error.message end + def test_methods_override_in_multi_level_subclass + klass = Class.new(Developer) do + def name + "dev:#{read_attribute(:name)}" + end + end + + 2.times { klass = Class.new klass } + dev = klass.new(name: 'arthurnn') + dev.save! + assert_equal 'dev:arthurnn', dev.reload.name + end + def test_global_methods_are_overwritten klass = Class.new(ActiveRecord::Base) do self.table_name = 'computers' diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index d2f97df0fc..09892d50ba 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -64,10 +64,6 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase private - def base - ActiveRecord::Base - end - def assert_no_difference_when_adding_callbacks_twice_for(model, association_name) reflection = model.reflect_on_association(association_name) assert_no_difference "callbacks_for_model(#{model.name}).length" do @@ -76,9 +72,9 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase end def callbacks_for_model(model) - model.instance_variables.grep(/_callbacks$/).map do |ivar| + model.instance_variables.grep(/_callbacks$/).flat_map do |ivar| model.instance_variable_get(ivar) - end.flatten + end end end @@ -622,15 +618,15 @@ end class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false - def setup - super + setup do @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?") @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning') end - def teardown + teardown do # We are running without transactional fixtures and need to cleanup. Bird.delete_all + Parrot.delete_all @ship.delete @pirate.delete end @@ -687,10 +683,23 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end + @ship.pirate.catchphrase = "Changed Catchphrase" + assert_raise(RuntimeError) { assert !@pirate.save } assert_not_nil @pirate.reload.ship end + def test_should_save_changed_has_one_changed_object_if_child_is_saved + @pirate.ship.name = "NewName" + assert @pirate.save + assert_equal "NewName", @pirate.ship.reload.name + end + + def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved + @pirate.ship.expects(:save).never + assert @pirate.save + end + # belongs_to def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal assert !@ship.pirate.marked_for_destruction? diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 78c4e02434..98b1dafdf0 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -62,22 +62,20 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_fails_when_parameter_has_invalid_type - begin + if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter) + assert_raises ActiveRecord::StatementInvalid do + Topic.exists?(("9"*53).to_i) # number that's bigger than int + end + else assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int - flunk if defined? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter and Topic.connection.is_a? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter # PostgreSQL does raise here - rescue ActiveRecord::StatementInvalid - # PostgreSQL complains that it can't coerce a numeric that's bigger than int into int - rescue Exception - flunk end - begin + if current_adapter?(:PostgreSQLAdapter) + assert_raises ActiveRecord::StatementInvalid do + Topic.exists?("foo") + end + else assert_equal false, Topic.exists?("foo") - flunk if defined? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter and Topic.connection.is_a? ActiveRecord::ConnectionAdapters::PostgreSQLAdapter # PostgreSQL does raise here - rescue ActiveRecord::StatementInvalid - # PostgreSQL complains about string comparison with integer field - rescue Exception - flunk end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index c085663efb..575eb34a9c 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -177,7 +177,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_columns_in_right_order index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip - if current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) || current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition else assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition @@ -188,7 +188,7 @@ class SchemaDumperTest < ActiveRecord::TestCase index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip if current_adapter?(:PostgreSQLAdapter) assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition - elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) + elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition @@ -319,6 +319,13 @@ class SchemaDumperTest < ActiveRecord::TestCase end end + def test_schema_dump_includes_citext_shorthand_definition + output = standard_dump + if %r{create_table "postgresql_citext"} =~ output + assert_match %r[t.citext "text_citext"], output + end + end + def test_schema_dump_includes_ltrees_shorthand_definition output = standard_dump if %r{create_table "postgresql_ltrees"} =~ output diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index a86a188bcf..4fcbf4dbd2 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,7 +1,7 @@ ActiveRecord::Schema.define do %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees - postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name| + postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -99,6 +99,15 @@ _SQL _SQL end + if 't' == select_value("select 'citext'=ANY(select typname from pg_type)") + execute <<_SQL + CREATE TABLE postgresql_citext ( + id SERIAL PRIMARY KEY, + text_citext citext default ''::citext + ); +_SQL + end + if 't' == select_value("select 'json'=ANY(select typname from pg_type)") execute <<_SQL CREATE TABLE postgresql_json_data_type ( |