pg_taggable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +217 -0
- data/Rakefile +3 -0
- data/lib/pg_taggable/predicate_handler.rb +29 -0
- data/lib/pg_taggable/taggable.rb +40 -0
- data/lib/pg_taggable/version.rb +3 -0
- data/lib/pg_taggable.rb +10 -0
- metadata +68 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ec57f99287e3bb16b29e459e42c0f3c5a24d6f967917906d88dadfe491d133e3
|
4
|
+
data.tar.gz: 525995c96c17957ba46a5025d250c1703efec6da6841277864befd0278072449
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: caa213bb0b77a0c5f71922337563d9bf3222cf98a17571e90f700b5fabf5b345c1c4494c71c45889e8e081b1c0c44c200fa4c228638408d39d2039d26b5bca73
|
7
|
+
data.tar.gz: b4760ae519a53f6b414e5578ac6ec241b30b3342acdc82057a9a437599c65ce71bd473ef2be8ccd8c6ee99bff393fc775e647fee75ed5333f519b1ddb8617eda
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright Yi-Cyuan Chen
|
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,217 @@
|
|
1
|
+
# PgTaggable
|
2
|
+
A simple tagging gem for Rails using PostgreSQL array.
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
Add this line to your application's Gemfile:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
gem "pg_taggable"
|
9
|
+
```
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
```bash
|
13
|
+
$ bundle
|
14
|
+
```
|
15
|
+
|
16
|
+
Or install it yourself as:
|
17
|
+
```bash
|
18
|
+
$ gem install pg_taggable
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
### Setup
|
23
|
+
Add array columns and index to your table. For example:
|
24
|
+
```Ruby
|
25
|
+
class CreatePosts < ActiveRecord::Migration[8.0]
|
26
|
+
def change
|
27
|
+
create_table :posts do |t|
|
28
|
+
t.string :tags, array: true, default: []
|
29
|
+
|
30
|
+
t.timestamps
|
31
|
+
|
32
|
+
t.index :tags, using: 'gin'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
```
|
37
|
+
|
38
|
+
Indicate that attribute is "taggable" in a Rails model, like this:
|
39
|
+
```Ruby
|
40
|
+
class Post < ActiveRecord::Base
|
41
|
+
taggable :tags
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
### Modify
|
46
|
+
You can modify it like normal array
|
47
|
+
```Ruby
|
48
|
+
#set
|
49
|
+
post.tags = ['food', 'travel']
|
50
|
+
|
51
|
+
#add
|
52
|
+
post.tags += ['food']
|
53
|
+
post.tags += ['food', 'travel']
|
54
|
+
post.tags << 'food'
|
55
|
+
|
56
|
+
#remove
|
57
|
+
post.tags -= ['food']
|
58
|
+
```
|
59
|
+
|
60
|
+
### Queries
|
61
|
+
#### any_#{tag_name}
|
62
|
+
Find records with any of the tags.
|
63
|
+
```Ruby
|
64
|
+
Post.where(any_tags: ['food', 'travel'])
|
65
|
+
```
|
66
|
+
|
67
|
+
You can use with `not`
|
68
|
+
```Ruby
|
69
|
+
Post.where.not(any_tags: ['food', 'travel'])
|
70
|
+
```
|
71
|
+
|
72
|
+
#### all_#{tag_name}
|
73
|
+
Find records with all of the tags
|
74
|
+
```Ruby
|
75
|
+
Post.where(all_tags: ['food', 'travel'])
|
76
|
+
```
|
77
|
+
|
78
|
+
#### #{tag_name}_in
|
79
|
+
Find records that have all the tags included in the list
|
80
|
+
```Ruby
|
81
|
+
Post.where(tags_in: ['food', 'travel'])
|
82
|
+
```
|
83
|
+
|
84
|
+
#### #{tag_name}_eq
|
85
|
+
Find records that have exact same tags as the list, order is not important
|
86
|
+
```Ruby
|
87
|
+
Post.where(tags_eq: ['food', 'travel'])
|
88
|
+
```
|
89
|
+
|
90
|
+
#### #{tag_name}
|
91
|
+
Find records that have exact same tags as the list, order is important. This is the default behavior.
|
92
|
+
```Ruby
|
93
|
+
Post.where(tags: ['food', 'travel'])
|
94
|
+
```
|
95
|
+
|
96
|
+
Assume a post has tags: 'A', 'B'
|
97
|
+
|Method|Query|Matched|
|
98
|
+
|-|-|-|
|
99
|
+
|any_tags|A|True|
|
100
|
+
|any_tags|A, B|True|
|
101
|
+
|any_tags|B, A|True|
|
102
|
+
|any_tags|A, B, C|True|
|
103
|
+
|all_tags|A|True|
|
104
|
+
|all_tags|A, B|True|
|
105
|
+
|all_tags|B, A|True|
|
106
|
+
|all_tags|A, B, C|False|
|
107
|
+
|tags_in|A|False|
|
108
|
+
|tags_in|A, B|True|
|
109
|
+
|tags_in|B, A|True|
|
110
|
+
|tags_in|A, B, C|True|
|
111
|
+
|tags_eq|A|False|
|
112
|
+
|tags_eq|A, B|True|
|
113
|
+
|tags_eq|B, A|True|
|
114
|
+
|tags_eq|A, B, C|False|
|
115
|
+
|tags|A|False|
|
116
|
+
|tags|A, B|True|
|
117
|
+
|tags|B, A|False|
|
118
|
+
|tags|A, B, C|False|
|
119
|
+
|
120
|
+
### Class Methods
|
121
|
+
#### taggable(name, unique: true)
|
122
|
+
Indicate that attribute is "taggable".
|
123
|
+
|
124
|
+
#### unique: true
|
125
|
+
You can use `unique` option to ensure that tags are unique. It will be deduplicated before saving. The default is `true`.
|
126
|
+
|
127
|
+
```Ruby
|
128
|
+
# taggable :tags, unique: true
|
129
|
+
post = Post.create(tags: ['food', 'travel', 'food'])
|
130
|
+
post.tags
|
131
|
+
# => ['food', 'travel']
|
132
|
+
|
133
|
+
# taggable :tags, unique: false
|
134
|
+
post = Post.create(tags: ['food', 'travel', 'food'])
|
135
|
+
post.tags
|
136
|
+
# => ['food', 'travel', 'food']
|
137
|
+
```
|
138
|
+
|
139
|
+
#### #{tag_name}
|
140
|
+
Return unnested tags. The column name will be `tag`, For example:
|
141
|
+
```Ruby
|
142
|
+
Post.tags
|
143
|
+
# => #<ActiveRecord::Relation [#<Post tag: "food", id: nil>, #<Post tag: "travel", id: nil>, #<Post tag: "travel", id: nil>, #<Post tag: "technology", id: nil>]>
|
144
|
+
|
145
|
+
Post.tags.size
|
146
|
+
# => 4
|
147
|
+
|
148
|
+
Post.tags.distinct.size
|
149
|
+
# => 3
|
150
|
+
|
151
|
+
Post.tags.distinct.pluck(:tag)
|
152
|
+
# => ["food", "travel", "technology"]
|
153
|
+
|
154
|
+
Post.tags.group(:tag).count
|
155
|
+
# => {"food"=>1, "travel"=>2, "technology"=>1}
|
156
|
+
```
|
157
|
+
|
158
|
+
#### uniq_#{tag_name}
|
159
|
+
Return an array of unique tag strings.
|
160
|
+
```Ruby
|
161
|
+
Post.uniq_tags
|
162
|
+
# => ["food", "travel", "technology"]
|
163
|
+
|
164
|
+
# equal to
|
165
|
+
Post.tags.distinct.pluck(:tag)
|
166
|
+
```
|
167
|
+
|
168
|
+
#### count_#{tag_name}
|
169
|
+
Calculates the number of occurrences of each tag.
|
170
|
+
```Ruby
|
171
|
+
Post.count_tags
|
172
|
+
# => {"food"=>1, "travel"=>2, "technology"=>1}
|
173
|
+
|
174
|
+
# equal to
|
175
|
+
Post.tags.group(:tag).count
|
176
|
+
```
|
177
|
+
|
178
|
+
### Case Insensitive
|
179
|
+
If you use `string` type, it is case sensitive.
|
180
|
+
```Ruby
|
181
|
+
# tags is string[]
|
182
|
+
post = Post.create(tags: ['food', 'travel', 'Food'])
|
183
|
+
post.tags
|
184
|
+
# => ['food', 'travel', 'Food']
|
185
|
+
```
|
186
|
+
|
187
|
+
If you want case insensitive, you need to use `citext`
|
188
|
+
```Ruby
|
189
|
+
class CreatePosts < ActiveRecord::Migration[8.0]
|
190
|
+
enable_extension('citext') unless extensions.include?('citext')
|
191
|
+
|
192
|
+
def change
|
193
|
+
create_table :posts do |t|
|
194
|
+
t.citext :tags, array: true, default: []
|
195
|
+
|
196
|
+
t.timestamps
|
197
|
+
|
198
|
+
t.index :tags, using: 'gin'
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
```
|
203
|
+
|
204
|
+
You will get the diffent result
|
205
|
+
```Ruby
|
206
|
+
# tags is citext[]
|
207
|
+
post = Post.create(tags: ['food', 'travel', 'Food'])
|
208
|
+
post.tags
|
209
|
+
# => ['food', 'travel']
|
210
|
+
```
|
211
|
+
|
212
|
+
## License
|
213
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
214
|
+
|
215
|
+
## Contact
|
216
|
+
The project's website is located at https://github.com/emn178/pg_taggable
|
217
|
+
Author: Chen, Yi-Cyuan (emn178@gmail.com)
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module PgTaggable
|
2
|
+
class PredicateHandler < ActiveRecord::PredicateBuilder::ArrayHandler
|
3
|
+
attr_reader :taggable_attributes
|
4
|
+
|
5
|
+
def initialize(predicate_builder, taggable_attributes)
|
6
|
+
@taggable_attributes = taggable_attributes
|
7
|
+
super(predicate_builder)
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(attribute, query)
|
11
|
+
taggable_attribute = taggable_attributes[attribute.name]
|
12
|
+
if taggable_attribute
|
13
|
+
attribute.name, type, operator = taggable_attribute
|
14
|
+
query_attribute = ActiveRecord::Relation::QueryAttribute.new(attribute.name, query, type)
|
15
|
+
bind_param = Arel::Nodes::BindParam.new(query_attribute)
|
16
|
+
if operator == '='
|
17
|
+
operator = '@>'
|
18
|
+
Arel::Nodes::NamedFunction.new('ARRAY_LENGTH', [attribute, 1]).eq(query.size).and(
|
19
|
+
Arel::Nodes::InfixOperation.new(operator, attribute, bind_param)
|
20
|
+
)
|
21
|
+
else
|
22
|
+
Arel::Nodes::InfixOperation.new(operator, attribute, bind_param)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
super(attribute, query)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module PgTaggable
|
2
|
+
module Taggable
|
3
|
+
@@taggable_attributes = nil
|
4
|
+
|
5
|
+
def predicate_builder
|
6
|
+
register_taggable_handler
|
7
|
+
super
|
8
|
+
end
|
9
|
+
|
10
|
+
def register_taggable_handler
|
11
|
+
return if @register_taggable_handler || !@@taggable_attributes
|
12
|
+
|
13
|
+
@register_taggable_handler = true
|
14
|
+
predicate_builder.register_handler(Array, PgTaggable::PredicateHandler.new(predicate_builder, @@taggable_attributes))
|
15
|
+
end
|
16
|
+
|
17
|
+
def taggable(name, unique: true)
|
18
|
+
type = type_for_attribute(name)
|
19
|
+
@@taggable_attributes ||= {}
|
20
|
+
@@taggable_attributes = @@taggable_attributes.merge(
|
21
|
+
"any_#{name}" => [ name, type, '&&' ],
|
22
|
+
"all_#{name}" => [ name, type, '@>' ],
|
23
|
+
"#{name}_in" => [ name, type, '<@' ],
|
24
|
+
"#{name}_eq" => [ name, type, '=' ]
|
25
|
+
)
|
26
|
+
|
27
|
+
if unique
|
28
|
+
if type.type == :citext
|
29
|
+
before_save { assign_attributes(name => read_attribute(name).uniq { |t| t.downcase }) }
|
30
|
+
else
|
31
|
+
before_save { assign_attributes(name => read_attribute(name).uniq) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
scope name, -> { unscope(:where).from(select("UNNEST(#{table_name}.#{name}) AS tag"), table_name) }
|
36
|
+
scope "uniq_#{name}", -> { public_send(name).distinct.pluck(:tag) }
|
37
|
+
scope "count_#{name}", -> { public_send(name).group(:tag).count }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/pg_taggable.rb
ADDED
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pg_taggable
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Yi-Cyuan Chen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-04-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rails
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 8.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 8.0.0
|
27
|
+
description: A simple tagging gem for Rails using PostgreSQL array.
|
28
|
+
email:
|
29
|
+
- emn178@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- MIT-LICENSE
|
35
|
+
- README.md
|
36
|
+
- Rakefile
|
37
|
+
- lib/pg_taggable.rb
|
38
|
+
- lib/pg_taggable/predicate_handler.rb
|
39
|
+
- lib/pg_taggable/taggable.rb
|
40
|
+
- lib/pg_taggable/version.rb
|
41
|
+
homepage: https://github.com/emn178/pg_taggable
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata:
|
45
|
+
homepage_uri: https://github.com/emn178/pg_taggable
|
46
|
+
source_code_uri: https://github.com/emn178/pg_taggable
|
47
|
+
changelog_uri: https://github.com/emn178/pg_taggable/blob/master/CHANGELOG.md
|
48
|
+
rubygems_mfa_required: 'true'
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubygems_version: 3.5.22
|
65
|
+
signing_key:
|
66
|
+
specification_version: 4
|
67
|
+
summary: A simple tagging gem for Rails using PostgreSQL array.
|
68
|
+
test_files: []
|