make_taggable 1.0.0 → 1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be8e033e4d750a0bc75a77f652351843c096f38d8f8332f56e9d642e2085f12b
4
- data.tar.gz: 698531499c7e7636b241b854cf60e2e2aee3799af3dba3d2d8a5f1ae2ff954f1
3
+ metadata.gz: 9313e4b6fc5d1b81f44b56665f2dbf6c6a8c8a8213f492b00e17e1e20c0a27bb
4
+ data.tar.gz: ef32dbb42368817bea0791803f1013f1481d58ca50992551131483b7fac416ab
5
5
  SHA512:
6
- metadata.gz: c2f772cac980b2bab6c101c087916f3083f519aaec61c8c45ad4cd8750ab95dc6f9534d4ac2a511a6411872590dcfb4a58c3af7f68611458365ff8e5ce024866
7
- data.tar.gz: 50aae450f2dfd5923ecf8c18a348b6e7e27b761d71d7fd59b61498c338341f607b735194b2a56da154624ad71e84a748f3a3aebec84beb0a6a21b3a7d0f244b0
6
+ metadata.gz: 750c882a7d5836848cfd7d536896403cb619868121a817d9aa89ac2aed7e36d6f3d9cf70be5be04e1dd428e35484957b1f49ba17a2e7ef50f2a67adc271d7510
7
+ data.tar.gz: d14dba688a46a5e48507a082d7b65071aa8e0197df657156c3859652fcabd8eca5e0c5256033f7a9c4665d197c87177f0d9107c010e811d671fc1de2011b4d1f
data/CHANGELOG.md CHANGED
@@ -5,7 +5,26 @@ 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.0.0] - unreleased
8
+ ## [1.1.0] - 2026-08-22
9
+
10
+ ### Added
11
+
12
+ - A migration adding `taggings_unowned_idx`, a partial unique index that stops duplicate unowned
13
+ taggings at the database level. `taggings_idx` never could: it spans the nullable tagger columns,
14
+ and SQL compares nulls as distinct, so only the model validation stood in the way and a validation
15
+ cannot win a race. MySQL has no partial indexes, so the migration is a no-op there.
16
+
17
+ Install it with `rails make_taggable_engine:install:migrations`. If it fails, the table already
18
+ holds duplicates -- see [docs/database.md](docs/database.md).
19
+
20
+ ### Internal
21
+
22
+ - The suite runs in random order. Examples were leaking state into each other: tagging declarations
23
+ on the shared models, and the library configuration -- one example leaving `remove_unused_tags`
24
+ on could make an unrelated example fail a foreign key check. Both are now snapshotted and restored
25
+ around every example.
26
+
27
+ ## [1.0.0] - 2026-08-22
9
28
 
10
29
  ### Breaking
11
30
 
data/CONTRIBUTING.md CHANGED
@@ -47,10 +47,16 @@ bundle exec appraisal rake
47
47
 
48
48
  ### A note on test ordering
49
49
 
50
- The suite runs in defined order. Several examples mutate shared model classes adding a context to
51
- `TaggableModel`, flipping `preserve_tag_order` and rely on siblings having run first. Randomising
52
- the order exposes this. Making those examples self-contained is welcome work; until then, please
53
- don't add new examples that depend on a sibling's side effects.
50
+ The suite runs in random order, so an example that depends on a sibling having run first will fail
51
+ sooner or later. Reproduce a failure with the seed it reports:
52
+
53
+ ```shell
54
+ bundle exec rspec --order random:12345
55
+ ```
56
+
57
+ Examples are free to change global state — the tagging declarations on the shared models, and the
58
+ `MakeTaggable` configuration — because `spec_helper.rb` snapshots both and restores them after every
59
+ example. Anything else you make global is yours to clean up.
54
60
 
55
61
  ## Documentation
56
62
 
data/UPGRADING.md CHANGED
@@ -23,6 +23,10 @@ This release renames the declaration methods and changes how delimiters are esca
23
23
 
24
24
  4. Requirements are now Ruby 3.2 and Active Record 7.2 or newer.
25
25
 
26
+ Note for anyone upgrading past 1.0: a later migration adds a unique index
27
+ preventing duplicate unowned taggings. If it fails, your taggings table
28
+ already holds duplicates -- docs/database.md has a snippet to clear them.
29
+
26
30
  Install any new migrations:
27
31
 
28
32
  rails make_taggable_engine:install:migrations
