make_taggable 1.1.1 → 1.2.1
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 +4 -4
- data/CHANGELOG.md +56 -0
- data/db/migrate/7_remove_redundant_tagging_indexes.rb +38 -0
- data/docs/configuration.md +8 -4
- data/docs/contexts.md +17 -0
- data/docs/database.md +41 -6
- data/docs/querying.md +9 -1
- data/lib/make_taggable/tag.rb +8 -2
- data/lib/make_taggable/taggable/core.rb +5 -2
- data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +12 -6
- data/lib/make_taggable/taggable.rb +27 -0
- data/lib/make_taggable/tagging.rb +14 -1
- data/lib/make_taggable/utils.rb +15 -2
- data/lib/make_taggable/version.rb +1 -1
- data/lib/make_taggable.rb +2 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 99cb64f83f0cef74cc21f3fc09a341e62db5a2248b8c88ef5d720de4b9446abb
|
|
4
|
+
data.tar.gz: c3c08be5a20df6100567523555be2394dc32665770f6c65226e0fe91b3be5652
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad8701479ff9565786acebff64af05fe5aaa88eddbcfc660f88778e4ca885a0d26438677754cfa3072dd54e3eef6374ebed4f1e4de6a70fe911824050cb0b19e
|
|
7
|
+
data.tar.gz: 47804d66800ff4df491f3b7037ba5e82c131fbaf4c9eb8442a650ed6ecbf1772483d2ae2237c5437cb8ffefca831d107744a3ffacb2c517cb65a161063bc6d0e
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,62 @@ All notable changes to this project are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
6
6
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.1] - 2026-08-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `tagged_with(..., exclude: true)` raised on any model whose primary key is not `id`. The exclude
|
|
13
|
+
strategy built its `NOT IN` predicate against a hardcoded `id` column, where the other two
|
|
14
|
+
strategies already used the model's primary key.
|
|
15
|
+
|
|
16
|
+
- `tagged_with(..., on: <context>, exclude: true)` ignored the context entirely, gathering taggings
|
|
17
|
+
from every context, so a record tagged in one context was excluded from a query about another.
|
|
18
|
+
|
|
19
|
+
- `tagged_with([], exclude: true)` returned nothing rather than everything. Excluding no tags rules
|
|
20
|
+
nothing out, so the whole scope now stands. This restores the property that `tagged_with(list)`
|
|
21
|
+
and `tagged_with(list, exclude: true)` partition the scope between them for any list.
|
|
22
|
+
|
|
23
|
+
- A tag context whose name cannot become a Ruby method name -- one starting with a digit, say --
|
|
24
|
+
raised `SyntaxError` while the model was loading, from inside Active Record's association
|
|
25
|
+
builder. Since `SyntaxError` is not a `StandardError` it slipped past application rescues.
|
|
26
|
+
Contexts are now checked as they are declared and rejected with an `ArgumentError` naming the
|
|
27
|
+
context. Non-ASCII context names keep working.
|
|
28
|
+
|
|
29
|
+
### Internal
|
|
30
|
+
|
|
31
|
+
- The suite's schema teardown no longer depends on the order `connection.tables` returns. MySQL
|
|
32
|
+
ignores `DROP TABLE ... CASCADE` for foreign keys, so it only worked because `taggings` happens
|
|
33
|
+
to sort before `tags`.
|
|
34
|
+
|
|
35
|
+
## [1.2.0] - 2026-08-23
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- `Tag.find_or_create_all_with_like_by_name` recovered from a lost race for a tag name by issuing a
|
|
40
|
+
raw `ROLLBACK`. That statement is not scoped to the failed insert -- it discarded whatever
|
|
41
|
+
transaction was open on the connection, which is nearly always one the caller opened, and on a
|
|
42
|
+
multi-database application it targeted whichever connection `ActiveRecord::Base` held rather than
|
|
43
|
+
the one the tags are on. Each insert now takes a savepoint of its own.
|
|
44
|
+
|
|
45
|
+
- `remove_unused_tags` did nothing at all when `tags_counter` was off, because the check read the
|
|
46
|
+
counter cache. With the counter off it now asks the tag's taggings directly, at the cost of one
|
|
47
|
+
indexed lookup per destroyed tagging. The documentation said the setting required `tags_counter`;
|
|
48
|
+
it no longer does.
|
|
49
|
+
|
|
50
|
+
- `Utils.using_postgresql?` matched only the adapter named `PostgreSQL`, so a PostGIS application
|
|
51
|
+
took the MySQL path -- `LIKE` in place of `ILIKE`, which quietly made tag matching
|
|
52
|
+
case-sensitive, and the wrong grouping strategy in `all_tags_on` and `find_related_*`.
|
|
53
|
+
|
|
54
|
+
### Changed
|
|
55
|
+
|
|
56
|
+
- A migration dropping five indexes from `taggings` that no query planner can reach: `tag_id`,
|
|
57
|
+
`taggable_id`, `taggable_type` and `tagger_id` on their own, each a leading column of an index
|
|
58
|
+
that remains, plus a second copy of the tagger pair in the opposite column order. Twelve indexes
|
|
59
|
+
become seven, and every one of them is maintained on insert.
|
|
60
|
+
|
|
61
|
+
Install it with `rails make_taggable_engine:install:migrations`. It is reversible -- see
|
|
62
|
+
[docs/database.md](docs/database.md).
|
|
63
|
+
|
|
8
64
|
## [1.1.1] - 2026-08-22
|
|
9
65
|
|
|
10
66
|
### Fixed
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class RemoveRedundantTaggingIndexes < ActiveRecord::Migration[7.2]
|
|
2
|
+
# Migrations 2 and 5 between them put twelve indexes on the taggings table.
|
|
3
|
+
# Five of them earn nothing: a B-tree index already answers any query that
|
|
4
|
+
# filters on a leading subset of its columns, so an index on a single column
|
|
5
|
+
# is dead weight whenever another index starts with that same column.
|
|
6
|
+
#
|
|
7
|
+
# tag_id -> covered by taggings_idx and taggings_unowned_idx
|
|
8
|
+
# taggable_id -> covered by taggings_taggable_context_idx and taggings_idy
|
|
9
|
+
# taggable_type -> covered by index_taggings_on_taggable_type_and_taggable_id
|
|
10
|
+
# tagger_id -> covered by index_taggings_on_tagger_id_and_tagger_type
|
|
11
|
+
#
|
|
12
|
+
# The fifth is the tagger pair, which migration 2 and migration 5 each added
|
|
13
|
+
# in opposite column orders. One of the two is enough; the order kept is the
|
|
14
|
+
# one the library's own queries filter in, tagger_id first.
|
|
15
|
+
#
|
|
16
|
+
# Every index is maintained on insert, so this is a write-cost and a
|
|
17
|
+
# disk-footprint change, not a query-plan one. Nothing here is load-bearing:
|
|
18
|
+
# each dropped index is a prefix of one that remains.
|
|
19
|
+
REDUNDANT_COLUMNS = [
|
|
20
|
+
[:tag_id],
|
|
21
|
+
[:taggable_id],
|
|
22
|
+
[:taggable_type],
|
|
23
|
+
[:tagger_id],
|
|
24
|
+
[:tagger_type, :tagger_id]
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def up
|
|
28
|
+
REDUNDANT_COLUMNS.each do |columns|
|
|
29
|
+
remove_index MakeTaggable.taggings_table, column: columns if index_exists?(MakeTaggable.taggings_table, columns)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def down
|
|
34
|
+
REDUNDANT_COLUMNS.each do |columns|
|
|
35
|
+
add_index MakeTaggable.taggings_table, columns unless index_exists?(MakeTaggable.taggings_table, columns)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/docs/configuration.md
CHANGED
|
@@ -51,14 +51,18 @@ The library avoids creating both, but data loaded another way can still contain
|
|
|
51
51
|
|
|
52
52
|
### `remove_unused_tags`
|
|
53
53
|
|
|
54
|
-
When the last tagging referencing a tag is destroyed, the tag row is destroyed too.
|
|
55
|
-
|
|
54
|
+
When the last tagging referencing a tag is destroyed, the tag row is destroyed too.
|
|
55
|
+
|
|
56
|
+
This works whether or not `tags_counter` is on. With the counter cache the tag row already carries
|
|
57
|
+
the count and is only re-read; without it the tag's remaining taggings are queried directly, which
|
|
58
|
+
costs one extra query per destroyed tagging.
|
|
56
59
|
|
|
57
60
|
### `tags_counter`
|
|
58
61
|
|
|
59
62
|
Keeps `tags.taggings_count` up to date. Turning it off avoids a write to the tags row on every
|
|
60
|
-
tagging, at the cost of `Tag.most_used
|
|
61
|
-
|
|
63
|
+
tagging, at the cost of `Tag.most_used` and `Tag.least_used`, both of which read that counter.
|
|
64
|
+
|
|
65
|
+
`remove_unused_tags` is unaffected -- it falls back to querying the tag's taggings.
|
|
62
66
|
|
|
63
67
|
Changing this on an existing application leaves the existing counts frozen at their current values
|
|
64
68
|
rather than resetting them.
|
data/docs/contexts.md
CHANGED
|
@@ -40,6 +40,23 @@ user.skill_list_change # => [["jogging"], ["diving"]]
|
|
|
40
40
|
user.will_save_change_to_skill_list?
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
### Naming a context
|
|
44
|
+
|
|
45
|
+
Because the context becomes part of every name in the table above, it has to be usable as a Ruby
|
|
46
|
+
method and instance variable name. A context starting with a digit, or containing a hyphen or a
|
|
47
|
+
space, is rejected when the model declares it:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
class Book < ActiveRecord::Base
|
|
51
|
+
make_taggable :"1categories"
|
|
52
|
+
end
|
|
53
|
+
# => ArgumentError: :"1categories" cannot be used as a tag context: make_taggable generates
|
|
54
|
+
# methods and instance variables from it, and "1categories_list" is not a valid Ruby name.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Validity is decided by asking Ruby, not by a pattern, so anything that makes a legal method name is
|
|
58
|
+
allowed — non-ASCII context names included.
|
|
59
|
+
|
|
43
60
|
## Adding contexts later
|
|
44
61
|
|
|
45
62
|
Calling `make_taggable` again adds contexts rather than replacing them, which is what lets a
|
data/docs/database.md
CHANGED
|
@@ -26,13 +26,48 @@ Both table names are configurable — see [configuration.md](configuration.md).
|
|
|
26
26
|
|
|
27
27
|
## Indexes
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
plus four composites.
|
|
29
|
+
Migrations 2 and 5 between them put twelve indexes on `taggings`. Migration 7 drops five of them,
|
|
30
|
+
leaving seven:
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
| Index | Columns | Why it is there |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `taggings_idx` | `tag_id, taggable_id, taggable_type, context, tagger_id, tagger_type` | Unique. Stops a tag being applied twice in the same context by the same tagger |
|
|
35
|
+
| `taggings_unowned_idx` | `tag_id, taggable_id, taggable_type, context` where `tagger_id IS NULL` | Unique, partial. See below. Not created on MySQL |
|
|
36
|
+
| `taggings_taggable_context_idx` | `taggable_id, taggable_type, context` | Reading one record's tags in one context |
|
|
37
|
+
| `taggings_idy` | `taggable_id, taggable_type, tagger_id, context` | Reading one record's owned tags |
|
|
38
|
+
| — | `taggable_type, taggable_id` | The polymorphic association lookup |
|
|
39
|
+
| — | `tagger_id, tagger_type` | `Tagging.owned_by`, and `tagged_with(owned_by:)` |
|
|
40
|
+
| — | `context` | Filtering taggings by context alone |
|
|
41
|
+
|
|
42
|
+
The five migration 7 removes were `tag_id`, `taggable_id`, `taggable_type` and `tagger_id` on their
|
|
43
|
+
own, plus a second copy of the tagger pair in the opposite column order. A B-tree index already
|
|
44
|
+
answers any query filtering on a leading subset of its columns, so a single-column index earns
|
|
45
|
+
nothing when another index starts with that same column — and every index is maintained on insert.
|
|
46
|
+
|
|
47
|
+
If you installed the migrations before version 1.2.0, running
|
|
48
|
+
`rails make_taggable_engine:install:migrations` again copies migration 7 across. It is reversible,
|
|
49
|
+
and nothing it drops is load-bearing: each one is a prefix of an index that remains.
|
|
50
|
+
|
|
51
|
+
### Concurrent tag creation
|
|
52
|
+
|
|
53
|
+
`tags.name` carries a unique index, so two requests creating the same tag at the same moment leave
|
|
54
|
+
one of them holding an `ActiveRecord::RecordNotUnique`.
|
|
55
|
+
|
|
56
|
+
`Tag.find_or_create_all_with_like_by_name` -- which every tag list goes through -- absorbs that. It
|
|
57
|
+
re-reads the tags and retries, up to three times, and raises
|
|
58
|
+
{MakeTaggable::DuplicateTagError} if the name is still taken after that.
|
|
59
|
+
|
|
60
|
+
Each insert takes a savepoint of its own, so the failed insert is the only thing rolled back. If you
|
|
61
|
+
call this inside a transaction of your own, that transaction and everything written into it survive
|
|
62
|
+
the retry:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
Order.transaction do
|
|
66
|
+
order.update!(state: "placed")
|
|
67
|
+
order.tag_list = "priority" # a race here rolls back nothing but the tag insert
|
|
68
|
+
order.save!
|
|
69
|
+
end
|
|
70
|
+
```
|
|
36
71
|
|
|
37
72
|
### Duplicate unowned taggings
|
|
38
73
|
|
data/docs/querying.md
CHANGED
|
@@ -28,12 +28,20 @@ empty relation rather than every record — worth knowing when the tags come fro
|
|
|
28
28
|
| `:exclude` | Match records carrying none of the tags |
|
|
29
29
|
| `:match_all` | Match records carrying only these tags and no others |
|
|
30
30
|
| `:wild` | Match tags *containing* the given text, i.e. `%sci%` |
|
|
31
|
-
| `:on` | Restrict to one context |
|
|
31
|
+
| `:on` | Restrict to one context. Honoured by every option, `:exclude` included |
|
|
32
32
|
| `:owned_by` | Restrict to tags applied by one tagger |
|
|
33
33
|
| `:order_by_matching_tag_count` | With `:any`, order by how many tags matched, most first |
|
|
34
34
|
| `:start_at` | Only tags applied after this time |
|
|
35
35
|
| `:end_at` | Only tags applied before this time |
|
|
36
36
|
|
|
37
|
+
An empty tag list means "nothing matches" for the matching options and "nothing is ruled out" for
|
|
38
|
+
`:exclude`, so the two always partition the scope between them:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Book.tagged_with([]) # => none
|
|
42
|
+
Book.tagged_with([], exclude: true) # => every book
|
|
43
|
+
```
|
|
44
|
+
|
|
37
45
|
`:wild` combines with `:any` or `:exclude`:
|
|
38
46
|
|
|
39
47
|
```ruby
|
data/lib/make_taggable/tag.rb
CHANGED
|
@@ -139,6 +139,9 @@ module MakeTaggable
|
|
|
139
139
|
# Finds every tag in a list by name, creating those that do not exist yet.
|
|
140
140
|
#
|
|
141
141
|
# A competing write that takes a name first is retried up to three times before giving up.
|
|
142
|
+
# Each insert runs in a savepoint of its own, so a name lost to a race unwinds that insert
|
|
143
|
+
# alone -- an enclosing transaction the caller opened is left untouched, along with everything
|
|
144
|
+
# written into it.
|
|
142
145
|
#
|
|
143
146
|
# @param list [Array<String>] the tag names
|
|
144
147
|
# @return [Array<MakeTaggable::Tag>] in the order the names were given
|
|
@@ -162,10 +165,13 @@ module MakeTaggable
|
|
|
162
165
|
# Tags created earlier in this call have to stay visible to the names
|
|
163
166
|
# that follow, or a list holding both "Ruby" and "ruby" resolves to two
|
|
164
167
|
# rows even though the two names compare equal.
|
|
165
|
-
|
|
168
|
+
#
|
|
169
|
+
# The insert gets a savepoint of its own so that a RecordNotUnique
|
|
170
|
+
# unwinds only the failed insert. Without one the caller's transaction
|
|
171
|
+
# is left in an aborted state and everything it had done is lost.
|
|
172
|
+
transaction(requires_new: true) { create(name: tag_name) }.tap { |tag| existing_tags << tag }
|
|
166
173
|
rescue ActiveRecord::RecordNotUnique
|
|
167
174
|
if (tries -= 1).positive?
|
|
168
|
-
ActiveRecord::Base.connection.execute "ROLLBACK"
|
|
169
175
|
existing_tags = named_any(list).to_a
|
|
170
176
|
retry
|
|
171
177
|
end
|
|
@@ -129,7 +129,8 @@ module MakeTaggable::Taggable
|
|
|
129
129
|
# @option options [Symbol, String] :on only tags applied in this context
|
|
130
130
|
# @option options [Time, Date] :start_at only tags applied after this time
|
|
131
131
|
# @option options [Time, Date] :end_at only tags applied before this time
|
|
132
|
-
# @return [ActiveRecord::Relation] empty when no tags are given
|
|
132
|
+
# @return [ActiveRecord::Relation] empty when no tags are given, except under `:exclude`,
|
|
133
|
+
# where excluding no tags leaves the whole scope standing
|
|
133
134
|
#
|
|
134
135
|
# @example Every tag
|
|
135
136
|
# User.tagged_with(["awesome", "cool"])
|
|
@@ -144,7 +145,9 @@ module MakeTaggable::Taggable
|
|
|
144
145
|
tag_list = MakeTaggable.default_parser.new(tags).parse
|
|
145
146
|
options = options.dup
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
# Asking for no tags matches nothing, but *excluding* no tags rules
|
|
149
|
+
# nothing out, so the whole scope stands.
|
|
150
|
+
return options[:exclude].present? ? all : none if tag_list.empty?
|
|
148
151
|
|
|
149
152
|
::MakeTaggable::Taggable::TaggedWithQuery.build(self, MakeTaggable::Tag, MakeTaggable::Tagging, tag_list, options)
|
|
150
153
|
end
|
|
@@ -20,15 +20,21 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
22
|
def tags_not_in_list
|
|
23
|
-
|
|
23
|
+
on_condition = tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
|
|
24
|
+
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
|
|
25
|
+
.and(tags_match_type)
|
|
26
|
+
|
|
27
|
+
# Without this the subquery gathers taggings from every context, so a
|
|
28
|
+
# record tagged in one context is excluded from a query about another.
|
|
29
|
+
if options[:on].present?
|
|
30
|
+
on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on]))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
taggable_arel_table[taggable_model.primary_key].not_in(
|
|
24
34
|
tagging_arel_table
|
|
25
35
|
.project(tagging_arel_table[:taggable_id])
|
|
26
36
|
.join(tag_arel_table)
|
|
27
|
-
.on(
|
|
28
|
-
tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
|
|
29
|
-
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
|
|
30
|
-
.and(tags_match_type)
|
|
31
|
-
)
|
|
37
|
+
.on(on_condition)
|
|
32
38
|
)
|
|
33
39
|
|
|
34
40
|
# FIXME: missing time scope, this is also missing in the original implementation
|
|
@@ -69,6 +69,7 @@ module MakeTaggable
|
|
|
69
69
|
#
|
|
70
70
|
def taggable_on(preserve_tag_order, *tag_types)
|
|
71
71
|
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
|
72
|
+
tag_types.each { |tag_type| validate_tag_context!(tag_type) }
|
|
72
73
|
|
|
73
74
|
if taggable?
|
|
74
75
|
self.tag_types = (self.tag_types + tag_types).uniq
|
|
@@ -97,5 +98,31 @@ module MakeTaggable
|
|
|
97
98
|
include Ownership
|
|
98
99
|
include Related
|
|
99
100
|
end
|
|
101
|
+
|
|
102
|
+
# Rejects a context that cannot become the methods and instance variables the
|
|
103
|
+
# library generates from it.
|
|
104
|
+
#
|
|
105
|
+
# A context is interpolated straight into generated source -- `#{context}_list`,
|
|
106
|
+
# `#{context}_taggings`, `@#{context}_list`. A name that is not a valid identifier
|
|
107
|
+
# produces source Ruby cannot parse, and the resulting SyntaxError descends from
|
|
108
|
+
# ScriptError rather than StandardError, so it slips past an application's own
|
|
109
|
+
# rescue and takes the boot down pointing at Active Record's association builder
|
|
110
|
+
# rather than at the offending declaration.
|
|
111
|
+
#
|
|
112
|
+
# Validity is decided by asking Ruby rather than by pattern, so a context is
|
|
113
|
+
# accepted on exactly the terms the generated code needs -- non-ASCII names
|
|
114
|
+
# included, since those make perfectly good method names.
|
|
115
|
+
#
|
|
116
|
+
# @param tag_type [Symbol] the context being declared
|
|
117
|
+
# @return [void]
|
|
118
|
+
# @raise [ArgumentError] when the context cannot become an identifier
|
|
119
|
+
def validate_tag_context!(tag_type)
|
|
120
|
+
Object.new.instance_variable_defined?(:"@#{tag_type}_list")
|
|
121
|
+
rescue NameError
|
|
122
|
+
raise ArgumentError,
|
|
123
|
+
"#{tag_type.inspect} cannot be used as a tag context: " \
|
|
124
|
+
"make_taggable generates methods and instance variables from it, and " \
|
|
125
|
+
"\"#{tag_type}_list\" is not a valid Ruby name."
|
|
126
|
+
end
|
|
100
127
|
end
|
|
101
128
|
end
|
|
@@ -50,8 +50,21 @@ module MakeTaggable
|
|
|
50
50
|
|
|
51
51
|
private
|
|
52
52
|
|
|
53
|
+
# Destroys the tag this tagging pointed at, if nothing else references it and the library is
|
|
54
|
+
# configured to clean up after itself. Runs after destroy.
|
|
55
|
+
#
|
|
56
|
+
# How "nothing else references it" is established depends on the counter cache. With
|
|
57
|
+
# `tags_counter` on, the count is already on the row and only needs re-reading. With it off
|
|
58
|
+
# there is no count to read, so the taggings are asked directly -- `none?` stops at the first
|
|
59
|
+
# row rather than counting them all.
|
|
60
|
+
#
|
|
61
|
+
# @return [void]
|
|
53
62
|
def remove_unused_tags
|
|
54
|
-
|
|
63
|
+
return unless MakeTaggable.remove_unused_tags
|
|
64
|
+
|
|
65
|
+
if MakeTaggable.tags_counter
|
|
66
|
+
tag.destroy if tag.reload.taggings_count.zero?
|
|
67
|
+
elsif tag.taggings.reload.none?
|
|
55
68
|
tag.destroy
|
|
56
69
|
end
|
|
57
70
|
end
|
data/lib/make_taggable/utils.rb
CHANGED
|
@@ -5,6 +5,17 @@ module MakeTaggable
|
|
|
5
5
|
# Database differences the rest of the library needs to work around.
|
|
6
6
|
#
|
|
7
7
|
module Utils
|
|
8
|
+
##
|
|
9
|
+
# Adapter names that are PostgreSQL as far as SQL generation is concerned.
|
|
10
|
+
#
|
|
11
|
+
# PostGIS is the PostgreSQL adapter with spatial types layered on top. It reports its own
|
|
12
|
+
# adapter name, so it has to be named here or the library treats a PostGIS application as
|
|
13
|
+
# though it were on MySQL -- `LIKE` in place of `ILIKE`, and the wrong grouping strategy.
|
|
14
|
+
#
|
|
15
|
+
# @return [Array<String>] frozen
|
|
16
|
+
#
|
|
17
|
+
POSTGRESQL_ADAPTER_NAMES = %w[PostgreSQL PostGIS].freeze
|
|
18
|
+
|
|
8
19
|
class << self
|
|
9
20
|
##
|
|
10
21
|
# The connection tags are read and written through.
|
|
@@ -18,10 +29,12 @@ module MakeTaggable
|
|
|
18
29
|
##
|
|
19
30
|
# Whether tags are stored in PostgreSQL.
|
|
20
31
|
#
|
|
32
|
+
# True for PostGIS as well, which is PostgreSQL underneath.
|
|
33
|
+
#
|
|
21
34
|
# @return [TrueClass, FalseClass]
|
|
22
35
|
#
|
|
23
36
|
def using_postgresql?
|
|
24
|
-
connection && connection.adapter_name
|
|
37
|
+
!!connection && POSTGRESQL_ADAPTER_NAMES.include?(connection.adapter_name)
|
|
25
38
|
end
|
|
26
39
|
|
|
27
40
|
##
|
|
@@ -47,7 +60,7 @@ module MakeTaggable
|
|
|
47
60
|
##
|
|
48
61
|
# The case-insensitive pattern operator for the current adapter.
|
|
49
62
|
#
|
|
50
|
-
# @return [String] `"ILIKE"` on PostgreSQL, otherwise `"LIKE"`
|
|
63
|
+
# @return [String] `"ILIKE"` on PostgreSQL and PostGIS, otherwise `"LIKE"`
|
|
51
64
|
#
|
|
52
65
|
def like_operator
|
|
53
66
|
using_postgresql? ? "ILIKE" : "LIKE"
|
data/lib/make_taggable.rb
CHANGED
|
@@ -141,7 +141,8 @@ module MakeTaggable
|
|
|
141
141
|
# Whether tag names are parameterized before they are saved.
|
|
142
142
|
# @return [TrueClass, FalseClass] defaults to `false`
|
|
143
143
|
# @!attribute [rw] remove_unused_tags
|
|
144
|
-
# Whether a tag row is destroyed once its last tagging goes away.
|
|
144
|
+
# Whether a tag row is destroyed once its last tagging goes away. Works with or without
|
|
145
|
+
# `tags_counter`; without it the check costs one extra query per destroyed tagging.
|
|
145
146
|
# @return [TrueClass, FalseClass] defaults to `false`
|
|
146
147
|
# @!attribute [rw] default_parser
|
|
147
148
|
# The class used to turn tag input into a {MakeTaggable::TagList}.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: make_taggable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matthew Kennedy
|
|
@@ -47,6 +47,7 @@ files:
|
|
|
47
47
|
- db/migrate/4_add_index_to_tags.rb
|
|
48
48
|
- db/migrate/5_add_index_to_taggings.rb
|
|
49
49
|
- db/migrate/6_add_unowned_taggings_unique_index.rb
|
|
50
|
+
- db/migrate/7_remove_redundant_tagging_indexes.rb
|
|
50
51
|
- docs/caching.md
|
|
51
52
|
- docs/configuration.md
|
|
52
53
|
- docs/contexts.md
|