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 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,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+ require "rubocop/rake_task"
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ RuboCop::RakeTask.new
7
+
8
+ task default: [:rubocop, :spec]
@@ -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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PassiveColumns
4
+ VERSION = '0.1.2'
5
+ 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: []