@@ -0,0 +1,18 @@
1
+ class AddUnownedTaggingsUniqueIndex < ActiveRecord::Migration[7.2]
2
+ # taggings_idx spans tagger_id and tagger_type, which are NULL on every
3
+ # tagging nobody owns. SQL compares NULLs as distinct, so that index does not
4
+ # stop two identical unowned taggings -- only the model validation does, and a
5
+ # validation cannot win a race between two concurrent writes.
6
+ #
7
+ # A partial index closes it. MySQL has no partial indexes, so it keeps the
8
+ # validation on its own.
9
+ def change
10
+ return if MakeTaggable::Utils.using_mysql?
11
+
12
+ add_index MakeTaggable.taggings_table,
13
+ [:tag_id, :taggable_id, :taggable_type, :context],
14
+ unique: true,
15
+ where: "tagger_id IS NULL",
16
+ name: "taggings_unowned_idx"
17
+ end
18
+ end
data/docs/database.md CHANGED
@@ -34,22 +34,39 @@ That suits read-heavy tagging. If your application writes taggings in bulk, the
34
34
  looking at — every index is maintained on insert, and several of the standalone ones are prefixes of
35
35
  composites that already exist. Drop what your queries do not use.
36
36
 
37
- ### The unique index does not stop duplicate unowned taggings
37
+ ### Duplicate unowned taggings
38
38
 
39
39
  `taggings_idx` is unique across
40
40
  `[tag_id, taggable_id, taggable_type, context, tagger_id, tagger_type]`. Because `tagger_id` and
41
- `tagger_type` are null for unowned taggings, and SQL treats nulls as distinct, **the database will
42
- accept two identical unowned taggings**. Only the Active Record uniqueness validation prevents them,
43
- and a validation cannot prevent a race between two concurrent writes.
41
+ `tagger_type` are null on every tagging nobody owns, and SQL compares nulls as distinct, that index
42
+ does not stop two identical unowned taggings. Only the model validation does, and a validation
43
+ cannot win a race between two concurrent writes.
44
44
 
45
- If duplicate taggings would be a problem for you, add a partial unique index. On PostgreSQL:
45
+ Migration 6 closes it with a partial unique index, `taggings_unowned_idx`, covering
46
+ `[tag_id, taggable_id, taggable_type, context]` where `tagger_id IS NULL`.
47
+
48
+ **MySQL has no partial indexes**, so it keeps the validation on its own and the migration is a no-op
49
+ there. If duplicate taggings would be a serious problem on MySQL, the usual workaround is a
50
+ generated column holding a sentinel for the null tagger, indexed uniquely alongside the rest.
51
+
52
+ If the migration fails with a uniqueness error, the table already contains duplicates. Remove them
53
+ first:
46
54
 
47
55
  ```ruby
48
- add_index :taggings,
49
- [:tag_id, :taggable_id, :taggable_type, :context],
50
- unique: true,
51
- where: "tagger_id IS NULL",
52
- name: "taggings_unowned_idx"
56
+ duplicates = MakeTaggable::Tagging
57
+ .where(tagger_id: nil)
58
+ .group(:tag_id, :taggable_id, :taggable_type, :context)
59
+ .having("COUNT(*) > 1")
60
+ .pluck(Arel.sql("MIN(id), COUNT(*)"))
61
+
62
+ duplicates.each do |keep_id, _count|
63
+ tagging = MakeTaggable::Tagging.find(keep_id)
64
+ MakeTaggable::Tagging
65
+ .where(tagger_id: nil, tag_id: tagging.tag_id, taggable_id: tagging.taggable_id,
66
+ taggable_type: tagging.taggable_type, context: tagging.context)
67
+ .where.not(id: keep_id)
68
+ .delete_all
69
+ end
53
70
  ```
54
71
 
55
72
  ## PostgreSQL
@@ -6,5 +6,5 @@ module MakeTaggable
6
6
  #
7
7
  # @return [String]
8
8
  #
9
- VERSION = "1.0.0"
9
+ VERSION = "1.1.0"
10
10
  end
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.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matthew Kennedy
@@ -46,6 +46,7 @@ files:
46
46
  - db/migrate/3_change_tag_name_collation_mysql.rb
47
47
  - db/migrate/4_add_index_to_tags.rb
48
48
  - db/migrate/5_add_index_to_taggings.rb
49
+ - db/migrate/6_add_unowned_taggings_unique_index.rb
49
50
  - docs/caching.md
50
51
  - docs/configuration.md
51
52
  - docs/contexts.md
@@ -116,6 +117,10 @@ post_install_message: |
116
117
 
117
118
  4. Requirements are now Ruby 3.2 and Active Record 7.2 or newer.
118
119
 
120
+ Note for anyone upgrading past 1.0: a later migration adds a unique index
121
+ preventing duplicate unowned taggings. If it fails, your taggings table
122
+ already holds duplicates -- docs/database.md has a snippet to clear them.
123
+
119
124
  Install any new migrations:
120
125
 
121
126
  rails make_taggable_engine:install:migrations