pg_tags_on 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +9 -0
  5. data/CHANGELOG.md +3 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +91 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +195 -0
  11. data/Rakefile +16 -0
  12. data/bin/console +16 -0
  13. data/bin/setup +8 -0
  14. data/lib/pg_tags_on/active_record/arel.rb +66 -0
  15. data/lib/pg_tags_on/active_record/base.rb +41 -0
  16. data/lib/pg_tags_on/benchmark/benchmark.rb +52 -0
  17. data/lib/pg_tags_on/predicate_handler/array_integer_handler.rb +9 -0
  18. data/lib/pg_tags_on/predicate_handler/array_jsonb_handler.rb +31 -0
  19. data/lib/pg_tags_on/predicate_handler/array_jsonb_with_attrs_handler.rb +41 -0
  20. data/lib/pg_tags_on/predicate_handler/array_string_handler.rb +9 -0
  21. data/lib/pg_tags_on/predicate_handler/array_text_handler.rb +9 -0
  22. data/lib/pg_tags_on/predicate_handler/base_handler.rb +89 -0
  23. data/lib/pg_tags_on/predicate_handler.rb +64 -0
  24. data/lib/pg_tags_on/repositories/array_jsonb_repository.rb +88 -0
  25. data/lib/pg_tags_on/repositories/array_repository.rb +103 -0
  26. data/lib/pg_tags_on/repositories/base_repository.rb +44 -0
  27. data/lib/pg_tags_on/repository.rb +59 -0
  28. data/lib/pg_tags_on/tag.rb +31 -0
  29. data/lib/pg_tags_on/tags_query.rb +27 -0
  30. data/lib/pg_tags_on/validations/validator.rb +43 -0
  31. data/lib/pg_tags_on/version.rb +5 -0
  32. data/lib/pg_tags_on.rb +56 -0
  33. data/pg_tags_on.gemspec +38 -0
  34. data/spec/array_integers/records_spec.rb +47 -0
  35. data/spec/array_integers/tag_ops_spec.rb +65 -0
  36. data/spec/array_integers/taggings_spec.rb +27 -0
  37. data/spec/array_integers/tags_spec.rb +53 -0
  38. data/spec/array_jsonb/records_spec.rb +89 -0
  39. data/spec/array_jsonb/tag_ops_spec.rb +115 -0
  40. data/spec/array_jsonb/taggings_spec.rb +27 -0
  41. data/spec/array_jsonb/tags_spec.rb +41 -0
  42. data/spec/array_strings/records_spec.rb +61 -0
  43. data/spec/array_strings/tag_ops_spec.rb +65 -0
  44. data/spec/array_strings/taggings_spec.rb +27 -0
  45. data/spec/array_strings/tags_spec.rb +54 -0
  46. data/spec/config/database.yml +6 -0
  47. data/spec/configuration_spec.rb +48 -0
  48. data/spec/helpers/database_helpers.rb +46 -0
  49. data/spec/spec_helper.rb +39 -0
  50. data/spec/support/factory.rb +47 -0
  51. data/spec/tags_query_spec.rb +31 -0
  52. data/spec/validator_spec.rb +40 -0
  53. data/tasks/benchmark.rake +58 -0
  54. metadata +260 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7309e077c88514299fcd0166407bae538e99f6cf6308893957376ee2f7ff2959
