metka 2.3.3 → 3.0.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint_code.yml +9 -6
  3. data/.github/workflows/lint_docs.yml +15 -16
  4. data/.github/workflows/release.yml +23 -0
  5. data/.github/workflows/tests.yml +97 -0
  6. data/.gitignore +16 -7
  7. data/.rubocop.yml +8 -16
  8. data/.ruby-version +1 -1
  9. data/Gemfile +1 -1
  10. data/README.md +273 -236
  11. data/Rakefile +9 -4
  12. data/assets/metka-icon.svg +23 -0
  13. data/assets/metka-logo.svg +28 -0
  14. data/benchmark/Gemfile +17 -0
  15. data/benchmark/README.md +170 -0
  16. data/benchmark/benchmark.rb +718 -0
  17. data/benchmark/results.sqlite.txt +131 -0
  18. data/benchmark/results.txt +150 -0
  19. data/bin/setup +7 -0
  20. data/docs/superpowers/plans/2026-08-18-cloud-table-naming.md +316 -0
  21. data/docs/superpowers/specs/2026-08-18-cloud-table-naming-design.md +105 -0
  22. data/forspell.dict +3 -1
  23. data/gemfiles/rails71.gemfile +6 -0
  24. data/gemfiles/rails72.gemfile +6 -0
  25. data/gemfiles/rails80.gemfile +6 -0
  26. data/gemfiles/rails81.gemfile +6 -0
  27. data/gemfiles/rubocop.gemfile +2 -2
  28. data/lib/generators/metka/strategies/index/index_generator.rb +77 -0
  29. data/lib/generators/metka/strategies/index/templates/migration.rb.erb +89 -0
  30. data/lib/generators/metka/strategies/table/table_generator.rb +83 -0
  31. data/lib/generators/metka/strategies/table/templates/migration.rb.erb +128 -0
  32. data/lib/generators/metka/strategies/table/templates/migration.sqlite.rb.erb +93 -0
  33. data/lib/metka/generic_parser.rb +28 -14
  34. data/lib/metka/model.rb +121 -51
  35. data/lib/metka/query_builder.rb +45 -49
  36. data/lib/metka/tag_list.rb +16 -8
  37. data/lib/metka/tags_query.rb +93 -0
  38. data/lib/metka/version.rb +1 -1
  39. data/lib/metka.rb +24 -10
  40. data/metka.gemspec +21 -16
  41. metadata +45 -92
  42. data/.github/workflows/specs.yml +0 -86
  43. data/.rspec +0 -2
  44. data/Gemfile.lock +0 -201
  45. data/gemfiles/rails52.gemfile +0 -6
  46. data/gemfiles/rails6.gemfile +0 -6
  47. data/gemfiles/rails61.gemfile +0 -6
  48. data/lib/generators/metka/strategies/materialized_view/materialized_view_generator.rb +0 -73
  49. data/lib/generators/metka/strategies/materialized_view/templates/migration.rb.erb +0 -54
  50. data/lib/generators/metka/strategies/view/templates/migration.rb.erb +0 -26
  51. data/lib/generators/metka/strategies/view/view_generator.rb +0 -70
  52. data/lib/metka/query_builder/all_tags_query.rb +0 -11
  53. data/lib/metka/query_builder/any_tags_query.rb +0 -11
  54. data/lib/metka/query_builder/base_query.rb +0 -48
