passive_columns 0.1.2
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/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/lib/passive_columns/active_record_association_builder_extension.rb +32 -0
- data/lib/passive_columns/active_record_relation_extension.rb +23 -0
- data/lib/passive_columns/loader.rb +46 -0
- data/lib/passive_columns/railtie.rb +27 -0
- data/lib/passive_columns/version.rb +5 -0
- data/lib/passive_columns.rb +112 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: a9d9611e28f0d1a9a3bcb4c048158d3757294c0e13e2b3cf17f1912c62bf0886
|
|
4
|
+
data.tar.gz: 7650f8e0c2da1e72d9ddeb29b3be36c900ce2e3b887a59affcb13b6da531a270
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 103734dbe62aad0bf4a956c9fafa6ac69d0f92f4f57f673b3cff7329f6b854a02d69d561442b5a812d2c2416b426f2f5a8bd072f590b52a70b0a8b4023dc86bf
|
|
7
|
+
data.tar.gz: d7c56d6635c3537c472cd5286a29735c9b6e0ccd06d515a68de64367e16a58ee24c32f540e2f76635c17091a990c08ca8a3f46c9b49b3bfb888385876eb69877
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Dmitry Golovin
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# PASSIVE_COLUMNS
|
|
2
|
+
Short description and motivation.
|
|
3
|
+
|
|
4
|
+
## Usage
|
|
5
|
+
How to use my plugin.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
Add this line to your application's Gemfile:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
gem "passive_columns"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
And then execute:
|
|
15
|
+
```bash
|
|
16
|
+
$ bundle
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
```bash
|
|
21
|
+
$ gem install passive_columns
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Contributing
|
|
25
|
+
Contribution directions go here.
|
|
26
|
+
|
|
27
|
+
## License
|
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PassiveColumns
|
|
4
|
+
# This module is used to extend the ActiveRecord::Associations::Builder::Association class
|
|
5
|
+
# to add a proc with default scope to the association if there is no proc defined.
|
|
6
|
+
module ActiveRecordAssociationBuilderExtension
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def create_reflection(*)
|
|
11
|
+
super.tap do |res|
|
|
12
|
+
next if res.polymorphic?
|
|
13
|
+
next unless _klass_has_passive_columns(res) && res.scope.nil?
|
|
14
|
+
|
|
15
|
+
default_relation = -> { unscoped }
|
|
16
|
+
res.instance_variable_set(:@scope, proc { instance_exec(&default_relation) })
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Check if the association class has passive columns
|
|
21
|
+
# @param [ActiveRecord::Reflection::AssociationReflection] res
|
|
22
|
+
def _klass_has_passive_columns(res)
|
|
23
|
+
res.klass.respond_to?(:_passive_columns)
|
|
24
|
+
rescue NameError
|
|
25
|
+
# If +config.eager_load!+ is disabled, an association class may not be loaded yet
|
|
26
|
+
# so we can't constantize to check if the class has passive columns.
|
|
27
|
+
# In this case, we assume the class has passive columns.
|
|
28
|
+
true
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PassiveColumns
|
|
4
|
+
# ActiveRecordRelationExtension is a module that extends ActiveRecord::Relation
|
|
5
|
+
# to automatically select all columns except passive columns if no columns are selected.
|
|
6
|
+
module ActiveRecordRelationExtension
|
|
7
|
+
def exec_main_query(**args)
|
|
8
|
+
_set_columns_except_passive_if_nothing_selected
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def to_sql
|
|
13
|
+
_set_columns_except_passive_if_nothing_selected
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def _set_columns_except_passive_if_nothing_selected
|
|
18
|
+
return nil if klass.try(:_passive_columns).blank? || select_values.any?
|
|
19
|
+
|
|
20
|
+
self.select_values = klass.column_names - klass._passive_columns
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# @!attribute [r] lazy_columns
|
|
4
|
+
# @return [Array<Symbol>]
|
|
5
|
+
# @!attribute [r] model
|
|
6
|
+
# @return [LazyColumns]
|
|
7
|
+
module PassiveColumns
|
|
8
|
+
# Loader is a class helper that loads a column value from the database if it is not loaded yet.
|
|
9
|
+
class Loader
|
|
10
|
+
attr_reader :passive_columns, :model
|
|
11
|
+
|
|
12
|
+
# @param [LazyColumns] model
|
|
13
|
+
# @param [Array<Symbol>] passive_columns
|
|
14
|
+
def initialize(model, passive_columns)
|
|
15
|
+
@model = model
|
|
16
|
+
@passive_columns = passive_columns
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param [Symbol, String] column
|
|
20
|
+
# @param [Boolean] force
|
|
21
|
+
# @return [any]
|
|
22
|
+
def load(column, force: false)
|
|
23
|
+
return yield if block_given?
|
|
24
|
+
|
|
25
|
+
model.send(column)
|
|
26
|
+
rescue ActiveModel::MissingAttributeError
|
|
27
|
+
allowed_columns = (force ? [column] : passive_columns).map(&:to_s)
|
|
28
|
+
raise if allowed_columns.exclude?(column.to_s) || identity_constraints.value?(nil)
|
|
29
|
+
|
|
30
|
+
value = pick_value(column)
|
|
31
|
+
model[column] = value
|
|
32
|
+
model.send(:clear_attribute_change, column)
|
|
33
|
+
value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def pick_value(column)
|
|
39
|
+
model.class.unscoped.where(identity_constraints).pick(column)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def identity_constraints
|
|
43
|
+
@identity_constraints ||= model.send(:_query_constraints_hash)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# require 'passive_columns/active_record_relation_extension'
|
|
4
|
+
|
|
5
|
+
# module PassiveColumns # :nodoc:
|
|
6
|
+
# class Railtie < Rails::Railtie # :nodoc:
|
|
7
|
+
# config.to_prepare do |_app|
|
|
8
|
+
# ActiveSupport.on_load(:active_record) do
|
|
9
|
+
# ActiveRecord::Relation.prepend(ActiveRecordRelationExtension)
|
|
10
|
+
# end
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
|
|
15
|
+
require 'passive_columns/active_record_relation_extension'
|
|
16
|
+
require 'passive_columns/active_record_association_builder_extension'
|
|
17
|
+
|
|
18
|
+
module PassiveColumns # :nodoc:
|
|
19
|
+
class Railtie < Rails::Railtie # :nodoc:
|
|
20
|
+
config.to_prepare do |_app|
|
|
21
|
+
ActiveSupport.on_load(:active_record) do
|
|
22
|
+
ActiveRecord::Relation.prepend ActiveRecordRelationExtension
|
|
23
|
+
ActiveRecord::Associations::Builder::Association.prepend ActiveRecordAssociationBuilderExtension
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'passive_columns/railtie' if defined?(Rails::Railtie)
|
|
4
|
+
require 'passive_columns/loader'
|
|
5
|
+
|
|
6
|
+
# PassiveColumns module is the module
|
|
7
|
+
# that allows you to skip retrieving the column values from the database by default.
|
|
8
|
+
# The columns are retrieved only when they are called.
|
|
9
|
+
# This module is useful when you have a model with a lot of columns
|
|
10
|
+
# and you don't want to retrieve all of them at once.
|
|
11
|
+
#
|
|
12
|
+
# class Page < ApplicationRecord
|
|
13
|
+
# include PassiveColumns
|
|
14
|
+
# passive_columns :huge_article
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# By default it retrieves all the columns except the passive ones.
|
|
18
|
+
#
|
|
19
|
+
# article = Page.where(status: :active).to_a
|
|
20
|
+
# # => SELECT "pages"."id", "pages"."status", "pages"."title" FROM "pages" WHERE "pages"."status" = 'active'
|
|
21
|
+
#
|
|
22
|
+
# If you specify the columns via select it retrieves only the specified columns and nothing more.
|
|
23
|
+
#
|
|
24
|
+
# page = Page.select(:id, :title).take # => #<Page id: 1, title: "Some title">
|
|
25
|
+
# page.to_json # => {"id": 1, "title": "Some title"}
|
|
26
|
+
#
|
|
27
|
+
# But you still has an ability to retrieve the passive column on demand
|
|
28
|
+
#
|
|
29
|
+
# page.huge_article
|
|
30
|
+
# # => SELECT "pages"."huge_article" WHERE "pages"."id" = 1 LIMIT 1
|
|
31
|
+
# # => 'Some huge article...'
|
|
32
|
+
# page.to_json # => {"id": 1, "title": "Some title", "huge_article": "Some huge article..."}
|
|
33
|
+
#
|
|
34
|
+
# The next time you call the passive column it won't hit the database as it is already loaded.
|
|
35
|
+
# page.huge_article # => 'Some huge article...'
|
|
36
|
+
module PassiveColumns
|
|
37
|
+
extend ActiveSupport::Concern
|
|
38
|
+
|
|
39
|
+
included do
|
|
40
|
+
class_attribute :_passive_columns, default: []
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class_methods do
|
|
44
|
+
# Specify column names for on-demand loading.
|
|
45
|
+
# While the columns aren't actively loading, they are still responsive and load when called upon.
|
|
46
|
+
# passive_columns :huge_article, :settings
|
|
47
|
+
# @param [Array<Symbol>] columns
|
|
48
|
+
# @return [void]
|
|
49
|
+
def passive_columns(*columns)
|
|
50
|
+
self._passive_columns = columns.map(&:to_s)
|
|
51
|
+
columns.each do |column|
|
|
52
|
+
define_method(column) { _passive_column_loader.load(column) { super() } }
|
|
53
|
+
define_method(:"#{column}=") do |value|
|
|
54
|
+
_passive_column_loader.load(column)
|
|
55
|
+
super(value)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Each validation rule directly associated with a passive column
|
|
61
|
+
# has an "if: -> {..}" added by default to skip validation if the column is not set.
|
|
62
|
+
# passive_columns :huge_article
|
|
63
|
+
# validates :huge_article, presence: true
|
|
64
|
+
#
|
|
65
|
+
# The above code converts the validation rule into a rule with IF under the hood.
|
|
66
|
+
# and will be equivalent:
|
|
67
|
+
# passive_columns :huge_article
|
|
68
|
+
# validates :huge_article, presence: true, if: -> { attributes.key?('huge_article') }
|
|
69
|
+
#
|
|
70
|
+
# Another example with a condition:
|
|
71
|
+
# passive_columns :huge_article
|
|
72
|
+
# validates :huge_article, presence: true, if: -> { status == 'active' }
|
|
73
|
+
#
|
|
74
|
+
# The above code will be equivalent to the below under the hood:
|
|
75
|
+
# passive_columns :huge_article
|
|
76
|
+
# validates :huge_article, presence: true, if: -> { attributes.key?('huge_article') && status == 'active' }
|
|
77
|
+
#
|
|
78
|
+
# !! A validation rule will not be converted if the rule has been set for many attributes.
|
|
79
|
+
# passive_columns :huge_article
|
|
80
|
+
# validates :huge_article, :settings, presence: true
|
|
81
|
+
# The code above won't be transformed under the hood and the "if" condition won't be added.
|
|
82
|
+
# It is important to set the validation rule for each "passive column" separately.
|
|
83
|
+
# Otherwise, the passive column will be retrieved from DB before validation itself.
|
|
84
|
+
def set_callback(name, *filter_list, &block)
|
|
85
|
+
opts = filter_list.extract_options!
|
|
86
|
+
if name == :validate && opts[:attributes]&.one?
|
|
87
|
+
passive_column = opts[:attributes].map(&:to_s) & _passive_columns
|
|
88
|
+
opts[:if] = ([-> { attributes.key?(passive_column) }] + Array(opts[:if])) if passive_column.present?
|
|
89
|
+
end
|
|
90
|
+
super(name, *filter_list, opts, &block)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# This method loads a column value, if not already loaded, from the database
|
|
95
|
+
# regardless of whether the column is added to "passive_columns" or not.
|
|
96
|
+
#
|
|
97
|
+
# It uses the Rails' ".pick" method to get the value of the column under the hood
|
|
98
|
+
# user = User.select('id').take!
|
|
99
|
+
# user.load_column(:name) # => SELECT "name" FROM "users" WHERE "id" = ? LIMIT ?
|
|
100
|
+
# 'John'
|
|
101
|
+
# user.load_column(:name)
|
|
102
|
+
# 'John'
|
|
103
|
+
# @param [Symbol, String] column
|
|
104
|
+
# @return [any]
|
|
105
|
+
def load_column(column)
|
|
106
|
+
_passive_column_loader.load(column, force: true)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def _passive_column_loader
|
|
110
|
+
@_passive_column_loader ||= PassiveColumns::Loader.new(self, _passive_columns)
|
|
111
|
+
end
|
|
112
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: passive_columns
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dmitry Golovin
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2024-06-19 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: '7.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '7.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '7.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rspec
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec-rails
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rubocop
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
description: A gem that allows you to exclude some columns from a SELECT query by
|
|
84
|
+
default and load them only on demand
|
|
85
|
+
email: headman.dev@gmail.com
|
|
86
|
+
executables: []
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- MIT-LICENSE
|
|
91
|
+
- README.md
|
|
92
|
+
- Rakefile
|
|
93
|
+
- lib/passive_columns.rb
|
|
94
|
+
- lib/passive_columns/active_record_association_builder_extension.rb
|
|
95
|
+
- lib/passive_columns/active_record_relation_extension.rb
|
|
96
|
+
- lib/passive_columns/loader.rb
|
|
97
|
+
- lib/passive_columns/railtie.rb
|
|
98
|
+
- lib/passive_columns/version.rb
|
|
99
|
+
homepage: https://github.com/headmandev/passive_columns
|
|
100
|
+
licenses:
|
|
101
|
+
- MIT
|
|
102
|
+
metadata: {}
|
|
103
|
+
post_install_message:
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '2.7'
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.3.26
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: A gem that extends Active Record to retrieve columns from a db on demand
|
|
122
|
+
test_files: []
|