diff options
19 files changed, 348 insertions, 428 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 0d1d0c36ce..8a0c55d5d4 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -18,27 +18,27 @@ Style/BracesAroundHashParameters: Enabled: true # Align `when` with `case`. -Style/CaseIndentation: +Layout/CaseIndentation: Enabled: true # Align comments with method definitions. -Style/CommentIndentation: +Layout/CommentIndentation: Enabled: true # No extra empty lines. -Style/EmptyLines: +Layout/EmptyLines: Enabled: true # In a regular class definition, no empty lines around the body. -Style/EmptyLinesAroundClassBody: +Layout/EmptyLinesAroundClassBody: Enabled: true # In a regular method definition, no empty lines around the body. -Style/EmptyLinesAroundMethodBody: +Layout/EmptyLinesAroundMethodBody: Enabled: true # In a regular module definition, no empty lines around the body. -Style/EmptyLinesAroundModuleBody: +Layout/EmptyLinesAroundModuleBody: Enabled: true # Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. @@ -47,30 +47,30 @@ Style/HashSyntax: # Method definitions after `private` or `protected` isolated calls need one # extra level of indentation. -Style/IndentationConsistency: +Layout/IndentationConsistency: Enabled: true EnforcedStyle: rails # Two spaces, no tabs (for indentation). -Style/IndentationWidth: +Layout/IndentationWidth: Enabled: true -Style/SpaceAfterColon: +Layout/SpaceAfterColon: Enabled: true -Style/SpaceAfterComma: +Layout/SpaceAfterComma: Enabled: true -Style/SpaceAroundEqualsInParameterDefault: +Layout/SpaceAroundEqualsInParameterDefault: Enabled: true -Style/SpaceAroundKeyword: +Layout/SpaceAroundKeyword: Enabled: true -Style/SpaceAroundOperators: +Layout/SpaceAroundOperators: Enabled: true -Style/SpaceBeforeFirstArg: +Layout/SpaceBeforeFirstArg: Enabled: true # Defining a method with parameters needs parentheses. @@ -78,18 +78,18 @@ Style/MethodDefParentheses: Enabled: true # Use `foo {}` not `foo{}`. -Style/SpaceBeforeBlockBraces: +Layout/SpaceBeforeBlockBraces: Enabled: true # Use `foo { bar }` not `foo {bar}`. -Style/SpaceInsideBlockBraces: +Layout/SpaceInsideBlockBraces: Enabled: true # Use `{ a: 1 }` not `{a:1}`. -Style/SpaceInsideHashLiteralBraces: +Layout/SpaceInsideHashLiteralBraces: Enabled: true -Style/SpaceInsideParens: +Layout/SpaceInsideParens: Enabled: true # Check quotes usage according to lint rule below. @@ -98,15 +98,15 @@ Style/StringLiterals: EnforcedStyle: double_quotes # Detect hard tabs, no hard tabs. -Style/Tab: +Layout/Tab: Enabled: true # Blank lines should not have any spaces. -Style/TrailingBlankLines: +Layout/TrailingBlankLines: Enabled: true # No trailing whitespace. -Style/TrailingWhitespace: +Layout/TrailingWhitespace: Enabled: true # Use quotes for string literals when they are enough. diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 907dd894cd..d17bbf80ca 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,9 @@ +* Loading model schema from database is now thread-safe. + + Fixes #28589. + + *Vikrant Chaudhary*, *David Abdemoulaie* + * Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via the new versioned entries in `ActiveSupport::Cache`. This also means that `ActiveRecord::Base#cache_key` will now return a stable key that does not include a timestamp any more. diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index aa9087fae3..013562708c 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,3 +1,5 @@ +require "monitor" + module ActiveRecord module ModelSchema extend ActiveSupport::Concern @@ -152,6 +154,8 @@ module ActiveRecord self.inheritance_column = "type" delegate :type_for_attribute, to: :class + + initialize_load_schema_monitor end # Derives the join table name for +first_table+ and +second_table+. The @@ -435,15 +439,27 @@ module ActiveRecord initialize_find_by_cache end + protected + + def initialize_load_schema_monitor + @load_schema_monitor = Monitor.new + end + private + def inherited(child_class) + super + child_class.initialize_load_schema_monitor + end + def schema_loaded? - defined?(@columns_hash) && @columns_hash + defined?(@schema_loaded) && @schema_loaded end def load_schema - unless schema_loaded? - load_schema! + return if schema_loaded? + @load_schema_monitor.synchronize do + load_schema! unless defined?(@columns_hash) && @columns_hash end end @@ -457,6 +473,8 @@ module ActiveRecord user_provided_default: false ) end + + @schema_loaded = true end def reload_schema_from_cache @@ -471,6 +489,7 @@ module ActiveRecord @attributes_builder = nil @columns = nil @columns_hash = nil + @schema_loaded = false @attribute_names = nil @yaml_encoder = nil direct_descendants.each do |descendant| diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 1a9e0a4a40..65fdbc2fe4 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -199,7 +199,7 @@ module ActiveRecord def klass_join_scope(table, predicate_builder) # :nodoc: if klass.current_scope klass.current_scope.clone.tap { |scope| - scope.joins_values = [] + scope.joins_values = scope.left_outer_joins_values = [].freeze } else relation = ActiveRecord::Relation.create( diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index 6954006003..d311ffb703 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -1,17 +1,11 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" if ActiveRecord::Base.connection.supports_json? class Mysql2JSONTest < ActiveRecord::Mysql2TestCase - include SchemaDumpingHelper + include JSONSharedTestCases self.use_transactional_tests = false - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end - def setup @connection = ActiveRecord::Base.connection begin @@ -27,169 +21,9 @@ if ActiveRecord::Base.connection.supports_json? JsonDataType.reset_column_information end - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal :json, column.type - assert_equal "json", column.sql_type - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? - end - - def test_change_table_supports_json - @connection.change_table("json_data_type") do |t| - t.json "users" + private + def column_type + :json end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal :json, column.type - end - - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.json\s+"settings"/, output) - end - - def test_cast_value_on_write - x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } - assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) - x.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) - end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - data = "{\"a_key\":\"a_value\"}" - hash = type.deserialize(data) - assert_equal({ "a_key" => "a_value" }, hash) - assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) - - assert_equal({}, type.deserialize("{}")) - assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) - assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - assert_equal({ "k" => "v" }, x.payload) - end - - def test_select_multikey - @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| - x = JsonDataType.first - assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) - end - - def test_null_json - @connection.execute "insert into json_data_type (payload) VALUES(null)" - x = JsonDataType.first - assert_nil(x.payload) - end - - def test_select_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - assert_equal(["v0", { "k1" => "v1" }], x.payload) - end - - def test_select_nil_json_after_create - json = JsonDataType.create(payload: nil) - x = JsonDataType.where(payload: nil).first - assert_equal(json, x) - end - - def test_select_nil_json_after_update - json = JsonDataType.create(payload: "foo") - x = JsonDataType.where(payload: nil).first - assert_nil(x) - - json.update_attributes payload: nil - x = JsonDataType.where(payload: nil).first - assert_equal(json.reload, x) - end - - def test_rewrite_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - x.payload = ["v1", { "k2" => "v2" }, "v3"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload - end end end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index d4e627001c..4eeb563781 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,14 +1,8 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" module PostgresqlJSONSharedTestCases - include SchemaDumpingHelper - - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end + include JSONSharedTestCases def setup @connection = ActiveRecord::Base.connection @@ -28,16 +22,6 @@ module PostgresqlJSONSharedTestCases JsonDataType.reset_column_information end - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type - assert_not column.array? - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? - end - def test_default @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] } JsonDataType.reset_column_information @@ -48,34 +32,6 @@ module PostgresqlJSONSharedTestCases JsonDataType.reset_column_information end - def test_change_table_supports_json - @connection.transaction do - @connection.change_table("json_data_type") do |t| - t.public_send column_type, "users", default: "{}" # t.json 'users', default: '{}' - end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal column_type, column.type - - raise ActiveRecord::Rollback # reset the schema change - end - ensure - JsonDataType.reset_column_information - end - - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output) - end - - def test_cast_value_on_write - x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } - assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) - x.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) - end - def test_deserialize_with_array x = JsonDataType.new(objects: ["foo" => "bar"]) assert_equal ["foo" => "bar"], x.objects @@ -84,140 +40,6 @@ module PostgresqlJSONSharedTestCases x.reload assert_equal ["foo" => "bar"], x.objects end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - data = "{\"a_key\":\"a_value\"}" - hash = type.deserialize(data) - assert_equal({ "a_key" => "a_value" }, hash) - assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) - - assert_equal({}, type.deserialize("{}")) - assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) - assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - assert_equal({ "k" => "v" }, x.payload) - end - - def test_select_multikey - @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| - x = JsonDataType.first - assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) - end - - def test_null_json - @connection.execute "insert into json_data_type (payload) VALUES(null)" - x = JsonDataType.first - assert_nil(x.payload) - end - - def test_select_nil_json_after_create - json = JsonDataType.create(payload: nil) - x = JsonDataType.where(payload: nil).first - assert_equal(json, x) - end - - def test_select_nil_json_after_update - json = JsonDataType.create(payload: "foo") - x = JsonDataType.where(payload: nil).first - assert_nil(x) - - json.update_attributes payload: nil - x = JsonDataType.where(payload: nil).first - assert_equal(json.reload, x) - end - - def test_select_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - assert_equal(["v0", { "k1" => "v1" }], x.payload) - end - - def test_rewrite_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - x.payload = ["v1", { "k2" => "v2" }, "v3"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload - end end class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb new file mode 100644 index 0000000000..d190b027bf --- /dev/null +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -0,0 +1,177 @@ +require "support/schema_dumping_helper" + +module JSONSharedTestCases + include SchemaDumpingHelper + + class JsonDataType < ActiveRecord::Base + self.table_name = "json_data_type" + + store_accessor :settings, :resolution + end + + def test_column + column = JsonDataType.columns_hash["payload"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + + type = JsonDataType.type_for_attribute("payload") + assert_not type.binary? + end + + def test_change_table_supports_json + @connection.change_table("json_data_type") do |t| + t.public_send column_type, "users" + end + JsonDataType.reset_column_information + column = JsonDataType.columns_hash["users"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + end + + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t\.#{column_type}\s+"settings"/, output) + end + + def test_cast_value_on_write + x = JsonDataType.new(payload: { "string" => "foo", :symbol => :bar }) + assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) + x.save! + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) + end + + def test_type_cast_json + type = JsonDataType.type_for_attribute("payload") + + data = '{"a_key":"a_value"}' + hash = type.deserialize(data) + assert_equal({ "a_key" => "a_value" }, hash) + assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) + + assert_equal({}, type.deserialize("{}")) + assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) + assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = JsonDataType.first + x.payload = { '"a\'' => "b" } + assert x.save! + end + + def test_select + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = JsonDataType.first + assert_equal({ "k" => "v" }, x.payload) + end + + def test_select_multikey + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|) + x = JsonDataType.first + assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) + end + + def test_null_json + @connection.execute("insert into json_data_type (payload) VALUES(null)") + x = JsonDataType.first + assert_nil(x.payload) + end + + def test_select_nil_json_after_create + json = JsonDataType.create!(payload: nil) + x = JsonDataType.where(payload: nil).first + assert_equal(json, x) + end + + def test_select_nil_json_after_update + json = JsonDataType.create!(payload: "foo") + x = JsonDataType.where(payload: nil).first + assert_nil(x) + + json.update_attributes(payload: nil) + x = JsonDataType.where(payload: nil).first + assert_equal(json.reload, x) + end + + def test_select_array_json_value + @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + x = JsonDataType.first + assert_equal(["v0", { "k1" => "v1" }], x.payload) + end + + def test_rewrite_array_json_value + @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + x = JsonDataType.first + x.payload = ["v1", { "k2" => "v2" }, "v3"] + assert x.save! + end + + def test_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = JsonDataType.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = JsonDataType.first + assert_equal "640×1136", x.resolution + end + + def test_duplication_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? + + json.payload = { "one" => "two" } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload["three"] = "four" + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ "one" => "two", "three" => "four" }, json.payload) + assert_not json.changed? + end + + def test_assigning_string_literal + json = JsonDataType.create!(payload: "foo") + assert_equal "foo", json.payload + end + + def test_assigning_number + json = JsonDataType.create!(payload: 1.234) + assert_equal 1.234, json.payload + end + + def test_assigning_boolean + json = JsonDataType.create!(payload: true) + assert_equal true, json.payload + end +end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 3fbff7664b..8535be8402 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -229,12 +229,19 @@ class RelationScopingTest < ActiveRecord::TestCase end end - def test_circular_joins_with_current_scope_does_not_crash + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do - Post.current_scope.first(10) + Post.first(10) end assert_equal posts, Post.joins(comments: :post).first(10) end + + def test_circular_left_joins_with_scoping_does_not_crash + posts = Post.left_joins(comments: :post).scoping do + Post.first(10) + end + assert_equal posts, Post.left_joins(comments: :post).first(10) + end end class NestedRelationScopingTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 673392b4c4..e1bdaab5cf 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -349,4 +349,32 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic.foo refute topic.changed? end + + def test_serialized_attribute_works_under_concurrent_initial_access + model = Topic.dup + + topic = model.last + topic.update group: "1" + + model.serialize :group, JSON + model.reset_column_information + + # This isn't strictly necessary for the test, but a little bit of + # knowledge of internals allows us to make failures far more likely. + model.define_singleton_method(:define_attribute) do |*args| + Thread.pass + super(*args) + end + + threads = 4.times.map do + Thread.new do + topic.reload.group + end + end + + # All the threads should retrieve the value knowing it is JSON, and + # thus decode it. If this fails, some threads will instead see the + # raw string ("1"), or raise an exception. + assert_equal [1] * threads.size, threads.map(&:value) + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 33da3d11fc..c22d974536 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -294,13 +294,6 @@ if current_adapter?(:Mysql2Adapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end - def test_structure_dump - filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - end - def test_structure_dump_with_extra_flags filename = "awesome-file.sql" expected_command = ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--noop", "test-db"] diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index baea924c7f..7751ac00df 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -781,7 +781,7 @@ config.action_mailer.smtp_settings = { enable_starttls_auto: true } ``` Note: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure. -You can change your gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts. If your Gmail account has 2-factor authentication enabled, +You can change your Gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts. If your Gmail account has 2-factor authentication enabled, then you will need to set an [app password](https://myaccount.google.com/apppasswords) and use that instead of your regular password. Alternatively, you can use another ESP to send email by replacing 'smtp.gmail.com' above with the address of your provider. diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 26d01d4ede..d1cbe506b4 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -557,6 +557,19 @@ In other words, this query can be generated by calling `where` with no argument, SELECT * FROM clients WHERE (clients.locked != 1) ``` +### OR Conditions + +`OR` condition between two relations can be build by calling `or` on the first relation +and passing the second one as an argument. + +```ruby +Client.where(locked: true).or(Client.where(orders_count: [1,3,5])) +``` + +```sql +SELECT * FROM clients WHERE (clients.locked = 1 OR clients.orders_count IN (1,3,5)) +``` + Ordering -------- diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index d8e85497fa..5c7d1f5365 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -1559,7 +1559,7 @@ The `collection.size` method returns the number of objects in the collection. The `collection.find` method finds objects within the collection. It uses the same syntax and options as `ActiveRecord::Base.find`. ```ruby -@available_books = @author.books.find(1) +@available_book = @author.books.find(1) ``` ##### `collection.where(...)` diff --git a/railties/lib/rails/commands/secrets/secrets_command.rb b/railties/lib/rails/commands/secrets/secrets_command.rb index 03a640bd65..651411d444 100644 --- a/railties/lib/rails/commands/secrets/secrets_command.rb +++ b/railties/lib/rails/commands/secrets/secrets_command.rb @@ -13,10 +13,7 @@ module Rails end def setup - require "rails/generators" - require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" - - Rails::Generators::EncryptedSecretsGenerator.start + generator.start end def edit @@ -34,7 +31,6 @@ module Rails require_application_and_environment! Rails::Secrets.read_for_editing do |tmp_path| - say "Waiting for secrets file to be saved. Abort with Ctrl-C." system("\$EDITOR #{tmp_path}") end @@ -43,7 +39,22 @@ module Rails say "Aborted changing encrypted secrets: nothing saved." rescue Rails::Secrets::MissingKeyError => error say error.message + rescue Errno::ENOENT => error + raise unless error.message =~ /secrets\.yml\.enc/ + + Rails::Secrets.read_template_for_editing do |tmp_path| + system("\$EDITOR #{tmp_path}") + generator.skip_secrets_file { setup } + end end + + private + def generator + require "rails/generators" + require "rails/generators/rails/encrypted_secrets/encrypted_secrets_generator" + + Rails::Generators::EncryptedSecretsGenerator + end end end end diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb index 8b29213610..1da2fbc1a5 100644 --- a/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb +++ b/railties/lib/rails/generators/rails/encrypted_secrets/encrypted_secrets_generator.rb @@ -36,25 +36,29 @@ module Rails end def add_encrypted_secrets_file - unless File.exist?("config/secrets.yml.enc") + unless (defined?(@@skip_secrets_file) && @@skip_secrets_file) || File.exist?("config/secrets.yml.enc") say "Adding config/secrets.yml.enc to store secrets that needs to be encrypted." say "" + say "For now the file contains this but it's been encrypted with the generated key:" + say "" + say Secrets.template, :on_green + say "" - template "config/secrets.yml.enc" do |prefill| - say "" - say "For now the file contains this but it's been encrypted with the generated key:" - say "" - say prefill, :on_green - say "" - - Secrets.encrypt(prefill) - end + Secrets.write(Secrets.template) say "You can edit encrypted secrets with `bin/rails secrets:edit`." - - say "Add this to your config/environments/production.rb:" - say "config.read_encrypted_secrets = true" + say "" end + + say "Add this to your config/environments/production.rb:" + say "config.read_encrypted_secrets = true" + end + + def self.skip_secrets_file + @@skip_secrets_file = true + yield + ensure + @@skip_secrets_file = false end private diff --git a/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc b/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc deleted file mode 100644 index 70426a66a5..0000000000 --- a/railties/lib/rails/generators/rails/encrypted_secrets/templates/config/secrets.yml.enc +++ /dev/null @@ -1,3 +0,0 @@ -# See `secrets.yml` for tips on generating suitable keys. -# production: -# external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289… diff --git a/railties/lib/rails/secrets.rb b/railties/lib/rails/secrets.rb index 8b644f212c..20c20cb9f1 100644 --- a/railties/lib/rails/secrets.rb +++ b/railties/lib/rails/secrets.rb @@ -1,5 +1,6 @@ require "yaml" require "active_support/message_encryptor" +require "active_support/core_ext/string/strip" module Rails # Greatly inspired by Ara T. Howard's magnificent sekrets gem. 😘 @@ -37,6 +38,15 @@ module Rails ENV["RAILS_MASTER_KEY"] || read_key_file || handle_missing_key end + def template + <<-end_of_template.strip_heredoc + # See `secrets.yml` for tips on generating suitable keys. + # production: + # external_api_key: 1466aac22e6a869134be3d09b9e89232fc2c2289… + + end_of_template + end + def encrypt(data) encryptor.encrypt_and_sign(data) end @@ -54,15 +64,12 @@ module Rails FileUtils.mv("#{path}.tmp", path) end - def read_for_editing - tmp_path = File.join(Dir.tmpdir, File.basename(path)) - IO.binwrite(tmp_path, read) - - yield tmp_path + def read_for_editing(&block) + writing(read, &block) + end - write(IO.binread(tmp_path)) - ensure - FileUtils.rm(tmp_path) if File.exist?(tmp_path) + def read_template_for_editing(&block) + writing(template, &block) end private @@ -92,6 +99,17 @@ module Rails end end + def writing(contents) + tmp_path = File.join(Dir.tmpdir, File.basename(path)) + File.write(tmp_path, contents) + + yield tmp_path + + write(File.read(tmp_path)) + ensure + FileUtils.rm(tmp_path) if File.exist?(tmp_path) + end + def encryptor @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: @cipher) end diff --git a/railties/test/commands/secrets_test.rb b/railties/test/commands/secrets_test.rb index 00b0343397..fb8fd2325e 100644 --- a/railties/test/commands/secrets_test.rb +++ b/railties/test/commands/secrets_test.rb @@ -18,7 +18,8 @@ class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase end test "edit secrets" do - run_setup_command + # Runs setup before first edit. + assert_match(/Adding config\/secrets\.yml\.key to store the encryption key/, run_edit_command) # Run twice to ensure encrypted secrets can be reread after first edit pass. 2.times do @@ -30,8 +31,4 @@ class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase def run_edit_command(editor: "cat") Dir.chdir(app_path) { `EDITOR="#{editor}" bin/rails secrets:edit` } end - - def run_setup_command - Dir.chdir(app_path) { `bin/rails secrets:setup` } - end end diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index ff829eb197..31a5efb1b7 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -412,13 +412,6 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_generator_if_skip_yarn_is_given - run_generator [destination_root, "--skip-yarn"] - - assert_no_file "package.json" - assert_no_file "bin/yarn" - end - def test_generator_if_skip_action_cable_is_given run_generator [destination_root, "--skip-action-cable"] assert_file "config/application.rb", /#\s+require\s+["']action_cable\/engine["']/ @@ -524,6 +517,7 @@ class AppGeneratorTest < Rails::Generators::TestCase def test_generator_for_yarn_skipped run_generator([destination_root, "--skip-yarn"]) assert_no_file "package.json" + assert_no_file "bin/yarn" assert_file "config/initializers/assets.rb" do |content| assert_no_match(/node_modules/, content) |