4
+ data.tar.gz: 893119a8b6487ba8bb7acbe5a82cca67ac4dc7bc14bf760416024b98c2359381
5
+ SHA512:
6
+ metadata.gz: 1ba49f001365da7e716585d062f653dfe356b8c7355e1d7e99ba0a14926f4187e6ecb1217eb7b818684d7d29772fba7abc68722eeefb63847be1fea031eb4908
7
+ data.tar.gz: cf84932b3a0b46ec17e1dbbb2cc07b1f807940b7bea8a3aa2ac10af96b38a58774266ae93ebc9571d64f5b79f21c4ffb4243c5654bde412800c144be2b997065
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,9 @@
1
+ AllCops:
2
+ Exclude:
3
+ - '**/benchmark.rb'
4
+ Layout/LineLength:
5
+ Max: 120
6
+ Metrics/BlockLength:
7
+ Max: 120
8
+ Metrics/MethodLength:
9
+ Max: 20
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ == 0.1.0 / 2020-03-11
2
+
3
+ First release
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at catalin.marinescu@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in pg_tags_on.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,91 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ pg_tags_on (0.1.0)
5
+ activerecord (~> 6.0)
6
+ activesupport (~> 6.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ activemodel (6.0.1)
12
+ activesupport (= 6.0.1)
13
+ activerecord (6.0.1)
14
+ activemodel (= 6.0.1)
15
+ activesupport (= 6.0.1)
16
+ activesupport (6.0.1)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 0.7, < 2)
19
+ minitest (~> 5.1)
20
+ tzinfo (~> 1.1)
21
+ zeitwerk (~> 2.2)
22
+ ast (2.4.0)
23
+ coderay (1.1.2)
24
+ concurrent-ruby (1.1.5)
25
+ diff-lcs (1.3)
26
+ docile (1.3.2)
27
+ faker (2.10.2)
28
+ i18n (>= 1.6, < 2)
29
+ i18n (1.7.0)
30
+ concurrent-ruby (~> 1.0)
31
+ jaro_winkler (1.5.4)
32
+ method_source (0.9.2)
33
+ minitest (5.13.0)
34
+ parallel (1.19.1)
35
+ parser (2.7.0.2)
36
+ ast (~> 2.4.0)
37
+ pg (1.2.2)
38
+ pry (0.12.2)
39
+ coderay (~> 1.1.0)
40
+ method_source (~> 0.9.0)
41
+ rainbow (3.0.0)
42
+ rake (13.0.1)
43
+ rexml (3.2.4)
44
+ rspec (3.9.0)
45
+ rspec-core (~> 3.9.0)
46
+ rspec-expectations (~> 3.9.0)
47
+ rspec-mocks (~> 3.9.0)
48
+ rspec-core (3.9.0)
49
+ rspec-support (~> 3.9.0)
50
+ rspec-expectations (3.9.0)
51
+ diff-lcs (>= 1.2.0, < 2.0)
52
+ rspec-support (~> 3.9.0)
53
+ rspec-mocks (3.9.0)
54
+ diff-lcs (>= 1.2.0, < 2.0)
55
+ rspec-support (~> 3.9.0)
56
+ rspec-support (3.9.0)
57
+ rubocop (0.80.0)
58
+ jaro_winkler (~> 1.5.1)
59
+ parallel (~> 1.10)
60
+ parser (>= 2.7.0.1)
61
+ rainbow (>= 2.2.2, < 4.0)
62
+ rexml
63
+ ruby-progressbar (~> 1.7)
64
+ unicode-display_width (>= 1.4.0, < 1.7)
65
+ ruby-progressbar (1.10.1)
66
+ simplecov (0.18.2)
67
+ docile (~> 1.1)
68
+ simplecov-html (~> 0.11)
69
+ simplecov-html (0.12.1)
70
+ thread_safe (0.3.6)
71
+ tzinfo (1.2.5)
72
+ thread_safe (~> 0.1)
73
+ unicode-display_width (1.6.1)
74
+ zeitwerk (2.2.1)
75
+
76
+ PLATFORMS
77
+ ruby
78
+
79
+ DEPENDENCIES
80
+ bundler (~> 2.0)
81
+ faker (~> 2.10)
82
+ pg (~> 1.2)
83
+ pg_tags_on!
84
+ pry (~> 0.12)
85
+ rake (~> 13.0)
86
+ rspec (~> 3.9)
87
+ rubocop (~> 0.80)
88
+ simplecov (~> 0.18)
89
+
90
+ BUNDLED WITH
91
+ 2.1.4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Catalin Marinescu
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,195 @@
1
+ # PgTagsOn
2
+
3
+ ```pg_tags_on``` is a gem that makes working with tags stored in a Postgresql column easy. Supported column types are: ```character varying[]```, ```text[]```, ```integer[]``` and ```jsonb[]```.
4
+
5
+
6
+ Requirements:
7
+ * Postgresql >= 12
8
+ * Rails >= 6
9
+
10
+
11
+ Note: this gem is in early stage of development.
12
+
13
+ ## Table of contents
14
+
15
+ - [Installation](#installation)
16
+ - [Usage](#usage)
17
+ - [ActiveRecord model setup](#activerecord-model-setup)
18
+ - [Records queries](#records-queries)
19
+ - [Tags](#tags)
20
+ - [Set record's tags](#set-records-tags)
21
+ - [Configuration](#configuration)
22
+ - [Benchmarks](#benchmarks)
23
+ - [Contributing](#contributing)
24
+
25
+
26
+ ## Installation
27
+
28
+ Add this line to your application's Gemfile:
29
+
30
+ ```ruby
31
+ gem 'pg_tags_on'
32
+ ```
33
+
34
+ And then execute:
35
+
36
+ $ bundle
37
+
38
+ Or install it yourself as:
39
+
40
+ $ gem install pg_tags_on
41
+
42
+ ## Usage
43
+ ### ActiveRecord model setup
44
+
45
+ One or multiple columns from a model can be specified:
46
+
47
+ ```ruby
48
+ class Entity < ActiveRecord::Base
49
+ pg_tags_on :tags
50
+ pg_tags_on :other_tags
51
+ end
52
+ ```
53
+
54
+ Validations for max. number of tags and max. tag length can be added and errors will be injected into model's ```errors``` object.
55
+
56
+
57
+ ```ruby
58
+ class Entity < ActiveRecord::Base
59
+ pg_tags_on :tags, limit: 10, tag_length: 50 # limit to 10 tags and 50 chars. per tag.
60
+ end
61
+ ```
62
+
63
+ For ```jsonb[]``` you'll have to specify the key for the tag value. If you store multiple attributes in the objects then you must set also ```has_attributes: true```.
64
+
65
+ ```ruby
66
+ class Entity < ActiveRecord::Base
67
+ pg_tags_on :tags, key: :tag # => [{tag: 'alpha'}, {tag: 'beta'}]
68
+ pg_tags_on :other_tags, key: :tag, has_attributes: true # => [{tag: 'alpha', created_by: 'mike', ...}, {tag: 'beta', created_by: 'john', ...}]
69
+ end
70
+ ```
71
+
72
+ ### Records Queries
73
+ ```pg_tags_on``` registers ```Tags``` class in model's predicate builder, so you can filter the records by tags as you are usually doing in Rails. Class name can be changed if you have conflicts or you don't like it, see the [configuration](#configuration) section.
74
+
75
+ * Find records by tag:
76
+
77
+ ```ruby
78
+ Entity.where(tags: Tags.one('alpha'))
79
+ ```
80
+
81
+ * Find records that have exact same tags as the list, order is not important:
82
+
83
+ ```ruby
84
+ Entity.where(tags: Tags.eq('alpha', 'beta', 'gamma')) # Array argument is allowed, too
85
+ ```
86
+
87
+ * Find records that includes all the tags from the list:
88
+
89
+ ```ruby
90
+ Entity.where(tags: Tags.all('alpha', 'beta', 'gamma')) # Array argument is allowed, too
91
+ ```
92
+
93
+ * Find records that includes any tag from the list:
94
+
95
+ ```ruby
96
+ Entity.where(tags: Tags.any('alpha', 'beta', 'gamma')) # Array argument is allowed, too
97
+ ```
98
+
99
+ * Find records that have all the tags included in the list:
100
+
101
+ ```ruby
102
+ Entity.where(tags: Tags.in('alpha', 'beta', 'gamma')) # Array argument is allowed, too
103
+ ```
104
+
105
+ All the above queries supports negation operator. Example:
106
+
107
+ ```ruby
108
+ Entity.where.not(tags: Tags.one('alpha'))
109
+ ```
110
+
111
+ ### Tags
112
+ ```pg_tags_on``` creates a proxy at the class level, with the same name as the column, that can be used to query or do operations on tags.
113
+
114
+ Tags queries:
115
+
116
+ ```ruby
117
+ Entity.tags.all
118
+ => [#<PgTagsOn::Tag name: "alpha">, #<PgTagsOn::Tag name: "beta">, ... ]
119
+
120
+ Entity.tags.all_with_counts
121
+ => [#<PgTagsOn::Tag name: "alpha", count: 10>, #<PgTagsOn::Tag name: "beta", count: 20>, ... ]
122
+
123
+ Entity.tags.count
124
+ => 123
125
+
126
+ # filters can be applied
127
+
128
+ Entity.where(...).tags.all.where('name ilike ?', 'alp%')
129
+ => [#<PgTagsOn::Tag name: "alpha">, #<PgTagsOn::Tag name: "alpine">, ... ]
130
+
131
+ Entity.where(...).tags.all_with_counts.where('name ilike ?', 'alp%')
132
+ => [#<PgTagsOn::Tag name: "alpha", count: 10>, #<PgTagsOn::Tag name: "alpine", count: 20>, ... ]
133
+
134
+ ```
135
+
136
+ Taggings:
137
+
138
+ ```ruby
139
+ Entity.tags.taggings
140
+ => [#<PgTagsOn::Tag name: "alpha", entity_id: 1>, #<PgTagsOn::Tag name: "beta", entity_id: 1>, #<PgTagsOn::Tag name: "alpha", entity_id: 2>, ... ]
141
+ ```
142
+
143
+ Create, update and delete methods are using, for performance reasons, Postgresql functions to manipulate the arrays, so ActiveRecord models are not instantiated. A frequent problem is to ensure uniqueness of the tags for a record, and this can be achieved at the database level with a before create/update row trigger.
144
+
145
+ ```ruby
146
+ # create
147
+ Entity.tags.create('alpha') # add tag to all records
148
+ Entity.where(...).tags.create('alpha') # add tag to filtered records
149
+
150
+ # update
151
+ Entity.tags.update('alpha', 'a') # rename tag for all records
152
+ Entity.where(...).tags.update('alpha', 'a') # rename tag for filtered records
153
+
154
+ # delete
155
+ Entity.tags.delete('alpha') # delete tag from all records
156
+ Entity.where(...).tags.delete('alpha') # delete tag from filtered records
157
+ ```
158
+
159
+ ### Set record's tags
160
+ By default ```pg_tags_on``` does not add any logic in the way that the tags are set and saved in database. It'll let all the transformations, like lowercase, strip spaces, unique etc..., at the programmer choice.
161
+
162
+
163
+ ### Configuration
164
+
165
+ You can configure ```pg_tags_on``` in an initializer ```config/initializers/pg_tags_on.rb```:
166
+
167
+ ```ruby
168
+ PgTagsOn.configure do |c|
169
+ c.query_class = 'Tagz'
170
+ end
171
+ ```
172
+
173
+ ### Benchmarks
174
+
175
+ ```ruby
176
+ rake pg_tags_on:benchmark
177
+ ```
178
+
179
+ ## Contributing
180
+
181
+ 1. Fork it ( http://github.com/cata-m/pg_tags_on/fork )
182
+ 2. Install gem dependencies: ```bundle install```
183
+ 3. Create a new feature or fix branch like: 'feature/new-feature' or 'fix/fix-some-issues'
184
+ 4. Make sure all tests pass: ```bundle exec rake spec```
185
+ 5. Commit your changes: git commit -am 'your changes'
186
+ 6. Push to the branch
187
+ 7. Create new pull request
188
+
189
+ ## License
190
+
191
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
192
+
193
+ ## Code of Conduct
194
+
195
+ Everyone interacting in the PgTagsOn project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/cata-m/pg_tags_on/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.dirname(__FILE__)
4
+
5
+ require 'active_record'
6
+ require 'active_support/all'
7
+ require 'faker'
8
+ require './lib/pg_tags_on'
9
+
10
+ Dir['tasks/**/*.rake'].each { |rake| load rake }
11
+
12
+ require 'bundler/gem_tasks'
13
+ Bundler::GemHelper.install_tasks
14
+
15
+ require 'rspec/core/rake_task'
16
+ RSpec::Core::RakeTask.new(:spec)
data/bin/console ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'active_record'
5
+ require 'bundler/setup'
6
+ require 'pg_tags_on'
7
+
8
+ # You can add fixtures and/or initialization code here to make experimenting
9
+ # with your gem easier. You can also use a different console, if you like.
10
+
11
+ # (If you use this, don't forget to add pry to your Gemfile!)
12
+ # require "pry"
13
+ # Pry.start
14
+
15
+ require 'irb'
16
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ module ActiveRecord
5
+ # Arel extension
6
+ module Arel
7
+ def arel_function(name, *args)
8
+ ::Arel::Nodes::NamedFunction.new(name, args)
9
+ end
10
+
11
+ # unnest function
12
+ # +attr+ Arel attribute
13
+ def arel_unnest(attr)
14
+ arel_function('unnest', attr)
15
+ end
16
+
17
+ # distinct function
18
+ def arel_distinct(node)
19
+ arel_function('distinct', node)
20
+ end
21
+
22
+ # array_cat function
23
+ def arel_array_cat(attr, value)
24
+ arel_function('array_cat', attr, value)
25
+ end
26
+
27
+ # array_replace function
28
+ def arel_array_replace(attr, value1, value2)
29
+ arel_function('array_replace', attr, value1, value2)
30
+ end
31
+
32
+ # array_remove function
33
+ def arel_array_remove(attr, value)
34
+ arel_function('array_remove', attr, value)
35
+ end
36
+
37
+ def arel_jsonb_extract_path(attr, *args)
38
+ arel_function('jsonb_extract_path', [attr, *args])
39
+ end
40
+
41
+ def arel_query_attribute(attr, value, cast_type)
42
+ ::ActiveRecord::Relation::QueryAttribute.new(attr, value, cast_type)
43
+ end
44
+
45
+ def arel_bind(query_attr)
46
+ ::Arel::Nodes::BindParam.new(query_attr)
47
+ end
48
+
49
+ def arel_infix_operation(operator, left, right)
50
+ ::Arel::Nodes::InfixOperation.new(operator, left, right)
51
+ end
52
+
53
+ def arel_cast(attr, type)
54
+ ::Arel::Nodes::InfixOperation.new('::', attr, arel_sql(type))
55
+ end
56
+
57
+ def arel_sql(expr)
58
+ ::Arel.sql(expr)
59
+ end
60
+
61
+ def arel_build_quoted(expr)
62
+ ::Arel::Nodes.build_quoted(expr)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ module ActiveRecord
5
+ # ActiveRecord::Base extension
6
+ module Base
7
+ extend ActiveSupport::Concern
8
+
9
+ class_methods do
10
+ def pg_tags_on(name, options = {})
11
+ raise PgTagsOn::ColumnNotFoundError, "#{name} column not found" unless column_names.include?(name.to_s)
12
+
13
+ pg_tags_on_init_model unless @pg_tags_on_init_model
14
+ pg_tags_on_settings[name] = options.deep_symbolize_keys
15
+ validates(name, 'pg_tags_on/tags': true) if %i[limit tag_length].any? { |k| options[k] }
16
+ instance_eval <<-RUBY, __FILE__, __LINE__ + 1
17
+ scope :#{name}, -> { PgTagsOn::Repository.new(self, "#{name}") }
18
+ RUBY
19
+ end
20
+
21
+ def pg_tags_on_init_model
22
+ @pg_tags_on_init_model ||= begin
23
+ PgTagsOn.register_query_class
24
+ predicate_builder.register_handler(PgTagsOn.query_class, PgTagsOn::PredicateHandler.new(predicate_builder))
25
+ cattr_accessor :pg_tags_on_settings
26
+ self.pg_tags_on_settings ||= {}.with_indifferent_access
27
+ end
28
+ end
29
+
30
+ def pg_tags_on_options_for(name)
31
+ self.pg_tags_on_settings[name.to_sym]
32
+ end
33
+
34
+ def pg_tags_on_reset
35
+ @pg_tags_on_init_model = false
36
+ self.pg_tags_on_settings = nil
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ # Performs benchmarking for a field, on an existing set of data.
5
+ class Benchmark
6
+ def initialize(klass, field)
7
+ @klass = klass
8
+ @field = field
9
+ end
10
+
11
+ def call
12
+ tags = Entity.send(field).all.limit(5).pluck(:name)
13
+
14
+ ::Benchmark.bmbm do |b|
15
+ b.report('Tags.one') do
16
+ Entity.where(field => Tags.one(tags.first)).all.to_a
17
+ end
18
+ b.report('Tags.eq') do
19
+ Entity.where(field => Tags.eq(tags[0..1])).all.to_a
20
+ end
21
+ b.report('Tags.any') do
22
+ Entity.where(field => Tags.any(tags)).all.to_a
23
+ end
24
+ b.report('Tags.all') do
25
+ Entity.where(field => Tags.all(tags)).all.to_a
26
+ end
27
+ b.report('Tags.in') do
28
+ Entity.where(field => Tags.in(tags)).all.to_a
29
+ end
30
+ b.report('Entity.tags.all') do
31
+ Entity.send(field).all.to_a
32
+ end
33
+ b.report('Entity.tags.taggings') do
34
+ Entity.send(field).taggings.all.to_a
35
+ end
36
+ b.report('Entity.tags.create') do
37
+ Entity.send(field).create('new-tag')
38
+ end
39
+ b.report("Entity.tags.update") do
40
+ Entity.send(field).update(tags.pop, tags[0]+tags[1])
41
+ end
42
+ b.report("Entity.tags.delete") do
43
+ Entity.send(field).delete(tags.pop)
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :klass, :field
51
+ end
52
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ class PredicateHandler
5
+ # Predicate handler for character varying[] column type
6
+ class ArrayIntegerHandler < BaseHandler
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PgTagsOn
4
+ class PredicateHandler
5
+ # Predicate handler for jsonb[] column type
6
+ class ArrayJsonbHandler < BaseHandler
7
+ # Transforms value in Hash if :key option is set
8
+ def value
9
+ @value ||= begin
10
+ return query_value unless key?
11
+
12
+ query_value.each.map do |val|
13
+ key.reverse.inject(val) { |a, n| { n => a } }
14
+ end
15
+ end
16
+ end
17
+
18
+ def query_value
19
+ @query_value ||= Array.wrap(query.value)
20
+ end
21
+
22
+ def key
23
+ @key ||= Array.wrap(settings[:key])
24
+ end
25
+
26
+ def key?
27
+ key.present?
28
+ end
29
+ end
30
+ end
31
+ end