mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
816f6194b6
This change allows for applications to optionally run a `default_scope` on `update` and `delete` queries. Default scopes already ran on select and insert queries. Applications can now run a set default scope on all queries for a model by setting a `all_queries` option: ```ruby class Article < ApplicationRecord default_scope -> { where(blog_id: 1) }, all_queries: true end ``` Using the default scope in this way is useful for applications that need to query by more than the primary key by default. An example of this would be in an application using a sharding strategy like Vitess like. For Rails sharding, we route connections first and then query the database. However, Vitess and other solutions use a parameter in the query to figure out how to route the queries. By extending `default_scope` to apply to all queries we can allow applications to optionally apply additional constraints to all queries. Note that this only works with `where` queries as it does not make sense to select a record by primary key with an order. With this change we're allowing apps to select with a primary key and an additional key. To make this change dynamic for routing queries in a tenant sharding strategy applications can use the `Current` API or parameters in a request to route queries: ```ruby class Article < ApplicationRecord default_scope -> { where(blog_id: Current.blog.id) }, all_queries: true end ``` In order to achieve this I created a new object when default scopes are created. This allows us to store both the scope itself and whether we should run this on all queries. I chose not to implement an `on:` option that takes an array of actions because there is no simple or clear way to turn off the default scope for create/select. It also doesn't really make sense to only have a default scope for delete queries. The decision to use `all_queries` here allows for the implementation to be more flexible than it was without creating a mess in an application.
184 lines
4.4 KiB
Ruby
184 lines
4.4 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "cases/helper"
|
|
require "models/reply"
|
|
require "models/topic"
|
|
require "models/movie"
|
|
|
|
module ActiveRecord
|
|
class DupTest < ActiveRecord::TestCase
|
|
fixtures :topics
|
|
|
|
def test_dup
|
|
assert_not_predicate Topic.new.freeze.dup, :frozen?
|
|
end
|
|
|
|
def test_not_readonly
|
|
topic = Topic.first
|
|
|
|
duped = topic.dup
|
|
assert_not duped.readonly?, "should not be readonly"
|
|
end
|
|
|
|
def test_is_readonly
|
|
topic = Topic.first
|
|
topic.readonly!
|
|
|
|
duped = topic.dup
|
|
assert duped.readonly?, "should be readonly"
|
|
end
|
|
|
|
def test_dup_not_persisted
|
|
topic = Topic.first
|
|
duped = topic.dup
|
|
|
|
assert_not duped.persisted?, "topic not persisted"
|
|
assert duped.new_record?, "topic is new"
|
|
end
|
|
|
|
def test_dup_not_previously_new_record
|
|
topic = Topic.first
|
|
duped = topic.dup
|
|
|
|
assert_not duped.previously_new_record?, "should not be a previously new record"
|
|
end
|
|
|
|
def test_dup_not_destroyed
|
|
topic = Topic.first
|
|
topic.destroy
|
|
|
|
duped = topic.dup
|
|
assert_not_predicate duped, :destroyed?
|
|
end
|
|
|
|
def test_dup_has_no_id
|
|
topic = Topic.first
|
|
duped = topic.dup
|
|
assert_nil duped.id
|
|
end
|
|
|
|
def test_dup_with_modified_attributes
|
|
topic = Topic.first
|
|
topic.author_name = "Aaron"
|
|
duped = topic.dup
|
|
assert_equal "Aaron", duped.author_name
|
|
end
|
|
|
|
def test_dup_with_changes
|
|
dbtopic = Topic.first
|
|
topic = Topic.new
|
|
|
|
topic.attributes = dbtopic.attributes.except("id")
|
|
|
|
# duped has no timestamp values
|
|
duped = dbtopic.dup
|
|
|
|
# clear topic timestamp values
|
|
topic.send(:clear_timestamp_attributes)
|
|
|
|
assert_equal topic.changes, duped.changes
|
|
end
|
|
|
|
def test_dup_topics_are_independent
|
|
topic = Topic.first
|
|
topic.author_name = "Aaron"
|
|
duped = topic.dup
|
|
|
|
duped.author_name = "meow"
|
|
|
|
assert_not_equal topic.changes, duped.changes
|
|
end
|
|
|
|
def test_dup_attributes_are_independent
|
|
topic = Topic.first
|
|
duped = topic.dup
|
|
|
|
duped.author_name = "meow"
|
|
topic.author_name = "Aaron"
|
|
|
|
assert_equal "Aaron", topic.author_name
|
|
assert_equal "meow", duped.author_name
|
|
end
|
|
|
|
def test_dup_timestamps_are_cleared
|
|
topic = Topic.first
|
|
assert_not_nil topic.updated_at
|
|
assert_not_nil topic.created_at
|
|
|
|
# temporary change to the topic object
|
|
topic.updated_at -= 3.days
|
|
|
|
# dup should not preserve the timestamps if present
|
|
new_topic = topic.dup
|
|
assert_nil new_topic.updated_at
|
|
assert_nil new_topic.created_at
|
|
|
|
new_topic.save
|
|
assert_not_nil new_topic.updated_at
|
|
assert_not_nil new_topic.created_at
|
|
end
|
|
|
|
def test_dup_after_initialize_callbacks
|
|
topic = Topic.new
|
|
assert Topic.after_initialize_called
|
|
Topic.after_initialize_called = false
|
|
topic.dup
|
|
assert Topic.after_initialize_called
|
|
end
|
|
|
|
def test_dup_validity_is_independent
|
|
repair_validations(Topic) do
|
|
Topic.validates_presence_of :title
|
|
topic = Topic.new("title" => "Literature")
|
|
topic.valid?
|
|
|
|
duped = topic.dup
|
|
duped.title = nil
|
|
assert_predicate duped, :invalid?
|
|
|
|
topic.title = nil
|
|
duped.title = "Mathematics"
|
|
assert_predicate topic, :invalid?
|
|
assert_predicate duped, :valid?
|
|
end
|
|
end
|
|
|
|
def test_dup_with_default_scope
|
|
prev_default_scopes = Topic.default_scopes
|
|
Topic.default_scopes = [ActiveRecord::Scoping::DefaultScope.new(proc { Topic.where(approved: true) })]
|
|
topic = Topic.new(approved: false)
|
|
assert_not topic.dup.approved?, "should not be overridden by default scopes"
|
|
ensure
|
|
Topic.default_scopes = prev_default_scopes
|
|
end
|
|
|
|
def test_dup_without_primary_key
|
|
skip if current_adapter?(:OracleAdapter)
|
|
|
|
klass = Class.new(ActiveRecord::Base) do
|
|
self.table_name = "parrots_pirates"
|
|
end
|
|
|
|
record = klass.create!
|
|
|
|
assert_nothing_raised do
|
|
record.dup
|
|
end
|
|
end
|
|
|
|
def test_dup_record_not_persisted_after_rollback_transaction
|
|
movie = Movie.new(name: "test")
|
|
|
|
assert_raises(ActiveRecord::RecordInvalid) do
|
|
Movie.transaction do
|
|
movie.save!
|
|
duped = movie.dup
|
|
duped.name = nil
|
|
duped.save!
|
|
end
|
|
end
|
|
|
|
assert_not movie.persisted?
|
|
end
|
|
end
|
|
end
|