active-record-ex 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +35 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/active-record-ex.gemspec +28 -0
- data/lib/active_record_ex.rb +5 -0
- data/lib/active_record_ex/assoc_ordering.rb +57 -0
- data/lib/active_record_ex/assume_destroy.rb +42 -0
- data/lib/active_record_ex/many_to_many.rb +138 -0
- data/lib/active_record_ex/nillable_find.rb +33 -0
- data/lib/active_record_ex/polymorphic_build.rb +30 -0
- data/lib/active_record_ex/relation_extensions.rb +37 -0
- data/lib/active_record_ex/version.rb +3 -0
- data/test/test_helper.rb +104 -0
- data/test/unit/assoc_ordering_test.rb +71 -0
- data/test/unit/assume_destroy_test.rb +83 -0
- data/test/unit/many_to_many_test.rb +162 -0
- data/test/unit/nillable_find_test.rb +57 -0
- data/test/unit/polymorphic_build_test.rb +28 -0
- data/test/unit/relation_extensions_test.rb +62 -0
- metadata +159 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 510af257c830989c351130a6613f9320e7f18e4c
|
4
|
+
data.tar.gz: 91e4ae0a1b8da4acee5c958cd28d76ca6f3444e6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 98f6ac04aec0f9c0a4142f569be1e794f4cc7b288415db89d41791568028167f4100d33bd2924c21148f6410861d2be2c8f2e106de0bd5de90bdfd7a641d60a3
|
7
|
+
data.tar.gz: 530f2749ba17228e2a812acae3c84d3a142da31550f20579b118f723de3dcc0b850d87218dff9388b0aad9920a1c4e339fc87a5387687f5e5e320a8509513107
|
data/.gitignore
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# Created by .ignore support plugin (hsz.mobi)
|
2
|
+
|
3
|
+
### Ruby template
|
4
|
+
*.gem
|
5
|
+
*.rbc
|
6
|
+
/.config
|
7
|
+
/coverage/
|
8
|
+
/InstalledFiles
|
9
|
+
/pkg/
|
10
|
+
/spec/reports/
|
11
|
+
/test/tmp/
|
12
|
+
/test/version_tmp/
|
13
|
+
/tmp/
|
14
|
+
|
15
|
+
.idea
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/.bundle/
|
25
|
+
/vendor/bundle
|
26
|
+
/lib/bundler/man/
|
27
|
+
|
28
|
+
# for a library or gem, you might want to ignore these files since the code is
|
29
|
+
# intended to run in multiple environments; otherwise, check them in:
|
30
|
+
Gemfile.lock
|
31
|
+
.ruby-version
|
32
|
+
.ruby-gemset
|
33
|
+
|
34
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
35
|
+
.rvmrc
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 PagerDuty, Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# ActiveRecordEx
|
2
|
+
|
3
|
+
A library to make `ActiveRecord::Relation`s even more awesome.
|
4
|
+
|
5
|
+
[![Build Status](https://travis-ci.org/PagerDuty/active-record-ex.svg?branch=master)](https://travis-ci.org/PagerDuty/active-record-ex)
|
6
|
+
|
7
|
+
`ActiveRecordEx` is made of several [modules](#modules) that are used by `include`ing them on your `ActiveRecord` model classes.
|
8
|
+
|
9
|
+
#### Compatibility
|
10
|
+
|
11
|
+
Currently, only ActiveRecord 3.2 with Ruby 2.1 is supported. However, other versions have not been tested and may be compatible.
|
12
|
+
|
13
|
+
## Modules
|
14
|
+
|
15
|
+
### `AssocOrdering`
|
16
|
+
|
17
|
+
Extends setters for `has_many` associations so that ordering of association arrays is persisted.
|
18
|
+
|
19
|
+
### `AssumeDestroy`
|
20
|
+
|
21
|
+
Changes the behavior of `accepts_nested_attributes_for` so that an explicit `_destroy: true` is not required to destroy an association model.
|
22
|
+
|
23
|
+
Instead, all models in the association will be destroyed if they are not included in the set of models used to update the association.
|
24
|
+
|
25
|
+
### `ManyToMany`
|
26
|
+
|
27
|
+
Allows chaining of calls to `has_many` and `belongs_to` relationships.
|
28
|
+
|
29
|
+
### `NillableFind`
|
30
|
+
|
31
|
+
Allows you to treat passing `nil` to a parent association as representing the "parent" of all of the child associations without a parent association.
|
32
|
+
|
33
|
+
### `PolymorphicBuild`
|
34
|
+
|
35
|
+
Allows choosing the subclass of a model in an association via a passed `:type` parameter, useful for `accepts_nested_attributes_for` on a polymorphic association.
|
36
|
+
|
37
|
+
## Development
|
38
|
+
|
39
|
+
Run tests with `rake test`.
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'active_record_ex/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'active-record-ex'
|
8
|
+
spec.version = ActiveRecordEx::VERSION
|
9
|
+
spec.authors = ['Arjun Kavi', 'PagerDuty']
|
10
|
+
spec.email = ['arjun.kavi@gmail.com', 'developers@pagerduty.com']
|
11
|
+
spec.license = 'MIT'
|
12
|
+
spec.summary = 'Relation -> Relation methods'
|
13
|
+
spec.description = 'A library to make ActiveRecord::Relations even more awesome'
|
14
|
+
spec.homepage = 'https://github.com/PagerDuty/active-record-ex'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler'
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'shoulda'
|
24
|
+
spec.add_development_dependency 'mocha'
|
25
|
+
|
26
|
+
spec.add_runtime_dependency 'activesupport', '~> 3.2'
|
27
|
+
spec.add_runtime_dependency 'activerecord', '~> 3.2'
|
28
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Extends setters for has_many associations
|
2
|
+
# So that ordering of association arrays is persisted
|
3
|
+
module ActiveRecordEx
|
4
|
+
module AssocOrdering
|
5
|
+
def self.included(base)
|
6
|
+
base.extend(ClassMethods)
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def has_many(assoc_name, options = {}, &extension)
|
11
|
+
order_field = options.delete(:order_on)
|
12
|
+
super
|
13
|
+
return unless order_field
|
14
|
+
|
15
|
+
define_model_setter(assoc_name, order_field)
|
16
|
+
end
|
17
|
+
|
18
|
+
def accepts_nested_attributes_for(assoc_name, options = {})
|
19
|
+
order_field = options.delete(:order_on)
|
20
|
+
allow_destroy = options[:allow_destroy] || options[:assume_destroy]
|
21
|
+
super
|
22
|
+
return unless order_field
|
23
|
+
|
24
|
+
define_attribute_setter(assoc_name, order_field, allow_destroy)
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def define_model_setter(assoc_name, order_field)
|
30
|
+
setter_name = "#{assoc_name}="
|
31
|
+
unordering_setter_name = "#{assoc_name}_without_ordering="
|
32
|
+
ordering_setter_name = "#{assoc_name}_with_ordering="
|
33
|
+
|
34
|
+
define_method(ordering_setter_name) do |models|
|
35
|
+
models.each_with_index{ |m, i| m.send("#{order_field}=", i + 1) }
|
36
|
+
self.send(unordering_setter_name, models)
|
37
|
+
end
|
38
|
+
alias_method_chain setter_name, :ordering
|
39
|
+
end
|
40
|
+
|
41
|
+
def define_attribute_setter(assoc_name, order_field, allow_destroy)
|
42
|
+
attrs_name = "#{assoc_name}_attributes"
|
43
|
+
setter_name = "#{attrs_name}="
|
44
|
+
unordering_setter_name = "#{attrs_name}_without_ordering="
|
45
|
+
ordering_setter_name = "#{attrs_name}_with_ordering="
|
46
|
+
|
47
|
+
define_method(ordering_setter_name) do |attrs|
|
48
|
+
new_attrs = attrs
|
49
|
+
new_attrs = attrs.reject{ |a| a[:_destroy] } if allow_destroy
|
50
|
+
new_attrs.each_with_index{ |a, i| a[order_field] = i + 1 }
|
51
|
+
self.send(unordering_setter_name, attrs)
|
52
|
+
end
|
53
|
+
alias_method_chain setter_name, :ordering
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# extends accepts_nested_attributes_for
|
2
|
+
# by default, accepts_nested_attributes_for "allow_destroy"s,
|
3
|
+
# ie, will destroy associations if explicitly marked by _destroy: true
|
4
|
+
# this flips that, causing an association to be destroyed
|
5
|
+
# if it's not included in the updating attrs
|
6
|
+
module ActiveRecordEx
|
7
|
+
module AssumeDestroy
|
8
|
+
def self.included base
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def accepts_nested_attributes_for(assoc_name, options={})
|
14
|
+
assume_destroy = options[:assume_destroy]
|
15
|
+
options.delete(:assume_destroy)
|
16
|
+
options[:allow_destroy] = assume_destroy
|
17
|
+
|
18
|
+
super assoc_name, options
|
19
|
+
|
20
|
+
return unless assume_destroy
|
21
|
+
attrs_name = ("#{assoc_name.to_s}_attributes").to_sym
|
22
|
+
setter_name = ("#{attrs_name.to_s}=").to_sym
|
23
|
+
unassuming_setter_name = ("#{attrs_name.to_s}_without_assume=").to_sym
|
24
|
+
assuming_setter_name = ("#{attrs_name.to_s}_with_assume=").to_sym
|
25
|
+
|
26
|
+
define_method(assuming_setter_name) do |attrs|
|
27
|
+
ids = attrs.map { |a| a['id'] || a[:id] }.compact
|
28
|
+
assocs = self.send(assoc_name)
|
29
|
+
|
30
|
+
dead_assocs = []
|
31
|
+
# the ternary's 'cause Arel doesn't do the right thing with an empty array
|
32
|
+
dead_assocs = assocs.where('id NOT IN (?)', ids.any? ? ids : '') unless self.new_record?
|
33
|
+
dead_attrs = dead_assocs.map {|assoc| {id: assoc.id, _destroy: true }}
|
34
|
+
|
35
|
+
attrs = attrs.concat(dead_attrs)
|
36
|
+
self.send(unassuming_setter_name, attrs)
|
37
|
+
end
|
38
|
+
alias_method_chain setter_name, :assume
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# allows you to chain has_manys and belongs_tos
|
2
|
+
# eg: $esc_pol.escalation_rules.escalation_targets
|
3
|
+
# eg: $usr.schedules.escalation_policies
|
4
|
+
module ActiveRecordEx
|
5
|
+
module ManyToMany
|
6
|
+
class ModelArel < ActiveRecord::Relation
|
7
|
+
def initialize(model)
|
8
|
+
super(model.class, model.class.arel_table)
|
9
|
+
@loaded = true
|
10
|
+
@records = [model]
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset
|
14
|
+
# reset says "my currently loaded into memory models no longer currently represent this relation".
|
15
|
+
# That's never true for a ModelArel, so we no-op
|
16
|
+
end
|
17
|
+
|
18
|
+
def pluck(key)
|
19
|
+
key = key.name if key.respond_to? :name
|
20
|
+
@records.map(&:"#{key}")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.included(base)
|
25
|
+
base.extend(ClassMethods)
|
26
|
+
end
|
27
|
+
|
28
|
+
module ClassMethods
|
29
|
+
def belongs_to(name, options={})
|
30
|
+
subtypes = options.delete(:subtypes)
|
31
|
+
super
|
32
|
+
define_belongs_assoc(name, options.merge(subtypes: subtypes))
|
33
|
+
end
|
34
|
+
|
35
|
+
def has_many(name, options = {}, &extension)
|
36
|
+
super
|
37
|
+
define_has_assoc(name.to_s.singularize, options)
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_one(name, options = {}, &extension)
|
41
|
+
super
|
42
|
+
define_has_assoc(name.to_s, options)
|
43
|
+
end
|
44
|
+
|
45
|
+
def singularize(method_name)
|
46
|
+
define_method(method_name) do |*params|
|
47
|
+
ModelArel.new(self).send(method_name, *params)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
def define_belongs_assoc(name, options)
|
54
|
+
if options[:polymorphic] && options[:subtypes]
|
55
|
+
define_polymorphic_assoc(name, options[:subtypes])
|
56
|
+
elsif !options[:polymorphic]
|
57
|
+
define_monomorphic_assoc(name, options)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def define_has_assoc(name, options)
|
62
|
+
if options[:through]
|
63
|
+
define_through_assoc(name, options)
|
64
|
+
else
|
65
|
+
define_plain_has_assoc(name, options)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def define_monomorphic_assoc(name, options)
|
70
|
+
name = name.to_s.singularize
|
71
|
+
klass_name = options[:class_name] || self.parent_string + name.camelize
|
72
|
+
key_name = options[:foreign_key] || name.foreign_key
|
73
|
+
|
74
|
+
method_name = name.pluralize.to_sym
|
75
|
+
define_singleton_method(method_name) do
|
76
|
+
klass = klass_name.constantize
|
77
|
+
foreign_key = self.arel_table[key_name]
|
78
|
+
primary_keys = self.pluck(foreign_key).uniq
|
79
|
+
# eg, Account.where(id: ids)
|
80
|
+
klass.where(klass.primary_key => primary_keys)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def define_polymorphic_assoc(name, subtypes)
|
85
|
+
Array.wrap(subtypes).each do |subtype_klass|
|
86
|
+
key_name = name.to_s.foreign_key
|
87
|
+
type_key = "#{name.to_s}_type"
|
88
|
+
type_val = subtype_klass.to_s
|
89
|
+
|
90
|
+
method_name = subtype_klass.to_s.demodulize.underscore.pluralize.to_sym
|
91
|
+
define_singleton_method(method_name) do
|
92
|
+
foreign_key = self.arel_table[key_name]
|
93
|
+
primary_keys = self.where(type_key => type_val).pluck(foreign_key).uniq
|
94
|
+
# eg, Account.where(id: ids)
|
95
|
+
subtype_klass.where(subtype_klass.primary_key => primary_keys)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def define_plain_has_assoc(name, options)
|
101
|
+
klass_name = options[:class_name] || self.parent_string + name.camelize
|
102
|
+
|
103
|
+
conditions = {}
|
104
|
+
if options[:as]
|
105
|
+
type_key_name = "#{options[:as].to_s}_type"
|
106
|
+
conditions[type_key_name] = self.to_s
|
107
|
+
foreign_key_name = options[:as].to_s.foreign_key
|
108
|
+
else
|
109
|
+
foreign_key_name = options[:foreign_key] || self.to_s.foreign_key
|
110
|
+
end
|
111
|
+
|
112
|
+
method_name = name.pluralize.to_sym
|
113
|
+
define_singleton_method(method_name) do
|
114
|
+
primary_key = self.arel_table[self.primary_key]
|
115
|
+
foreign_keys = self.pluck(primary_key).uniq
|
116
|
+
|
117
|
+
other_klass = klass_name.constantize
|
118
|
+
other_klass.where(conditions).where(foreign_key_name => foreign_keys)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def define_through_assoc(name, options)
|
123
|
+
through_method = options[:through].to_s.pluralize
|
124
|
+
method_name = name.pluralize.to_sym
|
125
|
+
define_singleton_method(method_name) do
|
126
|
+
through_base = self.send(through_method)
|
127
|
+
through_base.send(method_name)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def parent_string
|
132
|
+
parent = self.parent
|
133
|
+
return '' if parent == Object
|
134
|
+
"#{parent.to_s}::"
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActiveRecordEx
|
2
|
+
module NillableFind
|
3
|
+
class NillableArel
|
4
|
+
def initialize(base, ids, parent_scope)
|
5
|
+
@base = base
|
6
|
+
@ids = ids
|
7
|
+
@parent_scope = parent_scope
|
8
|
+
end
|
9
|
+
|
10
|
+
def method_missing(method_name, *args, &block)
|
11
|
+
return super unless @base.respond_to? method_name
|
12
|
+
|
13
|
+
normals = @base.where(id: @ids).send(method_name, *args)
|
14
|
+
return normals unless @ids.include? nil
|
15
|
+
|
16
|
+
used = @base.send(method_name, *args)
|
17
|
+
# return those in normals AND those in parent scope not belonging to another
|
18
|
+
normals.disjunct(used.relative_complement(@parent_scope))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
raise ArgumentError.new("#{base} must include ManyToMany") unless base.included_modules.include? ActiveRecordEx::ManyToMany
|
24
|
+
base.extend(ClassMethods)
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def nillable_find(ids, parent_scope)
|
29
|
+
NillableArel.new(self, ids, parent_scope)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Allows choosing the subclass of a model
|
2
|
+
# via a passed :type parameter
|
3
|
+
# this is useful for using accepts_nested_attributes_for
|
4
|
+
# with subclassed associations
|
5
|
+
module ActiveRecordEx
|
6
|
+
module PolymorphicBuild
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
base.instance_eval do
|
10
|
+
attr_accessible :type
|
11
|
+
|
12
|
+
class << self
|
13
|
+
alias_method_chain :new, :typing
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module ClassMethods
|
19
|
+
def new_with_typing(attrs = {}, options = {})
|
20
|
+
if attrs[:type] && (klass = attrs[:type].constantize) < self
|
21
|
+
klass.new(attrs, options)
|
22
|
+
elsif attrs[:type] && !((klass = attrs[:type].constantize) <= self)
|
23
|
+
raise ArgumentError.new("Attempting to instantiate #{klass}, which is not a subclass of #{self}")
|
24
|
+
else
|
25
|
+
new_without_typing(attrs, options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Relation
|
3
|
+
def where_only(value)
|
4
|
+
relation = clone
|
5
|
+
relation.where_values = build_where(value)
|
6
|
+
relation
|
7
|
+
end
|
8
|
+
|
9
|
+
def none
|
10
|
+
self.where('1=0')
|
11
|
+
end
|
12
|
+
|
13
|
+
def disjunct(other)
|
14
|
+
other_where = other.collapsed_where
|
15
|
+
this_where = self.collapsed_where
|
16
|
+
self.where_only(this_where.or(other_where))
|
17
|
+
end
|
18
|
+
|
19
|
+
# If self and other are viewed as sets
|
20
|
+
# relative_complement represents everything
|
21
|
+
# that's in other but NOT in self
|
22
|
+
def relative_complement(other)
|
23
|
+
this_where = self.collapsed_where
|
24
|
+
other_where = other.collapsed_where
|
25
|
+
self.where_only(this_where.not.and(other_where))
|
26
|
+
end
|
27
|
+
|
28
|
+
protected
|
29
|
+
|
30
|
+
def collapsed_where
|
31
|
+
values = self.where_values
|
32
|
+
values = [true] if values.empty?
|
33
|
+
# FIXME: Needs to wrap string literal conditions (e.g., where("id > 1"))
|
34
|
+
Arel::Nodes::And.new(values)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'mocha'
|
5
|
+
|
6
|
+
require 'active_record_ex/relation_extensions'
|
7
|
+
|
8
|
+
class ActiveSupport::TestCase
|
9
|
+
def db_expects(arel, query, response = nil)
|
10
|
+
response_columns = response.try(:first).try(:keys).try(:map, &:to_s) || []
|
11
|
+
response_rows = response.try(:map, &:values) || []
|
12
|
+
response = ActiveRecord::Result.new(response_columns, response_rows)
|
13
|
+
|
14
|
+
case query.first
|
15
|
+
when /^SELECT/
|
16
|
+
arel.connection.expects(:exec_query).with(*query).returns(response)
|
17
|
+
when /^DELETE/
|
18
|
+
arel.connection.expects(:exec_delete).with(*query).returns(response)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Used as a "dummy" model in tests to avoid using a database connection
|
24
|
+
class StubModel < ActiveRecord::Base
|
25
|
+
self.abstract_class = true
|
26
|
+
|
27
|
+
conn = Class.new(ActiveRecord::ConnectionAdapters::AbstractAdapter) do
|
28
|
+
def quote_column_name(name)
|
29
|
+
"`#{name.to_s.gsub('`', '``')}`"
|
30
|
+
end
|
31
|
+
|
32
|
+
def quote_table_name(name)
|
33
|
+
quote_column_name(name).gsub('.', '`.`')
|
34
|
+
end
|
35
|
+
|
36
|
+
def select(sql, name = nil, _ = [])
|
37
|
+
exec_query(sql, name).to_a
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
visitor = Class.new(Arel::Visitors::ToSql) do
|
42
|
+
def table_exists?(*_)
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def column_for(attr)
|
47
|
+
pk = attr == 'id'
|
48
|
+
column = ActiveRecord::ConnectionAdapters::Column.new(attr, nil, pk ? :integer : :string)
|
49
|
+
column.primary = pk
|
50
|
+
column
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
@@connection = conn.new({})
|
55
|
+
@@connection.visitor = visitor.new(@@connection)
|
56
|
+
|
57
|
+
class << self
|
58
|
+
def connection
|
59
|
+
@@connection
|
60
|
+
end
|
61
|
+
|
62
|
+
def columns; []; end
|
63
|
+
|
64
|
+
def get_primary_key(*_); 'id'; end
|
65
|
+
end
|
66
|
+
|
67
|
+
# prevent AR from hitting the DB to get the schema
|
68
|
+
def get_primary_key(*_); 'id'; end
|
69
|
+
|
70
|
+
def with_transaction_returning_status; yield; end
|
71
|
+
end
|
72
|
+
|
73
|
+
# SQLCounter is part of ActiveRecord but is not distributed with the gem (used for internal tests only)
|
74
|
+
# see https://github.com/rails/rails/blob/3-2-stable/activerecord/test/cases/helper.rb#L59
|
75
|
+
module ActiveRecord
|
76
|
+
class SQLCounter
|
77
|
+
cattr_accessor :ignored_sql
|
78
|
+
self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
|
79
|
+
|
80
|
+
# FIXME: this needs to be refactored so specific database can add their own
|
81
|
+
# ignored SQL. This ignored SQL is for Oracle.
|
82
|
+
ignored_sql.concat [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
|
83
|
+
|
84
|
+
cattr_accessor :log
|
85
|
+
self.log = []
|
86
|
+
|
87
|
+
attr_reader :ignore
|
88
|
+
|
89
|
+
def initialize(ignore = self.class.ignored_sql)
|
90
|
+
@ignore = ignore
|
91
|
+
end
|
92
|
+
|
93
|
+
def call(name, start, finish, message_id, values)
|
94
|
+
sql = values[:sql]
|
95
|
+
|
96
|
+
# FIXME: this seems bad. we should probably have a better way to indicate
|
97
|
+
# the query was cached
|
98
|
+
return if 'CACHE' == values[:name] || ignore.any? { |x| x =~ sql }
|
99
|
+
self.class.log << sql
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
|
104
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_ex/assoc_ordering'
|
3
|
+
|
4
|
+
class OrderedAssoc < StubModel
|
5
|
+
attr_accessor :name
|
6
|
+
attr_accessor :order
|
7
|
+
attr_accessor :has_orderd_assoc_id
|
8
|
+
end
|
9
|
+
|
10
|
+
class DestroyableAssoc < StubModel
|
11
|
+
attr_accessor :name
|
12
|
+
attr_accessor :order
|
13
|
+
attr_accessor :has_orderd_assoc_id
|
14
|
+
end
|
15
|
+
|
16
|
+
class HasOrderedAssoc < StubModel
|
17
|
+
include ActiveRecordEx::AssocOrdering
|
18
|
+
|
19
|
+
has_many :ordered_assocs, order_on: :order
|
20
|
+
accepts_nested_attributes_for :ordered_assocs, order_on: :order
|
21
|
+
|
22
|
+
has_many :destroyable_assocs, order_on: :order
|
23
|
+
accepts_nested_attributes_for :destroyable_assocs, order_on: :order, allow_destroy: true
|
24
|
+
|
25
|
+
def save
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
class AssocOrderingTest < ActiveSupport::TestCase
|
31
|
+
context 'A class with ActiveREcord::AssocOrdering included' do
|
32
|
+
setup { @model = HasOrderedAssoc.new }
|
33
|
+
|
34
|
+
should 'order associations set as models' do
|
35
|
+
assocs = [OrderedAssoc.new(name: 'first'), OrderedAssoc.new(name: 'second')]
|
36
|
+
@model.ordered_assocs = assocs
|
37
|
+
sorted = @model.ordered_assocs.sort_by(&:order)
|
38
|
+
assert_equal 'first', sorted[0].name
|
39
|
+
assert_equal 1, sorted[0].order
|
40
|
+
assert_equal 'second', sorted[1].name
|
41
|
+
assert_equal 2, sorted[1].order
|
42
|
+
end
|
43
|
+
|
44
|
+
should 'order associations set as attributes' do
|
45
|
+
attrs = {ordered_assocs_attributes:
|
46
|
+
[{name: 'first'}, {name: 'second'}]
|
47
|
+
}
|
48
|
+
expected_attrs = [{name: 'first', order: 1}, {name: 'second', order: 2}]
|
49
|
+
@model.expects(:ordered_assocs_attributes_without_ordering=).with(expected_attrs)
|
50
|
+
@model.update_attributes(attrs)
|
51
|
+
end
|
52
|
+
|
53
|
+
should 'not ignore marked-for-destroy association attributes for ordering that don\'t allow destroy' do
|
54
|
+
attrs = {ordered_assocs_attributes:
|
55
|
+
[{name: 'first', _destroy: true}, {name: 'second'}]
|
56
|
+
}
|
57
|
+
expected_attrs = [{name: 'first', _destroy: true, order: 1}, {name: 'second', order: 2}]
|
58
|
+
@model.expects(:ordered_assocs_attributes_without_ordering=).with(expected_attrs)
|
59
|
+
@model.update_attributes(attrs)
|
60
|
+
end
|
61
|
+
|
62
|
+
should 'ignore marked-for-destroy association attributes for ordering that allow destroy' do
|
63
|
+
attrs = {destroyable_assocs_attributes:
|
64
|
+
[{name: 'first', _destroy: true}, {name: 'second'}]
|
65
|
+
}
|
66
|
+
expected_attrs = [{name: 'first', _destroy: true}, {name: 'second', order: 1}]
|
67
|
+
@model.expects(:destroyable_assocs_attributes_without_ordering=).with(expected_attrs)
|
68
|
+
@model.update_attributes(attrs)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_ex/assume_destroy'
|
3
|
+
|
4
|
+
class AssumeDestroyTest < ActiveRecord::TestCase
|
5
|
+
class AssumesDestroy < StubModel
|
6
|
+
include ActiveRecordEx::AssumeDestroy
|
7
|
+
|
8
|
+
has_many :destroyees
|
9
|
+
accepts_nested_attributes_for :destroyees, assume_destroy: true
|
10
|
+
end
|
11
|
+
|
12
|
+
class Destroyee < StubModel
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'ActiveRecordEx::AssumeDestroy' do
|
16
|
+
setup do
|
17
|
+
@subject = AssumesDestroy.new
|
18
|
+
@subject.stubs(:new_record?).returns(false)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'preconditions in ActiveRecord' do
|
22
|
+
should 'DELETE records marked for destruction' do
|
23
|
+
attrs = []
|
24
|
+
stub_association_query(@subject, '\'\'', [{'id' => 1}, {'id' => 2}])
|
25
|
+
db_expects(@subject, ['SELECT `destroyees`.* FROM `destroyees` WHERE `destroyees`.`assumes_destroy_id` IS NULL AND `destroyees`.`id` IN (1, 2)', 'AssumeDestroyTest::Destroyee Load'], [{'id' => 1}, {'id' => 2}])
|
26
|
+
db_expects(@subject.destroyees, ['DELETE FROM `destroyees` WHERE `destroyees`.`id` = ?', 'SQL', [[nil, 1]]])
|
27
|
+
db_expects(@subject.destroyees, ['DELETE FROM `destroyees` WHERE `destroyees`.`id` = ?', 'SQL', [[nil, 2]]])
|
28
|
+
|
29
|
+
@subject.update_attributes(destroyees_attributes: attrs)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
should 'not mark any for destruction if subject is new' do
|
34
|
+
@subject.stubs(:new_record?).returns(true)
|
35
|
+
attrs = [{name: 'one'}]
|
36
|
+
expected_attrs = [{name: 'one'}]
|
37
|
+
@subject.expects(:destroyees_attributes_without_assume=).with(expected_attrs)
|
38
|
+
|
39
|
+
# shouldn't even hit the DB
|
40
|
+
assert_no_queries { @subject.destroyees_attributes = attrs }
|
41
|
+
end
|
42
|
+
|
43
|
+
should 'mark all associations for destruction when passed an empty array' do
|
44
|
+
attrs = []
|
45
|
+
stub_association_query(@subject, '\'\'', [{'id' => 1}, {'id' => 2}])
|
46
|
+
|
47
|
+
expected_attrs = [{id: 1, _destroy: true}, {id: 2, _destroy: true}]
|
48
|
+
@subject.expects(:destroyees_attributes_without_assume=).with(expected_attrs)
|
49
|
+
@subject.destroyees_attributes = attrs
|
50
|
+
end
|
51
|
+
|
52
|
+
should 'mark all existing associations for destruction when passed an array of just new' do
|
53
|
+
attrs = [{name: 'one'}]
|
54
|
+
stub_association_query(@subject, '\'\'', [{'id' => 1}, {'id' => 2}])
|
55
|
+
|
56
|
+
expected_attrs = [{name: 'one'}, {id: 1, _destroy: true}, {id: 2, _destroy: true}]
|
57
|
+
@subject.expects(:destroyees_attributes_without_assume=).with(expected_attrs)
|
58
|
+
@subject.destroyees_attributes = attrs
|
59
|
+
end
|
60
|
+
|
61
|
+
should 'not mark explicitly passed in associations for destruction' do
|
62
|
+
attrs = [{name: 'one'}, {id: 1}]
|
63
|
+
stub_association_query(@subject, '1', [{id: 2}])
|
64
|
+
|
65
|
+
expected_attrs = [{name: 'one'}, {id: 1}, {id: 2, _destroy: true}]
|
66
|
+
@subject.expects(:destroyees_attributes_without_assume=).with(expected_attrs)
|
67
|
+
@subject.destroyees_attributes = attrs
|
68
|
+
end
|
69
|
+
|
70
|
+
should 'preserve existing marks for destruction' do
|
71
|
+
attrs = [{name: 'one'}, {id: 1, _destroy: true}]
|
72
|
+
stub_association_query(@subject, '1', [{id: 2}])
|
73
|
+
|
74
|
+
expected_attrs = [{name: 'one'}, {id: 1, _destroy: true}, {id: 2, _destroy: true}]
|
75
|
+
@subject.expects(:destroyees_attributes_without_assume=).with(expected_attrs)
|
76
|
+
@subject.destroyees_attributes = attrs
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def stub_association_query(subject, id_string, id_response)
|
81
|
+
db_expects(subject, ["SELECT `destroyees`.* FROM `destroyees` WHERE `destroyees`.`assumes_destroy_id` IS NULL AND (id NOT IN (#{id_string}))", 'AssumeDestroyTest::Destroyee Load'], id_response)
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_ex/many_to_many'
|
3
|
+
|
4
|
+
class ManyToManyTest < ActiveSupport::TestCase
|
5
|
+
class HasManied < StubModel
|
6
|
+
include ActiveRecordEx::ManyToMany
|
7
|
+
|
8
|
+
has_one :one
|
9
|
+
has_many :simple_belongs_tos
|
10
|
+
has_many :belongs_to_throughs, through: :simple_belongs_tos
|
11
|
+
has_many :class_nameds, class_name: 'ManyToManyTest::SomeClassName'
|
12
|
+
has_many :foreign_keyeds, foreign_key: :some_foreign_key_id
|
13
|
+
has_many :aseds, as: :some_as
|
14
|
+
|
15
|
+
singularize :ones
|
16
|
+
end
|
17
|
+
class SimpleBelongsTo < StubModel
|
18
|
+
include ActiveRecordEx::ManyToMany
|
19
|
+
|
20
|
+
belongs_to :has_manied
|
21
|
+
has_many :belongs_to_throughs
|
22
|
+
|
23
|
+
singularize :has_manieds
|
24
|
+
end
|
25
|
+
class BelongsToThrough < StubModel
|
26
|
+
include ActiveRecordEx::ManyToMany
|
27
|
+
|
28
|
+
belongs_to :has_manied
|
29
|
+
end
|
30
|
+
class SomeClassName < StubModel
|
31
|
+
include ActiveRecordEx::ManyToMany
|
32
|
+
|
33
|
+
belongs_to :some_name, class_name: 'ManyToManyTest::HasManied'
|
34
|
+
end
|
35
|
+
class ForeignKeyed < StubModel
|
36
|
+
include ActiveRecordEx::ManyToMany
|
37
|
+
|
38
|
+
belongs_to :has_manied, foreign_key: :some_foreign_key_id
|
39
|
+
end
|
40
|
+
class Ased < StubModel
|
41
|
+
include ActiveRecordEx::ManyToMany
|
42
|
+
|
43
|
+
belongs_to :some_as, polymorphic: true, subtypes: [HasManied]
|
44
|
+
end
|
45
|
+
class One < StubModel
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'ActiveRecord::ManyToMany' do
|
49
|
+
context '#has_one' do
|
50
|
+
setup { @arel = HasManied.scoped }
|
51
|
+
should 'handle the simple case correctly' do
|
52
|
+
db_expects(@arel, ['SELECT `has_manieds`.`id` FROM `has_manieds` '], [{id: 1}])
|
53
|
+
db_expects(@arel, ['SELECT `ones`.* FROM `ones` WHERE `ones`.`has_manied_id` IN (1)', 'ManyToManyTest::One Load'])
|
54
|
+
@arel.ones.to_a
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
context '#has_many' do
|
59
|
+
setup { @arel = HasManied.scoped }
|
60
|
+
|
61
|
+
should 'handle the simple case correctly' do
|
62
|
+
db_expects(@arel, ['SELECT `has_manieds`.`id` FROM `has_manieds` '], [id: 1])
|
63
|
+
db_expects(@arel, ['SELECT `simple_belongs_tos`.* FROM `simple_belongs_tos` WHERE `simple_belongs_tos`.`has_manied_id` IN (1)', 'ManyToManyTest::SimpleBelongsTo Load'])
|
64
|
+
@arel.simple_belongs_tos.to_a
|
65
|
+
end
|
66
|
+
|
67
|
+
should 'handle the empty base case correctly' do
|
68
|
+
db_expects(@arel, ['SELECT `has_manieds`.`id` FROM `has_manieds` WHERE (1=0)'], [])
|
69
|
+
db_expects(@arel, ['SELECT `simple_belongs_tos`.* FROM `simple_belongs_tos` WHERE 1=0', 'ManyToManyTest::SimpleBelongsTo Load'])
|
70
|
+
@arel.none.simple_belongs_tos.to_a
|
71
|
+
end
|
72
|
+
|
73
|
+
should 'handle the multiple base ids case correctly' do
|
74
|
+
db_expects(@arel, ['SELECT `has_manieds`.`id` FROM `has_manieds` '], [{id: 1}, {id: 2}])
|
75
|
+
db_expects(@arel, ['SELECT `simple_belongs_tos`.* FROM `simple_belongs_tos` WHERE `simple_belongs_tos`.`has_manied_id` IN (1, 2)', 'ManyToManyTest::SimpleBelongsTo Load'])
|
76
|
+
@arel.simple_belongs_tos.to_a
|
77
|
+
end
|
78
|
+
|
79
|
+
should 'chain queries for has_many through:' do
|
80
|
+
db_expects(@arel, ['SELECT `has_manieds`.`id` FROM `has_manieds` '], [{id: 1}])
|
81
|
+
db_expects(@arel, ['SELECT `simple_belongs_tos`.`id` FROM `simple_belongs_tos` WHERE `simple_belongs_tos`.`has_manied_id` IN (1)'], [{id: 1}])
|
82
|
+
db_expects(@arel, ['SELECT `belongs_to_throughs`.* FROM `belongs_to_throughs` WHERE `belongs_to_throughs`.`simple_belongs_to_id` IN (1)', 'ManyToManyTest::BelongsToThrough Load'])
|
83
|
+
|
84
|
+
@arel.belongs_to_throughs.to_a
|
85
|
+
end
|
86
|
+
|
87
|
+
should 'not N+1 has_many through:' do
|
88
|
+
db_expects(@arel, ['SELECT `has_manieds`.`id` FROM `has_manieds` '], [{id: 1}, {id: 2}])
|
89
|
+
db_expects(@arel, ['SELECT `simple_belongs_tos`.`id` FROM `simple_belongs_tos` WHERE `simple_belongs_tos`.`has_manied_id` IN (1, 2)'], [{id: 1}, {id: 2}])
|
90
|
+
db_expects(@arel, ['SELECT `belongs_to_throughs`.* FROM `belongs_to_throughs` WHERE `belongs_to_throughs`.`simple_belongs_to_id` IN (1, 2)', 'ManyToManyTest::BelongsToThrough Load'])
|
91
|
+
|
92
|
+
@arel.belongs_to_throughs.to_a
|
93
|
+
end
|
94
|
+
|
95
|
+
should 'use the class name passed in' do
|
96
|
+
db_expects(@arel, ['SELECT `has_manieds`.`id` FROM `has_manieds` '], [id: 1])
|
97
|
+
db_expects(@arel, ['SELECT `some_class_names`.* FROM `some_class_names` WHERE `some_class_names`.`has_manied_id` IN (1)', 'ManyToManyTest::SomeClassName Load'])
|
98
|
+
@arel.class_nameds.to_a
|
99
|
+
end
|
100
|
+
|
101
|
+
should 'use the foreign key passed in' do
|
102
|
+
db_expects(@arel, ['SELECT `has_manieds`.`id` FROM `has_manieds` '], [id: 1])
|
103
|
+
db_expects(@arel, ['SELECT `foreign_keyeds`.* FROM `foreign_keyeds` WHERE `foreign_keyeds`.`some_foreign_key_id` IN (1)', 'ManyToManyTest::ForeignKeyed Load'])
|
104
|
+
@arel.foreign_keyeds.to_a
|
105
|
+
end
|
106
|
+
|
107
|
+
should 'use the as passed in' do
|
108
|
+
db_expects(@arel, ['SELECT `has_manieds`.`id` FROM `has_manieds` '], [id: 1])
|
109
|
+
db_expects(@arel, ['SELECT `aseds`.* FROM `aseds` WHERE `aseds`.`some_as_type` = \'ManyToManyTest::HasManied\' AND `aseds`.`some_as_id` IN (1)', 'ManyToManyTest::Ased Load'])
|
110
|
+
@arel.aseds.to_a
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context '#belongs_to' do
|
115
|
+
should 'handle the simple case correctly' do
|
116
|
+
@arel = SimpleBelongsTo.scoped
|
117
|
+
db_expects(@arel, ['SELECT `simple_belongs_tos`.`has_manied_id` FROM `simple_belongs_tos` '], [has_manied_id: 1])
|
118
|
+
db_expects(@arel, ['SELECT `has_manieds`.* FROM `has_manieds` WHERE `has_manieds`.`id` IN (1)', 'ManyToManyTest::HasManied Load'])
|
119
|
+
@arel.has_manieds.to_a
|
120
|
+
end
|
121
|
+
|
122
|
+
should 'use the class name passed in' do
|
123
|
+
@arel = SomeClassName.scoped
|
124
|
+
db_expects(@arel, ['SELECT `some_class_names`.`some_name_id` FROM `some_class_names` '], [some_name_id: 1])
|
125
|
+
db_expects(@arel, ['SELECT `has_manieds`.* FROM `has_manieds` WHERE `has_manieds`.`id` IN (1)', 'ManyToManyTest::HasManied Load'])
|
126
|
+
@arel.some_names.to_a
|
127
|
+
end
|
128
|
+
|
129
|
+
should 'use the foreign key passed in' do
|
130
|
+
@arel = ForeignKeyed.scoped
|
131
|
+
db_expects(@arel, ['SELECT `foreign_keyeds`.`some_foreign_key_id` FROM `foreign_keyeds` '], [some_foreign_key_id: 1])
|
132
|
+
db_expects(@arel, ['SELECT `has_manieds`.* FROM `has_manieds` WHERE `has_manieds`.`id` IN (1)', 'ManyToManyTest::HasManied Load'])
|
133
|
+
@arel.has_manieds.to_a
|
134
|
+
end
|
135
|
+
|
136
|
+
should 'handle polymorphic belongs_to' do
|
137
|
+
@arel = Ased.scoped
|
138
|
+
db_expects(@arel, ['SELECT `aseds`.`some_as_id` FROM `aseds` WHERE `aseds`.`some_as_type` = \'ManyToManyTest::HasManied\''], [some_as_id: 1])
|
139
|
+
db_expects(@arel, ['SELECT `has_manieds`.* FROM `has_manieds` WHERE `has_manieds`.`id` IN (1)', 'ManyToManyTest::HasManied Load'])
|
140
|
+
@arel.has_manieds.to_a
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context '#singularize' do
|
145
|
+
should 'work for belongs_tos without triggering an extra query' do
|
146
|
+
@model = SimpleBelongsTo.new
|
147
|
+
@model.stubs(:has_manied_id).returns(42)
|
148
|
+
@arel = HasManied.scoped
|
149
|
+
db_expects(@arel, ['SELECT `has_manieds`.* FROM `has_manieds` WHERE `has_manieds`.`id` IN (42)', 'ManyToManyTest::HasManied Load'])
|
150
|
+
@model.has_manieds.to_a
|
151
|
+
end
|
152
|
+
|
153
|
+
should 'work for has_ones without triggering an extra query' do
|
154
|
+
@model = HasManied.new
|
155
|
+
@model.stubs(:id).returns(42)
|
156
|
+
@arel = One.scoped
|
157
|
+
db_expects(@arel, ['SELECT `ones`.* FROM `ones` WHERE `ones`.`has_manied_id` IN (42)', 'ManyToManyTest::One Load'])
|
158
|
+
@model.ones.to_a
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_ex/many_to_many'
|
3
|
+
require 'active_record_ex/nillable_find'
|
4
|
+
|
5
|
+
class NillableFindTest < ActiveSupport::TestCase
|
6
|
+
class Parent < StubModel
|
7
|
+
include ActiveRecordEx::ManyToMany
|
8
|
+
include ActiveRecordEx::NillableFind
|
9
|
+
|
10
|
+
has_many :children
|
11
|
+
end
|
12
|
+
|
13
|
+
class Child < StubModel
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'ActiveRecordEx::NillableFind' do
|
17
|
+
context '#nillable_find' do
|
18
|
+
setup { @arel = Parent.scoped }
|
19
|
+
# RC == relative complement
|
20
|
+
should 'request the RC of the base scope in the parent scope when just passed nil' do
|
21
|
+
# fetch IDs
|
22
|
+
db_expects(@arel, ['SELECT `parents`.`id` FROM `parents` WHERE `parents`.`id` IS NULL'], [])
|
23
|
+
# fetch outside set
|
24
|
+
db_expects(@arel, ['SELECT `parents`.`id` FROM `parents` '], [{id: 1}, {id: 2}])
|
25
|
+
# disjunct
|
26
|
+
db_expects(@arel, ['SELECT `children`.* FROM `children` WHERE ((1=0 OR NOT (`children`.`parent_id` IN (1, 2)) AND `children`.`foo` = \'bar\'))', 'NillableFindTest::Child Load'], [])
|
27
|
+
|
28
|
+
Parent.nillable_find([nil], Child.where(foo: 'bar')).children.all
|
29
|
+
end
|
30
|
+
|
31
|
+
should 'request the disjunct of the RC of base scope in parent scope and all children of non-nil ids' do
|
32
|
+
# fetch IDs
|
33
|
+
db_expects(@arel, ['SELECT `parents`.`id` FROM `parents` WHERE ((`parents`.`id` IN (1) OR `parents`.`id` IS NULL))'], [{id: 1}])
|
34
|
+
# fetch outside set
|
35
|
+
db_expects(@arel, ['SELECT `parents`.`id` FROM `parents` '], [{id: 1}, {id: 2}])
|
36
|
+
# disjunct
|
37
|
+
db_expects(@arel, ['SELECT `children`.* FROM `children` WHERE ((`children`.`parent_id` IN (1) OR NOT (`children`.`parent_id` IN (1, 2)) AND `children`.`foo` = \'bar\'))', 'NillableFindTest::Child Load'], [])
|
38
|
+
|
39
|
+
Parent.nillable_find([1, nil], Child.where(foo: 'bar')).children.all
|
40
|
+
end
|
41
|
+
|
42
|
+
should 'request nothing when passed no an empty set of ids' do
|
43
|
+
db_expects(@arel, ['SELECT `parents`.`id` FROM `parents` WHERE 1=0'], [])
|
44
|
+
db_expects(@arel, ['SELECT `children`.* FROM `children` WHERE 1=0', 'NillableFindTest::Child Load'], [])
|
45
|
+
|
46
|
+
Parent.nillable_find([], Child.where(foo: 'bar')).children.all
|
47
|
+
end
|
48
|
+
|
49
|
+
should 'request as a normal many-to-many when passed only normal ids' do
|
50
|
+
db_expects(@arel, ['SELECT `parents`.`id` FROM `parents` WHERE `parents`.`id` IN (1)'], [{id: 1}])
|
51
|
+
db_expects(@arel, ['SELECT `children`.* FROM `children` WHERE `children`.`parent_id` IN (1)', 'NillableFindTest::Child Load'], [])
|
52
|
+
|
53
|
+
Parent.nillable_find([1], Child.where(foo: 'bar')).children.all
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'active_record_ex/polymorphic_build'
|
3
|
+
|
4
|
+
class PolymorphicBuildTest < ActiveSupport::TestCase
|
5
|
+
class PolyBase < StubModel
|
6
|
+
include ActiveRecordEx::PolymorphicBuild
|
7
|
+
attr_accessor :type
|
8
|
+
end
|
9
|
+
|
10
|
+
class PolyChild < PolyBase
|
11
|
+
end
|
12
|
+
|
13
|
+
should 'instantiate an instance with the subclass passed in' do
|
14
|
+
inst = PolyBase.new(type: PolyChild.to_s)
|
15
|
+
assert_equal PolyChild, inst.class
|
16
|
+
end
|
17
|
+
|
18
|
+
should 'instantiate an instance with the class itself passed in' do
|
19
|
+
inst = PolyBase.new(type: PolyBase.to_s)
|
20
|
+
assert_equal PolyBase, inst.class
|
21
|
+
end
|
22
|
+
|
23
|
+
should 'throw an error if the passed in class is not a subclass' do
|
24
|
+
assert_raise(ArgumentError) do
|
25
|
+
PolyBase.new(type: StubModel.to_s)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class RelationExtensionsTest < ActiveSupport::TestCase
|
4
|
+
class HasManied < StubModel
|
5
|
+
has_many :belongs_tos
|
6
|
+
end
|
7
|
+
|
8
|
+
class BelongsTo < StubModel
|
9
|
+
belongs_to :has_manied
|
10
|
+
attr_accessor :has_manied_id
|
11
|
+
end
|
12
|
+
|
13
|
+
context '#relative_complement' do
|
14
|
+
setup do
|
15
|
+
@hm1 = HasManied.new
|
16
|
+
@hm2 = HasManied.new
|
17
|
+
@bt1 = @hm1.belongs_tos.new
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'disjoint set' do
|
21
|
+
should 'return all belongs_tos' do
|
22
|
+
assert_equal @hm1.belongs_tos.all, @hm2.belongs_tos.relative_complement(@hm1.belongs_tos)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'identical set' do
|
27
|
+
should 'return nothing' do
|
28
|
+
assert_empty @hm1.belongs_tos.relative_complement(@hm1.belongs_tos)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'subset' do
|
33
|
+
should 'return all other belongs_tos' do
|
34
|
+
assert_equal @hm1.belongs_tos.where('id <> ?', @bt1.id).all, BelongsTo.where(id: @bt1.id).relative_complement(@hm1.belongs_tos).all
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'by empty set' do
|
39
|
+
should 'return all belongs_tos' do
|
40
|
+
assert_equal @hm1.belongs_tos.all, BelongsTo.where(id: -1).relative_complement(@hm1.belongs_tos)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'of empty set' do
|
45
|
+
should 'return nothing' do
|
46
|
+
assert_empty @hm1.belongs_tos.relative_complement(BelongsTo.where(id: -1))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context 'by unconditional' do
|
51
|
+
should 'return nothing' do
|
52
|
+
assert_empty BelongsTo.scoped.relative_complement(@hm1.belongs_tos)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'of unconditional' do
|
57
|
+
should 'return all belongs_tos' do
|
58
|
+
assert_equal @hm2.belongs_tos.all, @hm1.belongs_tos.relative_complement(BelongsTo.scoped)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
metadata
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active-record-ex
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Arjun Kavi
|
8
|
+
- PagerDuty
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2015-10-31 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rake
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: shoulda
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: mocha
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: activesupport
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '3.2'
|
77
|
+
type: :runtime
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '3.2'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: activerecord
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '3.2'
|
91
|
+
type: :runtime
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - "~>"
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '3.2'
|
98
|
+
description: A library to make ActiveRecord::Relations even more awesome
|
99
|
+
email:
|
100
|
+
- arjun.kavi@gmail.com
|
101
|
+
- developers@pagerduty.com
|
102
|
+
executables: []
|
103
|
+
extensions: []
|
104
|
+
extra_rdoc_files: []
|
105
|
+
files:
|
106
|
+
- ".gitignore"
|
107
|
+
- ".travis.yml"
|
108
|
+
- Gemfile
|
109
|
+
- LICENSE
|
110
|
+
- README.md
|
111
|
+
- Rakefile
|
112
|
+
- active-record-ex.gemspec
|
113
|
+
- lib/active_record_ex.rb
|
114
|
+
- lib/active_record_ex/assoc_ordering.rb
|
115
|
+
- lib/active_record_ex/assume_destroy.rb
|
116
|
+
- lib/active_record_ex/many_to_many.rb
|
117
|
+
- lib/active_record_ex/nillable_find.rb
|
118
|
+
- lib/active_record_ex/polymorphic_build.rb
|
119
|
+
- lib/active_record_ex/relation_extensions.rb
|
120
|
+
- lib/active_record_ex/version.rb
|
121
|
+
- test/test_helper.rb
|
122
|
+
- test/unit/assoc_ordering_test.rb
|
123
|
+
- test/unit/assume_destroy_test.rb
|
124
|
+
- test/unit/many_to_many_test.rb
|
125
|
+
- test/unit/nillable_find_test.rb
|
126
|
+
- test/unit/polymorphic_build_test.rb
|
127
|
+
- test/unit/relation_extensions_test.rb
|
128
|
+
homepage: https://github.com/PagerDuty/active-record-ex
|
129
|
+
licenses:
|
130
|
+
- MIT
|
131
|
+
metadata: {}
|
132
|
+
post_install_message:
|
133
|
+
rdoc_options: []
|
134
|
+
require_paths:
|
135
|
+
- lib
|
136
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - ">="
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
requirements: []
|
147
|
+
rubyforge_project:
|
148
|
+
rubygems_version: 2.2.2
|
149
|
+
signing_key:
|
150
|
+
specification_version: 4
|
151
|
+
summary: Relation -> Relation methods
|
152
|
+
test_files:
|
153
|
+
- test/test_helper.rb
|
154
|
+
- test/unit/assoc_ordering_test.rb
|
155
|
+
- test/unit/assume_destroy_test.rb
|
156
|
+
- test/unit/many_to_many_test.rb
|
157
|
+
- test/unit/nillable_find_test.rb
|
158
|
+
- test/unit/polymorphic_build_test.rb
|
159
|
+
- test/unit/relation_extensions_test.rb
|