active_record-framing 0.1.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: db9fa84eb9a0af089cf82457843e6b46c05178f0dd3679253df766ba17755ec1
4
+ data.tar.gz: 37c02526ee91a9fbe029b9f4f740cac732738ddf9c18a189f7b6ba56a2614c10
5
+ SHA512:
6
+ metadata.gz: 734a4dff29f64d57563c622088256f8e3f12d992d4f4943c8eec959e47bab3e0ce91cf4058691e7b7a391cc98a4d35b689ec95c5ac6d15d13bdd69c5cb99fa04
7
+ data.tar.gz: 3abe8e57cf9d98312ab3e651fe48964854a03bb68e1577755249ad0dd33ba4dee615246229acf5c299d4593e8ea44ede1115a309b1de721ea259b5e7a02145f4
data/CHANGELOG.md ADDED
@@ -0,0 +1,53 @@
1
+ # DeletedAt
2
+
3
+ ## 0.5.0 _(June 25, 2018)_
4
+ - Removed use of invasive views in preference of sub-selects
5
+ - Dropped support for Ruby 2.0, 2.1, 2.2
6
+ - Dropped support for Rails 4.1
7
+ - Default `active_record-framing` options using `Proc`
8
+
9
+ ## 0.4.0 _(Never Released)_
10
+ - Specs for Rails 4.0-5.1
11
+ - Uses `combustion` gem for cleaner and more comprehensive testing
12
+ - Added badges to ReadMe
13
+ - Using `:prepend` to leverage ancestry chain
14
+ - Add logger for internal use
15
+ - DRYd up init code
16
+ - Removed partially supported features
17
+ - Added DSL in migrations/schema for adding `active_record-framing` timestamps to tables
18
+
19
+ ## 0.3.0 _(May 10, 2017)_
20
+ - Add specs
21
+ - Clean up dependencies
22
+ - Auto-init models after installing views
23
+ - Remove chained `create` methods
24
+
25
+ ## 0.2.6 _(April 06, 2017)_
26
+ - Add warning when no DB connection present
27
+
28
+ ## 0.2.5 _(March 28, 2017)_
29
+ - Extract injections to `.load` method
30
+
31
+ ## 0.2.4 _(February 03, 2017)_
32
+ - Use `becomes` to mask `::All` etc classes
33
+
34
+ ## 0.2.3 _(February 03, 2017)_
35
+ - Chain `create!` method to work properly
36
+
37
+ ## 0.2.2 _(February 03, 2017)_
38
+ - Chain `create` method to work properly
39
+
40
+ ## 0.2.1 _(February 03, 2017)_
41
+ - More reliable table name handling
42
+ - Changed API for installing views (e.g. `destroy_deleted_view`, `uninstall_deleted_view`)
43
+
44
+ ## 0.1.1 _(January 31, 2017)_
45
+ - Added instructions to readme
46
+ - Fixes stack-too-deep edge-case (by moving to `:include` over `:prepend`)
47
+
48
+ ## 0.1.0 _(January 30, 2017)_
49
+ - Renames primary table to `model_name/all`
50
+ - Creates views for each model using `active_record-framing`
51
+ - `model_name/deleted`
52
+ - `model_name/present`
53
+ - Classes created to read from views (`::All`, `::Present`, `::Deleted`)
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Dale Stevens
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
13
+ all 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
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ [![License ](https://img.shields.io/github/license/TwilightCoders/active_record-framing.svg)]()
2
+ [![Version ](https://img.shields.io/gem/v/active_record-framing.svg)](https://rubygems.org/gems/active_record-framing)
3
+ [![Build Status](https://travis-ci.org/TwilightCoders/active_record-framing.svg)](https://travis-ci.org/TwilightCoders/active_record-framing)
4
+ [![Maintenence ](https://api.codeclimate.com/v1/badges/762cdcd63990efa768b0/maintainability)](https://codeclimate.com/github/TwilightCoders/active_record-framing/maintainability)
5
+ [![Coverage ](https://codeclimate.com/github/TwilightCoders/active_record-framing/badges/coverage.svg)](https://codeclimate.com/github/TwilightCoders/active_record-framing/coverage)
6
+ [![Dependencies](https://img.shields.io/librariesio/github/twilightcoders/active_record-framing.svg)](https://depfu.com/github/TwilightCoders/active_record-framing)
7
+
8
+ # ActiveRecord::Framing
9
+
10
+ Works similar to `scopes`. Rather than modifying the where clause of the `ActiveRecord::Relation`, it creates a common table expression (CTE) to be applied upon execution.
11
+
12
+ Unlike scopes, they do not affect the values of attributes upon creation.
13
+
14
+ ## Requirements
15
+
16
+ - Ruby 2.3+
17
+ - ActiveRecord 4.2+
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'active_record-framing'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ $ bundle
30
+
31
+ Or install it yourself as:
32
+
33
+ $ gem install active_record-framing
34
+
35
+ ## Usage
36
+
37
+ Any `ActiveRecord::Base` descendant has access to two additional methods: `frame` and `default_frame`.
38
+
39
+ ```ruby
40
+ class User < ActiveRecord::Base
41
+ default_frame { where(active: true) }
42
+ # ...
43
+ end
44
+ ```
45
+ Afterwards, `User.all.to_sql` yields
46
+ ```sql
47
+ WITH "users" AS
48
+ (SELECT "users".* FROM "users" WHERE "users"."active" = true)
49
+ SELECT "users".* FROM "users"
50
+ ```
51
+
52
+ ```ruby
53
+ class Admin < User
54
+ default_frame('admins') { where(kind: 1) }
55
+ # ...
56
+ end
57
+ ```
58
+
59
+ Afterwards, `Admin.all.to_sql` yields
60
+ ```sql
61
+ WITH "admins" AS
62
+ (SELECT "users".* )
63
+ ```
64
+
65
+ If you're starting with a brand-new table, the existing `timestamps` DSL has been extended to accept `deleted_at: true` as an option, for convenience. Or you can do it seperately as shown above.
66
+
67
+ ```ruby
68
+ class CreatCommentsTable < ActiveRecord::Migration
69
+
70
+ def change
71
+ create_table :comments do |t|
72
+ # ...
73
+ # to the `timestamps` DSL
74
+ t.timestamps null: false, deleted_at: true
75
+ end
76
+ end
77
+
78
+ end
79
+ ```
80
+
81
+ ## Development
82
+
83
+ After checking out the repo, run `bundle` to install dependencies. Then, run `bundle exec rspec` to run the tests.
84
+
85
+ ## Contributing
86
+
87
+ Bug reports and pull requests are welcome on GitHub at https://github.com/TwilightCoders/active_record-framing. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
88
+
89
+ ## License
90
+
91
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,21 @@
1
+ module ActiveRecord
2
+ module Framing
3
+ module AttributeMethods
4
+ extend ActiveSupport::Concern
5
+
6
+ BLACKLISTED_CLASS_CONSTS = %w(unframed)
7
+
8
+ module ClassMethods
9
+ # A class const is 'dangerous' if it is already defined by Active Record, but
10
+ # not by any ancestors. (So 'All' is not dangerous but 'Frameless' is.)
11
+ def dangerous_class_const?(const_name)
12
+ BLACKLISTED_CLASS_CONSTS.include?(const_name.to_s) || class_const_defined_within?(const_name, Base)
13
+ end
14
+
15
+ def class_const_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
16
+ klass.const_defined?(name) || superklass.const_defined?(name)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/per_thread_registry"
4
+
5
+ module ActiveRecord
6
+ module Framing
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ include Default
11
+ include Named
12
+ include AttributeMethods
13
+ end
14
+
15
+ module ClassMethods # :nodoc:
16
+ def current_frame
17
+ FrameRegistry.value_for(:current_frame, self)
18
+ end
19
+
20
+ def current_frame=(frame)
21
+ FrameRegistry.set_value_for(:current_frame, self, frame)
22
+ end
23
+ end
24
+
25
+ # This class stores the +:current_frame+ and +:ignore_default_frame+ values
26
+ # for different classes. The registry is stored as a thread local, which is
27
+ # accessed through +FrameRegistry.current+.
28
+ #
29
+ # This class allows you to store and get the frame values on different
30
+ # classes and different types of frames. For example, if you are attempting
31
+ # to get the current_frame for the +Board+ model, then you would use the
32
+ # following code:
33
+ #
34
+ # registry = ActiveRecord::Framing::FrameRegistry
35
+ # registry.set_value_for(:current_frame, Board, some_new_frame)
36
+ #
37
+ # Now when you run:
38
+ #
39
+ # registry.value_for(:current_frame, Board)
40
+ #
41
+ # You will obtain whatever was defined in +some_new_frame+. The #value_for
42
+ # and #set_value_for methods are delegated to the current FrameRegistry
43
+ # object, so the above example code can also be called as:
44
+ #
45
+ # ActiveRecord::Framing::FrameRegistry.set_value_for(:current_frame,
46
+ # Board, some_new_frame)
47
+ class FrameRegistry # :nodoc:
48
+ extend ActiveSupport::PerThreadRegistry
49
+
50
+ VALID_SCOPE_TYPES = [:current_frame, :ignore_default_frame]
51
+
52
+ def initialize
53
+ @registry = Hash.new { |hash, key| hash[key] = {} }
54
+ end
55
+
56
+ # Obtains the value for a given +frame_type+ and +model+.
57
+ def value_for(frame_type, model)
58
+ raise_invalid_frame_type!(frame_type)
59
+ return @registry[frame_type][model.name]
60
+ end
61
+
62
+ # def value_for(frame_type, model, skip_inherited_frame = false)
63
+ # raise_invalid_frame_type!(frame_type)
64
+ # return @registry[frame_type][model.name] if skip_inherited_frame
65
+ # klass = model
66
+ # base = model.base_class
67
+ # while klass <= base
68
+ # value = @registry[frame_type][klass.name]
69
+ # return value if value
70
+ # klass = klass.superclass
71
+ # end
72
+ # end
73
+
74
+ # Sets the +value+ for a given +frame_type+ and +model+.
75
+ def set_value_for(frame_type, model, value)
76
+ raise_invalid_frame_type!(frame_type)
77
+ @registry[frame_type][model.name] = value
78
+ end
79
+
80
+ private
81
+
82
+ def raise_invalid_frame_type!(frame_type)
83
+ if !VALID_SCOPE_TYPES.include?(frame_type)
84
+ raise ArgumentError, "Invalid frame type '#{frame_type}' sent to the registry. Frame types must be included in VALID_SCOPE_TYPES"
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Framing
5
+ module Default
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Stores the default frame for the class.
10
+ class_attribute :default_frames, instance_writer: false, instance_predicate: false
11
+ class_attribute :default_frame_override, instance_writer: false, instance_predicate: false
12
+
13
+ self.default_frames = []
14
+ self.default_frame_override = nil
15
+ end
16
+
17
+ module ClassMethods
18
+ # Returns a frame for the model without the previously set frames.
19
+ #
20
+ # class Post < ActiveRecord::Base
21
+ # def self.default_frame
22
+ # where(published: true)
23
+ # end
24
+ # end
25
+ #
26
+ # Post.all # Fires "WITH posts AS (SELECT * FROM posts WHERE published = true) SELECT * FROM posts"
27
+ # Post.unframed.all # Fires "SELECT * FROM posts"
28
+ # Post.where(published: false).unframed.all # Fires "SELECT * FROM posts"
29
+ #
30
+ # This method also accepts a block. All queries inside the block will
31
+ # not use the previously set frames.
32
+ #
33
+ # Post.unframed {
34
+ # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
35
+ # }
36
+ def unframed
37
+ block_given? ? relation.framing { yield } : relation
38
+ end
39
+
40
+ # Are there attributes associated with this frame?
41
+ def frame_attributes? # :nodoc:
42
+ super || default_frames.any? || respond_to?(:default_frame)
43
+ end
44
+
45
+ def before_remove_const #:nodoc:
46
+ self.current_frame = nil
47
+ end
48
+
49
+ def ignore_default_frame?
50
+ FrameRegistry.value_for(:ignore_default_frame, base_class)
51
+ end
52
+
53
+ private
54
+
55
+ # Use this macro in your model to set a default frame for all operations on
56
+ # the model.
57
+ #
58
+ # class Article < ActiveRecord::Base
59
+ # default_frame { where(published: true) }
60
+ # end
61
+ #
62
+ # Article.all # => # Fires "WITH articles AS (SELECT * FROM articles WHERE published = true) SELECT * FROM articles"
63
+ #
64
+ # The #default_frame is not applied while updating/creating/building a record.
65
+ #
66
+ # Article.new.published # => nil
67
+ # Article.create.published # => nil
68
+ # Article.first.update(name: 'A Tale of Two Cities').published # => nil
69
+ #
70
+ # (You can also pass any object which responds to +call+ to the
71
+ # +default_frame+ macro, and it will be called when building the
72
+ # default frame.)
73
+ #
74
+ # If you use multiple #default_frame declarations in your model then
75
+ # they will be merged together:
76
+ #
77
+ # class Article < ActiveRecord::Base
78
+ # default_frame { where(published: true) }
79
+ # default_frame { where(rating: 'G') }
80
+ # end
81
+ #
82
+ # Article.all # => WITH articles AS (SELECT * FROM articles WHERE published = true AND rating = 'G') SELECT * FROM articles
83
+ #
84
+ # This is also the case with inheritance and module includes where the
85
+ # parent or module defines a #default_frame and the child or including
86
+ # class defines a second one.
87
+ #
88
+ # If you need to do more complex things with a default frame, you can
89
+ # alternatively define it as a class method:
90
+ #
91
+ # class Article < ActiveRecord::Base
92
+ # def self.default_frame
93
+ # # Should return a frame, you can call 'super' here etc.
94
+ # end
95
+ # end
96
+ def default_frame(frame = nil) # :doc:
97
+ frame = Proc.new if block_given?
98
+
99
+ if frame.is_a?(Relation) || !frame.respond_to?(:call)
100
+ raise ArgumentError,
101
+ "Support for calling #default_frame without a block is removed. For example instead " \
102
+ "of `default_frame where(color: 'red')`, please use " \
103
+ "`default_frame { where(color: 'red') }`. (Alternatively you can just redefine " \
104
+ "self.default_frame.)"
105
+ end
106
+
107
+ self.default_frames += [frame]
108
+ end
109
+
110
+ def build_default_frame(base_rel = nil)
111
+ return if abstract_class?
112
+
113
+ if default_frame_override.nil?
114
+ self.default_frame_override = !Base.is_a?(method(:default_frame).owner)
115
+ end
116
+
117
+ if default_frame_override
118
+ # The user has defined their own default frame method, so call that
119
+ # evaluate_default_frame do
120
+ # if frame = default_frame
121
+ # (base_rel ||= relation).merge!(frame)
122
+ # end
123
+ # end
124
+ warn "come back to me!"
125
+ elsif default_frames.any?
126
+ # cte_table = arel_table
127
+ cte_table = Arel::Table.new(table_name)
128
+
129
+ evaluate_default_frame do
130
+ # Create CTE here
131
+
132
+ cte_relation = default_frames.inject(relation) do |default_frame, frame|
133
+ frame = frame.respond_to?(:to_proc) ? frame : frame.method(:call)
134
+ default_frame.merge!(relation.instance_exec(&frame))
135
+ end
136
+
137
+ base_rel ||= relation
138
+ base_rel.frame!(Arel::Nodes::As.new(Arel::Table.new(table_name), cte_relation.arel))# if cte_relation
139
+ end
140
+ end
141
+ end
142
+
143
+ def ignore_default_frame=(ignore)
144
+ FrameRegistry.set_value_for(:ignore_default_frame, base_class, ignore)
145
+ end
146
+
147
+ # The ignore_default_frame flag is used to prevent an infinite recursion
148
+ # situation where a default frame references a frame which has a default
149
+ # frame which references a frame...
150
+ def evaluate_default_frame
151
+ return if ignore_default_frame?
152
+ self.ignore_default_frame = true
153
+ yield
154
+ ensure
155
+ self.ignore_default_frame = false
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,239 @@
1
+ require 'delegate'
2
+
3
+ module ActiveRecord
4
+ # = Active Record \Named \Frames
5
+ module Framing
6
+ module Named
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ # Returns an ActiveRecord::Relation frame object.
11
+ #
12
+ # posts = Post.all
13
+ # posts.size # Fires "select count(*) from posts" and returns the count
14
+ # posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
15
+ #
16
+ # fruits = Fruit.all
17
+ # fruits = fruits.where(color: 'red') if options[:red_only]
18
+ # fruits = fruits.limit(10) if limited?
19
+ #
20
+ # You can define a frame that applies to all finders using
21
+ # {default_frame}[rdoc-ref:Framing::Default::ClassMethods#default_frame].
22
+ # def all
23
+ # super.tap do |rel|
24
+ # if frame = framed_all
25
+ # rel.with(frame)
26
+ # end
27
+ # end
28
+ # end
29
+
30
+ # alias this method?, framed_all
31
+ # def all
32
+ # # if current_scope
33
+ # # current_scope.clone
34
+ # # else
35
+ # # default_scoped
36
+ # # end
37
+ # if current_frame
38
+ # puts "#{name} has a current_frame: #{current_frame.to_sql}"
39
+ # super.frame(current_frame.clone)
40
+ # else
41
+ # puts "#{name} is using a default_frame: #{default_framed.to_sql}" if default_framed
42
+ # super.frame(default_framed)
43
+ # end
44
+ # end
45
+
46
+ def all
47
+ framed_all(super)
48
+ end
49
+
50
+ def framed_all(rel)
51
+ if current_frame = self.current_frame
52
+ if self == current_frame.klass
53
+ current_frame.clone
54
+ else
55
+ rel.merge!(current_frame)
56
+ end
57
+ else
58
+ default_framed(rel)
59
+ end
60
+ end
61
+
62
+ def const_missing(const_name)
63
+ registered_frames[const_name]&.call() || super
64
+ end
65
+
66
+ def registered_frames
67
+ @registered_frames ||= {}
68
+ end
69
+
70
+ def frame_for_association(frame = relation) # :nodoc:
71
+ current_frame = self.current_frame
72
+
73
+ if current_frame && current_frame.empty_frame?
74
+ frame
75
+ else
76
+ default_framed(frame)
77
+ end
78
+ end
79
+
80
+ # def default_framed(frame = relation) # :nodoc:
81
+ def default_framed(frame = nil) # :nodoc:
82
+ !ignore_default_frame? && build_default_frame(frame) || frame
83
+ end
84
+
85
+ # Adds a class method for retrieving and querying objects.
86
+ # The method is intended to return an ActiveRecord::Relation
87
+ # object, which is composable with other frames.
88
+ # If it returns +nil+ or +false+, an
89
+ # {all}[rdoc-ref:Framing::Named::ClassMethods#all] frame is returned instead.
90
+ #
91
+ # A \frame represents a narrowing of a database query, such as
92
+ # <tt>where(color: :red).select('shirts.*').includes(:washing_instructions)</tt>.
93
+ #
94
+ # class Shirt < ActiveRecord::Base
95
+ # frame :red, -> { where(color: 'red') }
96
+ # frame :dry_clean_only, -> { joins(:washing_instructions).where('washing_instructions.dry_clean_only = ?', true) }
97
+ # end
98
+ #
99
+ # The above calls to #frame define class methods <tt>Shirt.red</tt> and
100
+ # <tt>Shirt::DryCleanOnly</tt>. <tt>Shirt::Red</tt>, in effect,
101
+ # represents the query <tt>Shirt.where(color: 'red')</tt>.
102
+ #
103
+ # You should always pass a callable object to the frames defined
104
+ # with #frame. This ensures that the frame is re-evaluated each
105
+ # time it is called.
106
+ #
107
+ # Note that this is simply 'syntactic sugar' for defining an actual
108
+ # class method:
109
+ #
110
+ # class Shirt < ActiveRecord::Base
111
+ # def self.red
112
+ # where(color: 'red')
113
+ # end
114
+ # end
115
+ #
116
+ # Unlike <tt>Shirt.find(...)</tt>, however, the object returned by
117
+ # <tt>Shirt.red</tt> is not an Array but an ActiveRecord::Relation,
118
+ # which is composable with other frames; it resembles the association object
119
+ # constructed by a {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
120
+ # declaration. For instance, you can invoke <tt>Shirt.red.first</tt>, <tt>Shirt.red.count</tt>,
121
+ # <tt>Shirt.red.where(size: 'small')</tt>. Also, just as with the
122
+ # association objects, named \frames act like an Array, implementing
123
+ # Enumerable; <tt>Shirt.red.each(&block)</tt>, <tt>Shirt.red.first</tt>,
124
+ # and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if
125
+ # <tt>Shirt.red</tt> really was an array.
126
+ #
127
+ # These named \frames are composable. For instance,
128
+ # <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are
129
+ # both red and dry clean only. Nested finds and calculations also work
130
+ # with these compositions: <tt>Shirt.red.dry_clean_only.count</tt>
131
+ # returns the number of garments for which these criteria obtain.
132
+ # Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
133
+ #
134
+ # All frames are available as class methods on the ActiveRecord::Base
135
+ # descendant upon which the \frames were defined. But they are also
136
+ # available to {has_many}[rdoc-ref:Associations::ClassMethods#has_many]
137
+ # associations. If,
138
+ #
139
+ # class Person < ActiveRecord::Base
140
+ # has_many :shirts
141
+ # end
142
+ #
143
+ # then <tt>elton.shirts.red.dry_clean_only</tt> will return all of
144
+ # Elton's red, dry clean only shirts.
145
+ #
146
+ # \Named frames can also have extensions, just as with
147
+ # {has_many}[rdoc-ref:Associations::ClassMethods#has_many] declarations:
148
+ #
149
+ # class Shirt < ActiveRecord::Base
150
+ # frame :red, -> { where(color: 'red') } do
151
+ # def dom_id
152
+ # 'red_shirts'
153
+ # end
154
+ # end
155
+ # end
156
+ #
157
+ # Frames cannot be used while creating/building a record.
158
+ #
159
+ # class Article < ActiveRecord::Base
160
+ # frame :published, -> { where(published: true) }
161
+ # end
162
+ #
163
+ # Article.published.new.published # => nil
164
+ # Article.published.create.published # => nil
165
+ #
166
+ # \Class methods on your model are automatically available
167
+ # on frames. Assuming the following setup:
168
+ #
169
+ # class Article < ActiveRecord::Base
170
+ # frame :published, -> { where(published: true) }
171
+ # frame :featured, -> { where(featured: true) }
172
+ #
173
+ # def self.latest_article
174
+ # order('published_at desc').first
175
+ # end
176
+ #
177
+ # def self.titles
178
+ # pluck(:title)
179
+ # end
180
+ # end
181
+ #
182
+ # We are able to call the methods like this:
183
+ #
184
+ # Article.published.featured.latest_article
185
+ # Article.featured.titles
186
+ def frame(frame_name, body, &block)
187
+ unless body.respond_to?(:call)
188
+ raise ArgumentError, "The frame body needs to be callable."
189
+ end
190
+
191
+ constant = frame_name.to_s.classify.to_sym
192
+ arel_tn = "#{frame_name}/#{self.table_name}"
193
+
194
+ the_frame = body.respond_to?(:to_proc) ? body : body.method(:call)
195
+ cte_relation = relation.merge!(relation.instance_exec(&the_frame) || relation)
196
+
197
+ # self.const_set constant, Class.new(DelegateClass(self)) do |klass|
198
+ delegator = self.name.to_sym
199
+ self.const_set(constant, self.dup).class_eval do |klass|
200
+ extend SingleForwardable
201
+ def_delegator delegator, :type_caster
202
+ def_delegator delegator, :table_name
203
+
204
+ klass.default_frames = []
205
+
206
+ @arel_table = klass.arel_table.dup.tap do |at|
207
+ at.name = arel_tn
208
+ end
209
+
210
+ klass.current_frame = build_frame(cte_relation, &block)
211
+ end
212
+
213
+ if dangerous_class_const?(constant)
214
+ raise ArgumentError, "You tried to define a frame named \"#{constant}\" " \
215
+ "on the model \"#{self.constant}\", but Active Record already defined " \
216
+ "a class method with the same name."
217
+ end
218
+
219
+ end
220
+
221
+ def build_frame(frame, &block)
222
+ extension = Module.new(&block) if block
223
+ relation.frame!(Arel::Nodes::As.new(arel_table, frame.arel)).tap do |rel|
224
+ rel.extending!(extension) if extension
225
+ end
226
+ end
227
+
228
+ private
229
+
230
+ def valid_frame_name?(name)
231
+ if respond_to?(name, true) && logger
232
+ logger.warn "Creating frame :#{name}. " \
233
+ "Overwriting existing method #{self.name}.#{name}."
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,119 @@
1
+ module ActiveRecord
2
+ module Framing
3
+ module QueryMethods
4
+
5
+ if ::ActiveRecord.version >= Gem::Version.new("5.1") # 5.1+
6
+ def frames_values
7
+ get_value(:frames)
8
+ end
9
+ def frames_values=(value)
10
+ set_value(:frames, value)
11
+ end
12
+ ::ActiveRecord::Relation::DEFAULT_VALUES[:frames] = ::ActiveRecord::Relation::FROZEN_EMPTY_HASH
13
+ elsif ::ActiveRecord.version >= Gem::Version.new("5.0") # 5.0+
14
+ def frames_values
15
+ @values[:frames] || ::ActiveRecord::Relation::FROZEN_EMPTY_HASH
16
+ end
17
+ def frames_values=(values)
18
+ assert_mutability!
19
+ @values[:frames] = values
20
+ end
21
+ elsif ::ActiveRecord.version >= Gem::Version.new("4.2") # 4.2+
22
+ def frames_values
23
+ @values[:frames] || {}
24
+ end
25
+ def frames_values=(values)
26
+ raise ImmutableRelation if @loaded
27
+ check_cached_relation
28
+ @values[:frames] = values
29
+ end
30
+ else
31
+ raise NotImplementedError, "ActiveRecord::Framing does not support Rails #{::ActiveRecord.version}"
32
+ end
33
+
34
+ def from!(value, subquery_name = nil) # :nodoc:
35
+ super.tap do |rel|
36
+ frames_values = frames_values.merge(value.frames_values) if value.is_a?(::ActiveRecord::Relation)
37
+ end
38
+ end
39
+
40
+ def frame(value)
41
+ spawn.frame!(value)
42
+ end
43
+
44
+ def frame!(value)
45
+ if key = frame_key(value)
46
+ self.frames_values = self.frames_values.merge(key => value)
47
+ end
48
+ self
49
+ end
50
+
51
+ # Removes an unwanted relation that is already defined on a chain of relations.
52
+ # This is useful when passing around chains of relations and would like to
53
+ # modify the relations without reconstructing the entire chain.
54
+ #
55
+ # User.order('email DESC').unframe(:order) == User.all
56
+ #
57
+ # The method arguments are symbols which correspond to the names of the methods
58
+ # which should be unframed. The valid arguments are given in VALID_UNSCOPING_VALUES.
59
+ # The method can also be called with multiple arguments. For example:
60
+ #
61
+ # User.order('email DESC').select('id').where(name: "John")
62
+ # .unframe(:order, :select, :where) == User.all
63
+ #
64
+ # One can additionally pass a hash as an argument to unframe specific +:where+ values.
65
+ # This is done by passing a hash with a single key-value pair. The key should be
66
+ # +:where+ and the value should be the where value to unframe. For example:
67
+ #
68
+ # User.where(name: "John", active: true).unframe(where: :name)
69
+ # == User.where(active: true)
70
+ #
71
+ # This method is similar to #except, but unlike
72
+ # #except, it persists across merges:
73
+ #
74
+ # User.order('email').merge(User.except(:order))
75
+ # == User.order('email')
76
+ #
77
+ # User.order('email').merge(User.unframe(:order))
78
+ # == User.all
79
+ #
80
+ # This means it can be used in association definitions:
81
+ #
82
+ # has_many :comments, -> { unframe(where: :trashed) }
83
+ #
84
+ # def unframe(*args)
85
+ # check_if_method_has_arguments!(:unframe, args)
86
+ # spawn.unframe!(*args)
87
+ # end
88
+
89
+ # def unframe!(*args) # :nodoc:
90
+ # args.flatten!
91
+ # self.unframe_values += args
92
+
93
+ # args.each do |frame|
94
+ # case frame
95
+ # when Symbol
96
+ # frame = :left_outer_joins if frame == :left_joins
97
+ # if !VALID_UNSCOPING_VALUES.include?(frame)
98
+ # raise ArgumentError, "Called unframe() with invalid unframing argument ':#{frame}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
99
+ # end
100
+ # set_value(frame, DEFAULT_VALUES[frame])
101
+ # when Hash
102
+ # frame.each do |key, target_value|
103
+ # if key != :where
104
+ # raise ArgumentError, "Hash arguments in .unframe(*args) must have :where as the key."
105
+ # end
106
+
107
+ # target_values = Array(target_value).map(&:to_s)
108
+ # self.where_clause = where_clause.except(*target_values)
109
+ # end
110
+ # else
111
+ # raise ArgumentError, "Unrecognized framing: #{args.inspect}. Use .unframe(where: :attribute_name) or .unframe(:order), for example."
112
+ # end
113
+ # end
114
+
115
+ # self
116
+ # end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,19 @@
1
+ require 'rails/railtie'
2
+ require 'active_record/framing/core_extension'
3
+ require 'active_record/framing/query_methods'
4
+ require 'active_record/framing/spawn_methods'
5
+ require 'active_record/framing/attribute_methods'
6
+ require 'active_record/framing/relation'
7
+
8
+ module ActiveRecord::Framing
9
+ class Railtie < Rails::Railtie
10
+ initializer 'active_record-framing.load' do |_app|
11
+ ActiveSupport.on_load(:active_record) do
12
+ ::ActiveRecord::Base.include(ActiveRecord::Framing)
13
+ ::ActiveRecord::Relation.prepend(ActiveRecord::Framing::Relation)
14
+ ::ActiveRecord::Relation.include(ActiveRecord::Framing::QueryMethods)
15
+ ::ActiveRecord::Relation.prepend(ActiveRecord::Framing::SpawnMethods)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,162 @@
1
+ module ActiveRecord
2
+ # = Active Record \Relation
3
+ module Framing
4
+ module Relation
5
+
6
+ # def arel_without_frames
7
+ # klass.ignore_default_frame
8
+ # Thread.currently(:without_frames, true) do
9
+ # @arel_without_frames ||= build_arel_without_frames
10
+ # end
11
+ # end
12
+
13
+ # def build_arel_without_frames
14
+ # @arel, old = nil, @arel
15
+ # arel
16
+ # ensure
17
+ # @arel = old
18
+ # end
19
+
20
+ def build_arel(*)
21
+ super.tap do |ar|
22
+ unless ignore_default_frame?
23
+ # alias_tracker.aliased_table_for(
24
+ # reflection.table_name,
25
+ # table_alias_for(reflection, parent, reflection != node.reflection),
26
+ # reflection.klass.type_caster
27
+ # )
28
+ build_frames(ar)
29
+
30
+ frames_values.each do |k,v|
31
+ puts "#{k} => #{v.to_sql}"
32
+ end
33
+
34
+ ar.with(*frames_values.values) if frames_values.any?
35
+ end
36
+ end
37
+ end
38
+
39
+ # This is all very unfortunate (rails 4.2):
40
+ # ActiveRecord, in it's infinite wisdom, has decided
41
+ # to create JoinDependency objects with arel_tables using a
42
+ # generic engine (ActiveRecord::Base) as opposed to that of
43
+ # the driving class. For example:
44
+ #
45
+ # #<Arel::Nodes::InnerJoin:0x00007f974e6eb008
46
+ # @left=
47
+ # #<Arel::Table:0x00007f97508f42f8
48
+ # @aliases=[],
49
+ # @columns=nil,
50
+ # @engine=ActiveRecord::Base, <=== Problem, should be `User`
51
+ # @name="users",
52
+ # @primary_key=nil,
53
+ # @table_alias=nil>,
54
+ # NOTE: In Rails 5.2 (at least) we could use the InnerJoin.left.type_caster
55
+ def build_frames(manager)
56
+ join_names = manager.join_sources.collect do |source|
57
+ source.left.name.to_s # TODO: Need to_s?
58
+ end
59
+
60
+ # scopes = klass.reflections.slice(*join_names).values.inject(Hash.new) do |collector, assoc|
61
+ # NOTE: We cannot early exclude associations because some associations are different from their table names
62
+ klass.reflect_on_all_associations.each do |assoc|
63
+ # collector.merge(assoc.klass.default_scopes) if join_names.include?(assoc.table_name)
64
+ # (collector[assoc.klass] ||= Set.new).merge(assoc.klass.default_scopes) if join_names.include?(assoc.table_name) && assoc.klass.default_scopes.any?
65
+ if join_names.include?(assoc.table_name) && assoc.klass.default_frames.any? && assoc_default_frame = assoc.klass.send(:build_default_frame)
66
+ merge!(assoc_default_frame)
67
+ # collector[assoc_default_frame.table_name] ||= assoc_default_frame
68
+ end
69
+
70
+ # collector
71
+ end
72
+ end
73
+
74
+ def frame_key(cte)
75
+ case cte
76
+ when ActiveRecord::Relation
77
+ cte.table.name
78
+ when Arel::Nodes::As
79
+ cte.left.name
80
+ when String
81
+ cte
82
+ else
83
+ nil
84
+ end
85
+ end
86
+
87
+ # def reframe(*args) # :nodoc:
88
+ # args.compact!
89
+ # args.flatten!
90
+ # # binding.pry
91
+ # self
92
+ # end
93
+
94
+ # def reframe!(*args) # :nodoc:
95
+ # args.flatten!
96
+ # self.unframe_values += args
97
+
98
+ # args.each do |frame|
99
+ # case frame
100
+ # when Symbol
101
+ # frame = :left_outer_joins if frame == :left_joins
102
+ # if !VALID_UNSCOPING_VALUES.include?(frame)
103
+ # raise ArgumentError, "Called unframe() with invalid unframing argument ':#{frame}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
104
+ # end
105
+ # set_value(frame, DEFAULT_VALUES[frame])
106
+ # when Hash
107
+ # frame.each do |key, target_value|
108
+ # if key != :where
109
+ # raise ArgumentError, "Hash arguments in .unframe(*args) must have :where as the key."
110
+ # end
111
+
112
+ # target_values = Array(target_value).map(&:to_s)
113
+ # self.where_clause = where_clause.except(*target_values)
114
+ # end
115
+ # else
116
+ # raise ArgumentError, "Unrecognized framing: #{args.inspect}. Use .unframe(where: :attribute_name) or .unframe(:order), for example."
117
+ # end
118
+ # end
119
+
120
+ # self
121
+ # end
122
+
123
+ # Frame all queries to the current frame.
124
+ #
125
+ # Comment.where(post_id: 1).framing do
126
+ # Comment.first
127
+ # end
128
+ # # => WITH "comments" AS (
129
+ # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1
130
+ # # => ) SELECT "comments".* FROM "comments" ORDER BY "comments"."id" ASC LIMIT 1
131
+ #
132
+ # Please check unframed if you want to remove all previous frames (including
133
+ # the default_frame) during the execution of a block.
134
+ def framing
135
+ previous, klass.current_frame = klass.current_frame, self unless @delegate_to_klass
136
+ yield
137
+ ensure
138
+ klass.current_frame = previous unless @delegate_to_klass
139
+ end
140
+
141
+ def scoping
142
+ framing { super }
143
+ end
144
+
145
+ # def _exec_frame(*args, &block) # :nodoc:
146
+ # @delegate_to_klass = true
147
+ # instance_exec(*args, &block) || self
148
+ # ensure
149
+ # @delegate_to_klass = false
150
+ # end
151
+
152
+ # def frame_for_create
153
+ # where_values_hash.merge!(create_with_value.stringify_keys)
154
+ # end
155
+
156
+ # def empty_frame? # :nodoc:
157
+ # @values == klass.unframed.values
158
+ # end
159
+
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,13 @@
1
+ module ActiveRecord
2
+ module Framing
3
+ module SpawnMethods
4
+
5
+ def merge!(other) # :nodoc:
6
+ super.tap do |rel|
7
+ rel.frames_values = rel.frames_values.merge(other.frames_values)
8
+ end
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Framing
3
+ VERSION = "0.1.0-1"
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ require 'active_record/framing/default'
2
+ require 'active_record/framing/named'
3
+ require 'active_record/framing/railtie' if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_record-framing
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre.1
5
+ platform: ruby
6
+ authors:
7
+ - Dale Stevens
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '6'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '4.2'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '6'
33
+ - !ruby/object:Gem::Dependency
34
+ name: pg
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: pry-byebug
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '3'
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ - !ruby/object:Gem::Dependency
76
+ name: rake
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '12.0'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '12.0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: combustion
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '0.7'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '0.7'
103
+ description: Allows for larger level scoping (framing) that affect complicated queries
104
+ more holistically
105
+ email:
106
+ - dale@twilightcoders.net
107
+ executables: []
108
+ extensions: []
109
+ extra_rdoc_files: []
110
+ files:
111
+ - CHANGELOG.md
112
+ - LICENSE
113
+ - README.md
114
+ - lib/active_record/framing.rb
115
+ - lib/active_record/framing/attribute_methods.rb
116
+ - lib/active_record/framing/core_extension.rb
117
+ - lib/active_record/framing/default.rb
118
+ - lib/active_record/framing/named.rb
119
+ - lib/active_record/framing/query_methods.rb
120
+ - lib/active_record/framing/railtie.rb
121
+ - lib/active_record/framing/relation.rb
122
+ - lib/active_record/framing/spawn_methods.rb
123
+ - lib/active_record/framing/version.rb
124
+ homepage: https://github.com/TwilightCoders/active_record-framing
125
+ licenses:
126
+ - MIT
127
+ metadata:
128
+ allowed_push_host: https://rubygems.org
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '2.3'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">"
141
+ - !ruby/object:Gem::Version
142
+ version: 1.3.1
143
+ requirements: []
144
+ rubygems_version: 3.0.3
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: Provides larger level scopes (frames) through the use of common table expressions.
148
+ test_files: []