@@ -0,0 +1,131 @@
1
+ Seeding 10000 posts per gem, 5 tags each, vocabulary of 100 tags
2
+
3
+ Bulk seed wall time (10000 posts):
4
+ metka 0.10 s
5
+ metka (table) 0.08 s
6
+ metka (index) 0.10 s
7
+ acts-as-taggable-on 31.43 s
8
+ gutentag 35.30 s
9
+
10
+ Storage (tables + indexes) for 10000 posts:
11
+ metka 0.62 MB
12
+ metka (table) 0.63 MB
13
+ acts-as-taggable-on 12.53 MB
14
+ gutentag 7.20 MB
15
+ metka (index) 1.39 MB
16
+
17
+ ========================================================================
18
+ QUERY: tagged with ALL of 2 tags -> load records (~25 rows)
19
+ ========================================================================
20
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
21
+ Warming up --------------------------------------
22
+ metka 47.000 i/100ms
23
+ metka (index) 715.000 i/100ms
24
+ acts-as-taggable-on 326.000 i/100ms
25
+ gutentag 197.000 i/100ms
26
+ Calculating -------------------------------------
27
+ metka 398.137 (± 2.3%) i/s (2.51 ms/i) - 2.021k in 5.076143s
28
+ metka (index) 7.199k (± 2.9%) i/s (138.90 μs/i) - 36.465k in 5.064972s
29
+ acts-as-taggable-on 3.189k (± 3.3%) i/s (313.59 μs/i) - 15.974k in 5.009333s
30
+ gutentag 1.965k (± 4.4%) i/s (508.87 μs/i) - 9.850k in 5.012329s
31
+
32
+ Comparison:
33
+ metka (index): 7199.4 i/s
34
+ acts-as-taggable-on: 3188.8 i/s - 2.26x slower
35
+ gutentag: 1965.2 i/s - 3.66x slower
36
+ metka: 398.1 i/s - 18.08x slower
37
+
38
+ ========================================================================
39
+ QUERY: tagged with ANY of 2 tags -> count
40
+ ========================================================================
41
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
42
+ Warming up --------------------------------------
43
+ metka 38.000 i/100ms
44
+ metka (index) 397.000 i/100ms
45
+ acts-as-taggable-on 41.000 i/100ms
46
+ gutentag 199.000 i/100ms
47
+ Calculating -------------------------------------
48
+ metka 379.318 (± 2.1%) i/s (2.64 ms/i) - 1.900k in 5.008996s
49
+ metka (index) 4.113k (± 3.7%) i/s (243.13 μs/i) - 20.644k in 5.019114s
50
+ acts-as-taggable-on 410.036 (± 2.4%) i/s (2.44 ms/i) - 2.091k in 5.099553s
51
+ gutentag 1.900k (± 6.4%) i/s (526.34 μs/i) - 9.552k in 5.027612s
52
+
53
+ Comparison:
54
+ metka (index): 4113.1 i/s
55
+ gutentag: 1899.9 i/s - 2.16x slower
56
+ acts-as-taggable-on: 410.0 i/s - 10.03x slower
57
+ metka: 379.3 i/s - 10.84x slower
58
+
59
+ ========================================================================
60
+ TAG CLOUD: tag -> usage count across all 10000 posts
61
+ ========================================================================
62
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
63
+ Warming up --------------------------------------
64
+ metka 11.000 i/100ms
65
+ metka (table) 1.275k i/100ms
66
+ acts-as-taggable-on 11.000 i/100ms
67
+ gutentag 8.000 i/100ms
68
+ Calculating -------------------------------------
69
+ metka 110.804 (± 2.7%) i/s (9.02 ms/i) - 561.000 in 5.062986s
70
+ metka (table) 12.696k (± 2.2%) i/s (78.76 μs/i) - 63.750k in 5.021086s
71
+ acts-as-taggable-on 110.668 (± 3.6%) i/s (9.04 ms/i) - 561.000 in 5.069223s
72
+ gutentag 87.688 (± 2.3%) i/s (11.40 ms/i) - 440.000 in 5.017818s
73
+
74
+ Comparison:
75
+ metka (table): 12696.5 i/s
76
+ metka: 110.8 i/s - 114.58x slower
77
+ acts-as-taggable-on: 110.7 i/s - 114.73x slower
78
+ gutentag: 87.7 i/s - 144.79x slower
79
+
80
+ ========================================================================
81
+ WRITE: create one post with 5 tags
82
+ ========================================================================
83
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
84
+ Warming up --------------------------------------
85
+ metka 776.000 i/100ms
86
+ metka (table) 683.000 i/100ms
87
+ metka (index) 522.000 i/100ms
88
+ acts-as-taggable-on 27.000 i/100ms
89
+ gutentag 28.000 i/100ms
90
+ Calculating -------------------------------------
91
+ metka 7.783k (± 3.7%) i/s (128.48 μs/i) - 39.576k in 5.084760s
92
+ metka (table) 6.895k (± 4.4%) i/s (145.04 μs/i) - 34.833k in 5.052142s
93
+ metka (index) 5.349k (± 5.0%) i/s (186.96 μs/i) - 27.144k in 5.074779s
94
+ acts-as-taggable-on 268.380 (± 5.2%) i/s (3.73 ms/i) - 1.350k in 5.030173s
95
+ gutentag 284.410 (± 6.3%) i/s (3.52 ms/i) - 1.428k in 5.020920s
96
+
97
+ Comparison:
98
+ metka: 7783.3 i/s
99
+ metka (table): 6894.7 i/s - 1.13x slower
100
+ metka (index): 5348.8 i/s - 1.46x slower
101
+ gutentag: 284.4 i/s - 27.37x slower
102
+ acts-as-taggable-on: 268.4 i/s - 29.00x slower
103
+
104
+ ========================================================================
105
+ WRITE: replace the tag list of an existing post
106
+ ========================================================================
107
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
108
+ Warming up --------------------------------------
109
+ metka 1.199k i/100ms
110
+ metka (table) 1.200k i/100ms
111
+ metka (index) 1.196k i/100ms
112
+ acts-as-taggable-on 19.000 i/100ms
113
+ gutentag 19.000 i/100ms
114
+ Calculating -------------------------------------
115
+ metka 12.670k (± 4.2%) i/s (78.93 μs/i) - 63.547k in 5.015591s
116
+ metka (table) 12.604k (± 4.1%) i/s (79.34 μs/i) - 63.600k in 5.045863s
117
+ metka (index) 12.696k (± 2.7%) i/s (78.77 μs/i) - 64.584k in 5.087111s
118
+ acts-as-taggable-on 416.435 (±90.3%) i/s (2.40 ms/i) - 2.090k in 5.018792s
119
+ gutentag 808.708 (± Inf%) i/s (1.24 ms/i) - 4.047k in 5.004277s
120
+
121
+ Comparison:
122
+ metka (index): 12695.6 i/s
123
+ metka: 12669.9 i/s - same-ish: difference falls within error
124
+ metka (table): 12604.4 i/s - same-ish: difference falls within error
125
+ gutentag: 808.7 i/s - 15.70x slower
126
+ acts-as-taggable-on: 416.4 i/s - 30.49x slower
127
+
128
+
129
+ Strategy integrity after all suites (0 = aggregate matches a live aggregation):
130
+ table mismatching tags: 0
131
+ index mismatching pairs: 0
@@ -0,0 +1,150 @@
1
+ Seeding 10000 posts per gem, 5 tags each, vocabulary of 100 tags
2
+
3
+ Bulk seed wall time (10000 posts):
4
+ metka 0.21 s
5
+ metka (table) 0.17 s
6
+ acts-as-taggable-array-on 0.17 s
7
+ tag_columns 0.16 s
8
+ acts-as-taggable-on 45.57 s
9
+ gutentag 50.24 s
10
+
11
+ Storage (tables + indexes) for 10000 posts:
12
+ metka 2.68 MB
13
+ metka (table) 2.75 MB
14
+ acts-as-taggable-array-on 2.68 MB
15
+ tag_columns 2.68 MB
16
+ acts-as-taggable-on 18.17 MB
17
+ gutentag 10.97 MB
18
+
19
+ ========================================================================
20
+ QUERY: tagged with ALL of 2 tags -> load records (~25 rows)
21
+ ========================================================================
22
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
23
+ Warming up --------------------------------------
24
+ metka 597.000 i/100ms
25
+ acts-as-taggable-array-on 757.000 i/100ms
26
+ tag_columns 101.000 i/100ms
27
+ acts-as-taggable-on 213.000 i/100ms
28
+ gutentag 124.000 i/100ms
29
+ Calculating -------------------------------------
30
+ metka 6.003k (± 8.2%) i/s (166.59 μs/i) - 30.447k in 5.072240s
31
+ acts-as-taggable-array-on 6.725k (± 8.6%) i/s (148.69 μs/i) - 34.065k in 5.065229s
32
+ tag_columns 985.914 (± 3.1%) i/s (1.01 ms/i) - 4.949k in 5.019708s
33
+ acts-as-taggable-on 2.292k (± 6.7%) i/s (436.38 μs/i) - 11.502k in 5.019218s
34
+ gutentag 1.299k (± 8.8%) i/s (769.73 μs/i) - 6.572k in 5.058649s
35
+
36
+ Comparison:
37
+ acts-as-taggable-array-on: 6725.3 i/s
38
+ metka: 6002.7 i/s - same-ish: difference falls within error
39
+ acts-as-taggable-on: 2291.6 i/s - 2.93x slower
40
+ gutentag: 1299.2 i/s - 5.18x slower
41
+ tag_columns: 985.9 i/s - 6.82x slower
42
+
43
+ ========================================================================
44
+ QUERY: tagged with ANY of 2 tags -> count
45
+ ========================================================================
46
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
47
+ Warming up --------------------------------------
48
+ metka 421.000 i/100ms
49
+ acts-as-taggable-array-on 466.000 i/100ms
50
+ tag_columns 66.000 i/100ms
51
+ acts-as-taggable-on 74.000 i/100ms
52
+ gutentag 106.000 i/100ms
53
+ Calculating -------------------------------------
54
+ metka 4.575k (±10.3%) i/s (218.60 μs/i) - 23.155k in 5.061751s
55
+ acts-as-taggable-array-on 4.479k (±12.4%) i/s (223.24 μs/i) - 22.834k in 5.097452s
56
+ tag_columns 678.434 (± 2.7%) i/s (1.47 ms/i) - 3.432k in 5.058707s
57
+ acts-as-taggable-on 788.456 (± 2.7%) i/s (1.27 ms/i) - 3.996k in 5.068134s
58
+ gutentag 1.027k (± 7.5%) i/s (973.91 μs/i) - 5.194k in 5.058492s
59
+
60
+ Comparison:
61
+ metka: 4574.5 i/s
62
+ acts-as-taggable-array-on: 4479.5 i/s - same-ish: difference falls within error
63
+ gutentag: 1026.8 i/s - 4.46x slower
64
+ acts-as-taggable-on: 788.5 i/s - 5.80x slower
65
+ tag_columns: 678.4 i/s - 6.74x slower
66
+
67
+ ========================================================================
68
+ TAG CLOUD: tag -> usage count across all 10000 posts
69
+ ========================================================================
70
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
71
+ Warming up --------------------------------------
72
+ metka 21.000 i/100ms
73
+ metka (table) 986.000 i/100ms
74
+ acts-as-taggable-array-on 20.000 i/100ms
75
+ tag_columns 19.000 i/100ms
76
+ acts-as-taggable-on 12.000 i/100ms
77
+ gutentag 16.000 i/100ms
78
+ Calculating -------------------------------------
79
+ metka 212.110 (± 2.8%) i/s (4.71 ms/i) - 1.071k in 5.049256s
80
+ metka (table) 9.025k (± 4.6%) i/s (110.81 μs/i) - 45.356k in 5.025682s
81
+ acts-as-taggable-array-on 203.695 (± 2.0%) i/s (4.91 ms/i) - 1.020k in 5.007496s
82
+ tag_columns 197.664 (± 2.0%) i/s (5.06 ms/i) - 1.007k in 5.094517s
83
+ acts-as-taggable-on 127.192 (± 3.1%) i/s (7.86 ms/i) - 648.000 in 5.094650s
84
+ gutentag 161.495 (± 3.1%) i/s (6.19 ms/i) - 816.000 in 5.052782s
85
+
86
+ Comparison:
87
+ metka (table): 9024.8 i/s
88
+ metka: 212.1 i/s - 42.55x slower
89
+ acts-as-taggable-array-on: 203.7 i/s - 44.31x slower
90
+ tag_columns: 197.7 i/s - 45.66x slower
91
+ gutentag: 161.5 i/s - 55.88x slower
92
+ acts-as-taggable-on: 127.2 i/s - 70.95x slower
93
+
94
+ ========================================================================
95
+ WRITE: create one post with 5 tags
96
+ ========================================================================
97
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
98
+ Warming up --------------------------------------
99
+ metka 176.000 i/100ms
100
+ metka (table) 156.000 i/100ms
101
+ acts-as-taggable-array-on 154.000 i/100ms
102
+ tag_columns 157.000 i/100ms
103
+ acts-as-taggable-on 19.000 i/100ms
104
+ gutentag 18.000 i/100ms
105
+ Calculating -------------------------------------
106
+ metka 1.619k (± 6.9%) i/s (617.73 μs/i) - 8.096k in 5.001165s
107
+ metka (table) 1.495k (± 5.8%) i/s (668.77 μs/i) - 7.488k in 5.007742s
108
+ acts-as-taggable-array-on 1.633k (± 5.8%) i/s (612.19 μs/i) - 8.316k in 5.090931s
109
+ tag_columns 1.599k (± 7.7%) i/s (625.58 μs/i) - 8.007k in 5.009026s
110
+ acts-as-taggable-on 203.548 (± 9.3%) i/s (4.91 ms/i) - 1.026k in 5.040586s
111
+ gutentag 196.158 (±11.7%) i/s (5.10 ms/i) - 990.000 in 5.046963s
112
+
113
+ Comparison:
114
+ acts-as-taggable-array-on: 1633.5 i/s
115
+ metka: 1618.8 i/s - same-ish: difference falls within error
116
+ tag_columns: 1598.5 i/s - same-ish: difference falls within error
117
+ metka (table): 1495.3 i/s - same-ish: difference falls within error
118
+ acts-as-taggable-on: 203.5 i/s - 8.03x slower
119
+ gutentag: 196.2 i/s - 8.33x slower
120
+
121
+ ========================================================================
122
+ WRITE: replace the tag list of an existing post
123
+ ========================================================================
124
+ ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +PRISM [x86_64-linux]
125
+ Warming up --------------------------------------
126
+ metka 140.000 i/100ms
127
+ metka (table) 121.000 i/100ms
128
+ acts-as-taggable-array-on 145.000 i/100ms
129
+ tag_columns 147.000 i/100ms
130
+ acts-as-taggable-on 13.000 i/100ms
131
+ gutentag 11.000 i/100ms
132
+ Calculating -------------------------------------
133
+ metka 8.293k (± 9.2%) i/s (120.58 μs/i) - 41.580k in 5.013907s
134
+ metka (table) 8.131k (± 9.6%) i/s (122.98 μs/i) - 40.656k in 4.999930s
135
+ acts-as-taggable-array-on 7.766k (± 9.6%) i/s (128.77 μs/i) - 38.860k in 5.003909s
136
+ tag_columns 7.396k (± 8.2%) i/s (135.22 μs/i) - 37.044k in 5.008958s
137
+ acts-as-taggable-on 189.648 (± Inf%) i/s (5.27 ms/i) - 962.000 in 5.072563s
138
+ gutentag 182.869 (± Inf%) i/s (5.47 ms/i) - 924.000 in 5.052787s
139
+
140
+ Comparison:
141
+ metka: 8292.9 i/s
142
+ metka (table): 8131.3 i/s - same-ish: difference falls within error
143
+ acts-as-taggable-array-on: 7765.9 i/s - same-ish: difference falls within error
144
+ tag_columns: 7395.6 i/s - same-ish: difference falls within error
145
+ acts-as-taggable-on: 189.6 i/s - 43.73x slower
146
+ gutentag: 182.9 i/s - 45.35x slower
147
+
148
+
149
+ Strategy integrity after all suites (0 = aggregate matches a live UNNEST..GROUP BY):
150
+ table mismatching tags: 0
data/bin/setup CHANGED
@@ -6,3 +6,10 @@ gem install bundler --conservative
6
6
  bundle check || bundle install
