active-record-ex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
[](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
|