1
0
Fork 0
mirror of https://github.com/rails/rails.git synced 2022-11-09 12:12:34 -05:00
rails--rails/activerecord/test/cases/dup_test.rb
eileencodes 816f6194b6
Add option for default_scope to run on all queries
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.
2020-12-01 11:15:08 -05:00

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