7
7
 
8
8
  RAILS_ENV=test bundle exec rake dummy:db:migrate:reset
9
+
10
+ # SQLite resets by deleting the file, not through db:migrate:reset: the rake
11
+ # composite drops the file while the booted app still holds an open handle, so
12
+ # the migrations land on the unlinked inode and vanish when the process exits.
13
+ # A fresh process migrating into a fresh file has no such problem.
14
+ rm -f test/dummy/db/metka_test.sqlite3
15
+ DB=sqlite RAILS_ENV=test bundle exec rake dummy:db:migrate
@@ -0,0 +1,316 @@
1
+ # Cloud Table Naming Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Rename the table-strategy generator's derived summary-table name from `tagged_<source_table>` to `<source_table>_<columns>_cloud` (e.g. `songs_tags_cloud`), matching the index strategy's `<source_table>_<column>_index` pattern.
6
+
7
+ **Architecture:** One code change — the `table_name` derivation in the table generator. The ERB migration templates interpolate `<%= table_name %>` everywhere, so they need no changes. Everything else is fixtures and docs modeling the new convention: generator tests, the dummy app's hand-written equivalent migration/model/test, the README, and the benchmark.
8
+
9
+ **Tech Stack:** Ruby gem (metka), Rails generators, Minitest, dummy Rails app under `test/dummy` running on PostgreSQL or SQLite (`DB=sqlite` env var switches the adapter).
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-08-18-cloud-table-naming-design.md`
12
+
13
+ **Reference — the naming rule:** derived name is `#{source_table_name}_#{source_columns.join("_and_")}_cloud`, uniformly. No special case for the default `tags` column. `--table-name` still overrides. Migration name stays `create_#{table_name}_table`.
14
+
15
+ **Reference — what does NOT change:** trigger names (`metka_ins_on_<source_table>_<columns>` — derived from the source table), the `tag_name`/`taggings_count` columns, both ERB templates, all runtime code in `lib/metka/`, and the historical benchmark results files (`benchmark/results*.txt` are recorded outputs of past runs — leave them).
16
+
17
+ **Test environment setup (once, before Task 1):**
18
+
19
+ ```bash
20
+ cd /home/igor/Work/metka/.claude/worktrees/tagging-table-naming-5cd0c9
21
+ bundle install
22
+ rm -f test/dummy/db/*.sqlite3 test/dummy/db/schema.rb
23
+ RAILS_ENV=test DB=sqlite bundle exec rake dummy:db:migrate
24
+ ```
25
+
26
+ Always prefix rake/test commands with `RAILS_ENV=test` — the dummy app only configures the `test` environment. Do NOT use `dummy:db:migrate:reset` under SQLite: locally it leaves an empty database and dumps a stale empty `schema.rb` that then makes `maintain_test_schema` wipe the DB on test boot (CI's SQLite job uses plain `dummy:db:migrate` for the same reason). To reset, delete the `.sqlite3` file and any `test/dummy/db/schema.rb`, then `dummy:db:migrate`.
27
+
28
+ SQLite always works locally. PostgreSQL runs the other template branch; if a local server is available (`psql -h localhost -U postgres -c 'select 1'` succeeds), also run the PG variants of each verification step (`bundle exec rake dummy:db:create dummy:db:migrate:reset`, no `DB` env var). If PG is unavailable, say so in the final report — CI runs both.
29
+
30
+ ---
31
+
32
+ ### Task 1: Generator derivation (TDD)
33
+
34
+ **Files:**
35
+ - Modify: `test/generators/strategies/table_generator_test.rb`
36
+ - Modify: `lib/generators/metka/strategies/table/table_generator.rb:62-67`
37
+
38
+ - [ ] **Step 1: Update the generator test's expected names**
39
+
40
+ In `test/generators/strategies/table_generator_test.rb`:
41
+
42
+ 1. Change the migration constant (line 8):
43
+
44
+ ```ruby
45
+ MIGRATION = "db/migrate/create_notes_tags_cloud_table.rb"
46
+ ```
47
+
48
+ 2. Replace every `tagged_notes` in the assertion regexes with `notes_tags_cloud`. That is exactly these lines: 24 (`CREATE TABLE`), 28 (`INSERT INTO ... (tag_name, taggings_count)`), 44 (`DROP TABLE IF EXISTS`), 73 (`INSERT INTO` inside the lock-ordering test), 80–82 (`CREATE OR REPLACE FUNCTION metka_ins/upd/del_notes_tags_cloud`), 93–95 (`DROP FUNCTION IF EXISTS metka_ins/upd/del_notes_tags_cloud`). Do NOT touch the trigger-name assertions (lines 32–34, 38–40, `metka_ins_on_notes` etc.) — trigger names derive from the source table and stay the same.
49
+
50
+ 3. Add two new tests at the end of the class (before the closing `end`, after the `if SQLITE`/`else` block), covering the multi-column derivation and the override — both previously untested:
51
+
52
+ ```ruby
53
+ test "derives the name from the source columns when given" do
54
+ prepare_destination
55
+ run_generator [ "--source-table-name=notes", "--source-columns=tags", "genres" ]
56
+
57
+ assert_migration "db/migrate/create_notes_tags_and_genres_cloud_table.rb",
58
+ /CREATE TABLE notes_tags_and_genres_cloud/i
59
+ end
60
+
61
+ test "respects an explicit --table-name" do
62
+ prepare_destination
63
+ run_generator [ "--source-table-name=notes", "--table-name=note_cloud" ]
64
+
65
+ assert_migration "db/migrate/create_note_cloud_table.rb", /CREATE TABLE note_cloud/i
66
+ end
67
+ ```
68
+
69
+ - [ ] **Step 2: Run the generator tests to verify they fail**
70
+
71
+ ```bash
72
+ DB=sqlite bundle exec ruby -Itest test/generators/strategies/table_generator_test.rb
73
+ ```
74
+
75
+ Expected: FAIL — every test errors or fails because the generator still emits `db/migrate/*_create_tagged_notes_table.rb`, so `assert_migration "db/migrate/create_notes_tags_cloud_table.rb"` finds no migration.
76
+
77
+ - [ ] **Step 3: Change the derivation**
78
+
79
+ In `lib/generators/metka/strategies/table/table_generator.rb`, replace the `table_name` method (lines 62–67):
80
+
81
+ ```ruby
82
+ def table_name
83
+ return options[:table_name] if options[:table_name]
84
+
85
+ "#{source_table_name}_#{source_columns_names}_cloud"
86
+ end
87
+ ```
88
+
89
+ (The `columns_sequence` local and its default-columns special case disappear entirely; `source_columns_names` already joins with `_and_`.)
90
+
91
+ Also update the generator's `desc` LONGDESC if it mentions the derived name — it currently doesn't (it only shows the flags), so no change expected there; just confirm.
92
+
93
+ - [ ] **Step 4: Run the generator tests to verify they pass**
94
+
95
+ ```bash
96
+ DB=sqlite bundle exec ruby -Itest test/generators/strategies/table_generator_test.rb
97
+ ```
98
+
99
+ Expected: PASS (SQLite runs 11 tests after the two added in Step 1). If PostgreSQL is available, also run without `DB=sqlite` — it exercises the PG branch (function-name assertions): PASS, 13 tests.
100
+
101
+ - [ ] **Step 5: Commit**
102
+
103
+ ```bash
104
+ git add lib/generators/metka/strategies/table/table_generator.rb test/generators/strategies/table_generator_test.rb
105
+ git commit -m "feat(generator): Derive cloud table name as <source>_<columns>_cloud
106
+
107
+ The table strategy summary table default changes from
108
+ tagged_<source_table> to <source_table>_<columns>_cloud
109
+ (songs_tags_cloud), mirroring the index strategy's
110
+ <source_table>_<column>_index naming. Generation-time only:
111
+ existing installs keep their tagged_* tables; --table-name
112
+ still overrides.
113
+
114
+ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
115
+ ```
116
+
117
+ ---
118
+
119
+ ### Task 2: Dummy app models the new convention
120
+
121
+ **Files:**
122
+ - Rename: `test/dummy/db/migrate/11_create_tagged_table_posts_table.rb` → `test/dummy/db/migrate/11_create_table_posts_tags_cloud_table.rb`
123
+ - Rename: `test/dummy/app/models/tagged_table_post.rb` → `test/dummy/app/models/table_posts_tags_cloud.rb`
124
+ - Rename: `test/metka/tagged_table_post_test.rb` → `test/metka/table_posts_tags_cloud_test.rb`
125
+
126
+ The dummy source table is `table_posts` (migration 10), so the new summary table name is `table_posts_tags_cloud`.
127
+
128
+ - [ ] **Step 1: Rename and edit the dummy migration**
129
+
130
+ ```bash
131
+ git mv test/dummy/db/migrate/11_create_tagged_table_posts_table.rb test/dummy/db/migrate/11_create_table_posts_tags_cloud_table.rb
132
+ ```
133
+
134
+ Then in the renamed file:
135
+ - Class name: `CreateTaggedTablePostsTable` → `CreateTablePostsTagsCloudTable` (must match the new filename or Rails raises on migrate).
136
+ - Replace all `tagged_table_posts` → `table_posts_tags_cloud` (both the SQLite and PostgreSQL branches: CREATE TABLE, INSERT INTO, UPDATE, DELETE FROM, DROP TABLE, and the PG function names `metka_ins/upd/del_tagged_table_posts` → `metka_ins/upd/del_table_posts_tags_cloud`, including the `DROP FUNCTION IF EXISTS` lines).
137
+ - Leave trigger names (`metka_*_on_table_posts*`) untouched.
138
+
139
+ Sanity check — this file must end up matching what the updated generator emits. After editing, verify with:
140
+
141
+ ```bash
142
+ grep -c "tagged" test/dummy/db/migrate/11_create_table_posts_tags_cloud_table.rb
143
+ ```
144
+
145
+ Expected: `0`.
146
+
147
+ - [ ] **Step 2: Rename and edit the dummy model**
148
+
149
+ ```bash
150
+ git mv test/dummy/app/models/tagged_table_post.rb test/dummy/app/models/table_posts_tags_cloud.rb
151
+ ```
152
+
153
+ New content (the explicit `table_name` is required — Rails infers the plural `table_posts_tags_clouds` from the class name):
154
+
155
+ ```ruby
156
+ # frozen_string_literal: true
157
+
158
+ class TablePostsTagsCloud < ActiveRecord::Base
159
+ self.table_name = "table_posts_tags_cloud"
160
+ end
161
+ ```
162
+
163
+ - [ ] **Step 3: Rename and edit the test**
164
+
165
+ ```bash
166
+ git mv test/metka/tagged_table_post_test.rb test/metka/table_posts_tags_cloud_test.rb
167
+ ```
168
+
169
+ In the renamed file: class `TaggedTablePostTest` → `TablePostsTagsCloudTest`, and every `TaggedTablePost` constant reference → `TablePostsTagsCloud` (10 lines contain it, including the class name; `grep -n TaggedTablePost` afterwards must return nothing). Everything else (fixtures, `TablePost` writes, trigger assertions) stays.
170
+
171
+ - [ ] **Step 4: Recreate the dummy DB and run the suite**
172
+
173
+ ```bash
174
+ rm -f test/dummy/db/*.sqlite3 test/dummy/db/schema.rb
175
+ RAILS_ENV=test DB=sqlite bundle exec rake dummy:db:migrate
176
+ RAILS_ENV=test DB=sqlite bundle exec ruby -Itest test/metka/table_posts_tags_cloud_test.rb
177
+ ```
178
+
179
+ Expected: migration succeeds (the renamed migration runs under its new class name); test PASS. If PostgreSQL is available, repeat with `bundle exec rake dummy:db:migrate:reset` / the test command without `DB=sqlite`.
180
+
181
+ - [ ] **Step 5: Commit**
182
+
183
+ ```bash
184
+ git add -A test/dummy test/metka/table_posts_tags_cloud_test.rb
185
+ git commit -m "test(dummy): Rename summary table fixtures to table_posts_tags_cloud
186
+
187
+ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
188
+ ```
189
+
190
+ ---
191
+
192
+ ### Task 3: README
193
+
194
+ **Files:**
195
+ - Modify: `README.md` (table-strategy section, ~lines 336–410)
196
+
197
+ - [ ] **Step 1: Update the derived-name examples and model example**
198
+
199
+ Exact edits (line numbers as of commit 281f0ea):
200
+
201
+ 1. Line 345 — the migration-template link: `test/dummy/db/migrate/11_create_tagged_table_posts_table.rb` → `test/dummy/db/migrate/11_create_table_posts_tags_cloud_table.rb` (both the link target and any visible filename).
202
+ 2. Line 347: `the resulting `tagged_notes` table` → `the resulting `notes_tags_cloud` table`.
203
+ 3. Line 357 — replace the model sentence with one that carries the pluralization gotcha:
204
+
205
+ ```markdown
206
+ And you can also create a model to work with the table as with a Rails model — set the table name explicitly, since Rails would infer the plural `notes_tags_clouds` from the class name:
207
+
208
+ ```ruby
209
+ class NotesTagsCloud < ApplicationRecord
210
+ self.table_name = "notes_tags_cloud"
211
+ end
212
+ ```
213
+ ```
214
+
215
+ 4. Lines 378–385 — the `TaggedSong` model example in "Migrating from on-the-fly tag clouds". This is not a plain rename: like item 3, the new class name needs an explicit table name. Replace the code block and the sentence after it:
216
+
217
+ ```markdown
218
+ ```ruby
219
+ class SongsTagsCloud < ActiveRecord::Base
220
+ self.table_name = "songs_tags_cloud"
221
+ end
222
+
223
+ Song.tag_cloud # before
224
+ SongsTagsCloud.pluck(:tag_name, :taggings_count) # after
225
+ ```
226
+
227
+ Sorting and limiting that used to happen in Ruby becomes a normal query: `SongsTagsCloud.order(taggings_count: :desc).limit(50)`.
228
+ ```
229
+
230
+ 5. Lines 392–406 (the reseeding SQL in the same section): every `tagged_songs` → `songs_tags_cloud` (4 occurrences: two `DELETE FROM`, two `INSERT INTO`). Read the surrounding prose of that section and update any other derived-name mentions to match.
231
+
232
+ - [ ] **Step 2: Verify no stale names remain**
233
+
234
+ ```bash
235
+ grep -n "tagged_notes\|tagged_songs\|tagged_table_posts\|TaggedNote\|TaggedTablePost\|TaggedSong" README.md
236
+ ```
237
+
238
+ Expected: no output. (`tagged_with` and prose like "tagged columns" are unrelated and must remain.)
239
+
240
+ - [ ] **Step 3: Commit**
241
+
242
+ ```bash
243
+ git add README.md
244
+ git commit -m "docs: Document <source>_<columns>_cloud naming in README
245
+
246
+ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
247
+ ```
248
+
249
+ ---
250
+
251
+ ### Task 4: Benchmark
252
+
253
+ **Files:**
254
+ - Modify: `benchmark/benchmark.rb`
255
+
256
+ - [ ] **Step 1: Rename the benchmark's table-strategy table**
257
+
258
+ In `benchmark/benchmark.rb`, replace:
259
+ - every `tagged_metka_table_posts` → `metka_table_posts_tags_cloud` (SQL strings at lines ~172–341, the model's `self.table_name` at 378, `relation_size` at 480, `cloud_mismatches` at 705);
260
+ - the PG function names `metka_ins/upd/del_tagged_metka_table_posts` → `metka_ins/upd/del_metka_table_posts_tags_cloud`;
261
+ - the model class `TaggedMetkaTablePost` (line 377) → `MetkaTablePostsTagsCloud`, and its one usage (line ~558, the tag-cloud read benchmark).
262
+
263
+ Leave `benchmark/results.txt` / `benchmark/results.sqlite.txt` untouched — they are recorded outputs of past runs. `benchmark/README.md` never names the table literally; no change.
264
+
265
+ - [ ] **Step 2: Verify**
266
+
267
+ ```bash
268
+ ruby -c benchmark/benchmark.rb
269
+ grep -n "tagged_metka\|TaggedMetka" benchmark/benchmark.rb
270
+ ```
271
+
272
+ Expected: `Syntax OK`, then no grep output. (Running the benchmark itself needs a seeded DB and minutes of runtime — out of scope; the SQL mirrors the dummy migration verified in Task 2.)
273
+
274
+ - [ ] **Step 3: Commit**
275
+
276
+ ```bash
277
+ git add benchmark/benchmark.rb
278
+ git commit -m "bench: Rename table strategy table to metka_table_posts_tags_cloud
279
+
280
+ Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>"
281
+ ```
282
+
283
+ ---
284
+
285
+ ### Task 5: Full-suite verification and sweep
286
+
287
+ - [ ] **Step 1: Run the whole suite on SQLite**
288
+
289
+ ```bash
290
+ rm -f test/dummy/db/*.sqlite3 test/dummy/db/schema.rb
291
+ RAILS_ENV=test DB=sqlite bundle exec rake dummy:db:migrate
292
+ RAILS_ENV=test DB=sqlite bundle exec rake test
293
+ ```
294
+
295
+ Expected: 0 failures, 0 errors.
296
+
297
+ - [ ] **Step 2: Run the whole suite on PostgreSQL (if available)**
298
+
299
+ ```bash
300
+ bundle exec rake dummy:db:create dummy:db:migrate:reset
301
+ bundle exec rake test
302
+ ```
303
+
304
+ Expected: 0 failures, 0 errors. If no local PG server, note it in the report — CI covers it.
305
+
306
+ - [ ] **Step 3: Final repo sweep for stale names**
307
+
308
+ ```bash
309
+ grep -rn "tagged_notes\|tagged_songs\|tagged_table_posts\|tagged_metka\|TaggedNote\|TaggedTablePost\|TaggedMetka\|TaggedSong" --include="*.rb" --include="*.erb" --include="*.md" . | grep -v benchmark/results | grep -v docs/superpowers
310
+ ```
311
+
312
+ Expected: no output (the spec/plan docs and historical results files legitimately mention old names).
313
+
314
+ - [ ] **Step 4: Commit any stragglers found by the sweep**
315
+
316
+ Only if Step 3 surfaced something; fix and commit with an appropriate message.