make_taggable 1.4.0 → 1.5.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 +4 -4
- data/CHANGELOG.md +32 -0
- data/docs/configuration.md +10 -0
- data/docs/database.md +19 -2
- data/docs/querying.md +15 -1
- data/lib/make_taggable/tag_list.rb +5 -1
- data/lib/make_taggable/taggable/core.rb +30 -5
- data/lib/make_taggable/taggable/ownership.rb +4 -1
- data/lib/make_taggable/taggable/tagged_with_query/all_tags_query.rb +2 -2
- data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +2 -2
- data/lib/make_taggable/taggable/tagged_with_query/query_base.rb +29 -5
- data/lib/make_taggable/version.rb +1 -1
- data/lib/make_taggable.rb +33 -9
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d900a8ac37e0f88ead6c0516fd30ddee5e8cc11c893b7d64e85c090a8b59598
|
|
4
|
+
data.tar.gz: b364eef565e3a171c56558f928360de5230c445b4b210e1aab8f536f9b998c70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 611fd4635e580ff5aaaee314600d2d66e7cdb70a2bd22d8988e3eb541c3d632fc25625e485880821beeec8f0bfb71677242f3d0d14ba96e98e0dd4d15038e0ae
|
|
7
|
+
data.tar.gz: b1713ea6a65eb7a8e45dfb0d4b8d41c9d72334cfb8de93ef2c1e60a21a6a4e04758acdcb7761a117b8b275f0aecb3e98d78258a8c7f8389f433836353e676958
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,38 @@ 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.5.0] - 2026-08-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `:on` accepts several contexts as well as one, so a query can search a subset:
|
|
13
|
+
`tagged_with("classic", on: [:genres, :moods], any: true)`. Passing an array previously raised
|
|
14
|
+
`TypeError` from inside Arel.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- `force_parameterize` discarded a tag outright when the name had no ASCII in it. `parameterize`
|
|
19
|
+
reduces such a name to an empty string, which was then rejected as blank, so a user tagging in
|
|
20
|
+
Japanese, Greek, Hebrew, Arabic or Cyrillic saved successfully and got back fewer tags than they
|
|
21
|
+
typed. The parameterized form is now kept only where there is one.
|
|
22
|
+
|
|
23
|
+
- `force_binary_collation = true` ran an `ALTER TABLE` every time it was assigned, with no check for
|
|
24
|
+
whether the column already carried that collation. The documented home for it is an initializer,
|
|
25
|
+
which runs once per process, so a deployment issued a schema change against the `tags` table from
|
|
26
|
+
every web worker, background worker, console and rake task -- each one taking a metadata lock that
|
|
27
|
+
other queries queue behind. The current collation is now read first and the statement skipped when
|
|
28
|
+
it matches. The blanket `rescue`/`puts` around it is replaced by an explicit `table_exists?` check.
|
|
29
|
+
|
|
30
|
+
- On PostgreSQL, tag names were matched as `LOWER(name) ILIKE ...`. `ILIKE` already folds case, so
|
|
31
|
+
the `LOWER()` changed nothing while making the expression non-sargable -- no index on `tags.name`
|
|
32
|
+
could be used, including a trigram index built for wild searches. It is skipped on PostgreSQL and
|
|
33
|
+
kept on the adapters that need it.
|
|
34
|
+
|
|
35
|
+
- Taggings are created in tag id order. Each insert bumps the tag's counter cache, so two concurrent
|
|
36
|
+
saves touching the same tags took row locks in whatever order their lists happened to be in, which
|
|
37
|
+
invites deadlocks. Models using `make_ordered_taggable` are unaffected -- there the creation order
|
|
38
|
+
carries the meaning.
|
|
39
|
+
|
|
8
40
|
## [1.4.0] - 2026-08-23
|
|
9
41
|
|
|
10
42
|
### Fixed
|
data/docs/configuration.md
CHANGED
|
@@ -40,6 +40,10 @@ Tags are downcased as they are cleaned, so `"Ruby"` is stored as `"ruby"`.
|
|
|
40
40
|
Tags are parameterized, so `"Ruby on Rails"` is stored as `"ruby-on-rails"`. Applied after
|
|
41
41
|
`force_lowercase` if both are on.
|
|
42
42
|
|
|
43
|
+
`String#parameterize` strips anything outside a conservative ASCII set, so a name with no ASCII in
|
|
44
|
+
it -- `"日本語"` -- has no slug to reduce to. Such a name is kept as it is rather than parameterized
|
|
45
|
+
into nothing and dropped.
|
|
46
|
+
|
|
43
47
|
### `strict_case_match`
|
|
44
48
|
|
|
45
49
|
Off, tag lookups are case insensitive and `"Ruby"` finds `"ruby"`; a list containing both keeps one.
|
|
@@ -98,6 +102,12 @@ Note that the shipped migrations already apply `utf8mb4_bin` to the column on My
|
|
|
98
102
|
setting adds is `strict_case_match`, which is what actually makes the library's lookups case
|
|
99
103
|
sensitive — see [database.md](database.md).
|
|
100
104
|
|
|
105
|
+
Because the migrations have usually applied the collation already, assigning this in an initializer
|
|
106
|
+
is normally a no-op: the current collation is read first and the `ALTER TABLE` is skipped when it
|
|
107
|
+
already matches. That matters because an initializer runs once per process — every web worker,
|
|
108
|
+
background worker, console and rake task — and each `ALTER TABLE` takes a metadata lock on the tags
|
|
109
|
+
table.
|
|
110
|
+
|
|
101
111
|
```ruby
|
|
102
112
|
MakeTaggable.force_binary_collation = true
|
|
103
113
|
```
|
data/docs/database.md
CHANGED
|
@@ -107,8 +107,25 @@ end
|
|
|
107
107
|
## PostgreSQL
|
|
108
108
|
|
|
109
109
|
- `named_like` uses `ILIKE`, so partial matching is case insensitive regardless of collation.
|
|
110
|
-
-
|
|
111
|
-
|
|
110
|
+
- Queries group by the primary key. PostgreSQL wanted every selected non-aggregated column listed
|
|
111
|
+
before 9.1; since then it works the dependency out itself.
|
|
112
|
+
|
|
113
|
+
**Case-insensitive matching of non-ASCII tags depends on the locale the cluster was created with.**
|
|
114
|
+
Exact matching goes through `LOWER(name) = LOWER(?)`, and `LOWER()` follows `lc_ctype`. On a UTF-8
|
|
115
|
+
locale it folds Cyrillic, Greek and the rest as you would expect. On a cluster initialised with
|
|
116
|
+
`lc_ctype = C` it folds ASCII only, and `"ПРИВЕТ"` and `"привет"` become two separate tags — the same
|
|
117
|
+
limitation described under SQLite below, on a database that otherwise has none.
|
|
118
|
+
|
|
119
|
+
Check with:
|
|
120
|
+
|
|
121
|
+
```sql
|
|
122
|
+
SHOW lc_ctype; -- C means ASCII-only folding
|
|
123
|
+
SELECT LOWER('Ü'); -- returns 'Ü' unchanged on such a cluster
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`ILIKE` is unaffected, so partial matching keeps working either way. If you need exact matching to
|
|
127
|
+
fold non-ASCII, create the database with a UTF-8 locale, or set
|
|
128
|
+
`MakeTaggable.strict_case_match = true` so the behaviour is at least consistent.
|
|
112
129
|
|
|
113
130
|
## MySQL
|
|
114
131
|
|
data/docs/querying.md
CHANGED
|
@@ -28,12 +28,18 @@ 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. Honoured by every option, `:exclude` included |
|
|
31
|
+
| `:on` | Restrict to one context, or an array of them. Honoured by every option, `:exclude` included |
|
|
32
32
|
| `:owned_by` | Restrict to tags applied by one tagger |
|
|
33
33
|
| `:order_by_matching_tag_count` | Order by how many matching taggings a record has, most first. No effect with `:match_all` |
|
|
34
34
|
| `:start_at` | Only tags applied after this time. Honoured by every option, `:exclude` included |
|
|
35
35
|
| `:end_at` | Only tags applied before this time. Honoured by every option, `:exclude` included |
|
|
36
36
|
|
|
37
|
+
`:on` takes several contexts as well as one, for searching a subset:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
Book.tagged_with("classic", on: [:genres, :moods], any: true)
|
|
41
|
+
```
|
|
42
|
+
|
|
37
43
|
An empty tag list means "nothing matches" for the matching options and "nothing is ruled out" for
|
|
38
44
|
`:exclude`, so the two always partition the scope between them:
|
|
39
45
|
|
|
@@ -183,6 +189,14 @@ its own — add `.distinct` if you want records back rather than matches.
|
|
|
183
189
|
is still worth reaching for `any: true` where the semantics allow it.
|
|
184
190
|
- Saving a taggable reads nothing from the taggings table unless a tag list was actually assigned,
|
|
185
191
|
so a save that touches no tags costs no extra query and works under `strict_loading`.
|
|
192
|
+
- On PostgreSQL, tag names are matched with `ILIKE` against the column itself rather than through
|
|
193
|
+
`LOWER()`, so an index on `tags.name` can be used. A wild search is the case that most wants one:
|
|
194
|
+
|
|
195
|
+
```sql
|
|
196
|
+
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
|
197
|
+
CREATE INDEX index_tags_on_name_trgm ON tags USING gin (name gin_trgm_ops);
|
|
198
|
+
```
|
|
199
|
+
|
|
186
200
|
- `:order_by_matching_tag_count` adds a correlated subquery to the `ORDER BY`. It is fine for a
|
|
187
201
|
page of results and expensive across a whole table.
|
|
188
202
|
- `all_tag_counts` joins the taggables to count them. Reach for `all_tags` when the counts are not
|
|
@@ -158,7 +158,11 @@ module MakeTaggable
|
|
|
158
158
|
map!(&:to_s)
|
|
159
159
|
map!(&:strip)
|
|
160
160
|
map!(&:downcase) if MakeTaggable.force_lowercase
|
|
161
|
-
|
|
161
|
+
# A name with no ASCII in it parameterizes to "", and reject! below would
|
|
162
|
+
# then drop it -- so the tag vanished rather than being slugged. Keep the
|
|
163
|
+
# original where there is no slug to be had; a caller who wanted strict
|
|
164
|
+
# slugs still gets one wherever one exists.
|
|
165
|
+
map! { |tag| tag.parameterize.presence || tag } if MakeTaggable.force_parameterize
|
|
162
166
|
|
|
163
167
|
MakeTaggable.strict_case_match ? uniq! : uniq! { |tag| tag.downcase }
|
|
164
168
|
self
|
|
@@ -118,7 +118,17 @@ module MakeTaggable::Taggable
|
|
|
118
118
|
initialize_make_taggable_core
|
|
119
119
|
end
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
##
|
|
122
|
+
# Every column on a table, qualified, joined for a `GROUP BY` clause.
|
|
123
|
+
#
|
|
124
|
+
# Nothing in the library needs this any more. PostgreSQL wanted every selected
|
|
125
|
+
# non-aggregated column in the `GROUP BY` before 9.1; since then the primary key is enough,
|
|
126
|
+
# and that is what the library groups by. Kept because it is public and useful for building
|
|
127
|
+
# such a clause by hand.
|
|
128
|
+
#
|
|
129
|
+
# @param object [Class] the model whose columns to list
|
|
130
|
+
# @return [String]
|
|
131
|
+
#
|
|
122
132
|
def grouped_column_names_for(object)
|
|
123
133
|
object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
|
|
124
134
|
end
|
|
@@ -179,7 +189,12 @@ module MakeTaggable::Taggable
|
|
|
179
189
|
end
|
|
180
190
|
end
|
|
181
191
|
|
|
182
|
-
|
|
192
|
+
##
|
|
193
|
+
# @see ClassMethods#grouped_column_names_for
|
|
194
|
+
#
|
|
195
|
+
# @param object [Class] the model whose columns to list
|
|
196
|
+
# @return [String]
|
|
197
|
+
#
|
|
183
198
|
def grouped_column_names_for(object)
|
|
184
199
|
self.class.grouped_column_names_for(object)
|
|
185
200
|
end
|
|
@@ -442,11 +457,15 @@ module MakeTaggable::Taggable
|
|
|
442
457
|
opts = ["#{tagging_table_name}.context = ?", context.to_s]
|
|
443
458
|
scope = base_tags.where(opts)
|
|
444
459
|
|
|
460
|
+
group_columns = "#{MakeTaggable::Tag.table_name}.#{MakeTaggable::Tag.primary_key}"
|
|
461
|
+
|
|
445
462
|
if MakeTaggable::Utils.using_postgresql?
|
|
446
|
-
|
|
463
|
+
# Ordering by an aggregate is why this groups at all. Grouping by the
|
|
464
|
+
# primary key alone is enough on every supported PostgreSQL -- it works
|
|
465
|
+
# the functional dependency out itself, and has since 9.1.
|
|
447
466
|
scope.order(Arel.sql("max(#{tagging_table_name}.created_at)")).group(group_columns)
|
|
448
467
|
else
|
|
449
|
-
scope.group(
|
|
468
|
+
scope.group(group_columns)
|
|
450
469
|
end.to_a
|
|
451
470
|
end
|
|
452
471
|
|
|
@@ -587,7 +606,13 @@ module MakeTaggable::Taggable
|
|
|
587
606
|
taggings.not_owned.by_context(context).where(tag_id: old_tags).destroy_all
|
|
588
607
|
end
|
|
589
608
|
|
|
590
|
-
# Create new taggings
|
|
609
|
+
# Create new taggings, in a consistent order. Each insert bumps the tag's
|
|
610
|
+
# counter cache, so two concurrent saves touching the same tags would
|
|
611
|
+
# otherwise take row locks in whatever order their lists happened to be
|
|
612
|
+
# in, and deadlock. Ordering is skipped where the model asked for tag
|
|
613
|
+
# order to be preserved, since there the creation order is the point.
|
|
614
|
+
new_tags = new_tags.sort_by(&:id) unless self.class.preserve_tag_order?
|
|
615
|
+
|
|
591
616
|
new_tags.each do |tag|
|
|
592
617
|
taggings.create!(tag_id: tag.id, context: context.to_s, taggable: self)
|
|
593
618
|
end
|
|
@@ -202,7 +202,10 @@ module MakeTaggable::Taggable
|
|
|
202
202
|
tag_id: old_tags, context: context).destroy_all
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
-
# Create new taggings
|
|
205
|
+
# Create new taggings, in a consistent order -- see the note in
|
|
206
|
+
# Core#save_tags on why the order matters.
|
|
207
|
+
new_tags = new_tags.sort_by(&:id) unless self.class.preserve_tag_order?
|
|
208
|
+
|
|
206
209
|
new_tags.each do |tag|
|
|
207
210
|
taggings.create!(tag_id: tag.id, context: context.to_s, tagger: owner, taggable: self)
|
|
208
211
|
end
|
|
@@ -56,7 +56,7 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
if options[:on].present?
|
|
59
|
-
condition = condition.and(
|
|
59
|
+
condition = condition.and(context_predicate)
|
|
60
60
|
end
|
|
61
61
|
|
|
62
62
|
if (owner = options[:owned_by]).present?
|
|
@@ -92,7 +92,7 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
if options[:on].present?
|
|
95
|
-
on_condition = on_condition.and(
|
|
95
|
+
on_condition = on_condition.and(context_predicate)
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
on_condition
|
|
@@ -28,7 +28,7 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
28
28
|
# Left off, the subquery gathers taggings from other contexts and other
|
|
29
29
|
# times, and excludes records on the strength of them.
|
|
30
30
|
if options[:on].present?
|
|
31
|
-
on_condition = on_condition.and(
|
|
31
|
+
on_condition = on_condition.and(context_predicate)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
if options[:start_at].present?
|
|
@@ -85,7 +85,7 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
if options[:on].present?
|
|
88
|
-
on_condition = on_condition.and(
|
|
88
|
+
on_condition = on_condition.and(context_predicate)
|
|
89
89
|
end
|
|
90
90
|
|
|
91
91
|
on_condition
|
|
@@ -40,8 +40,7 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def tag_match_type(tag)
|
|
43
|
-
matches_attribute =
|
|
44
|
-
matches_attribute = matches_attribute.lower unless MakeTaggable.strict_case_match
|
|
43
|
+
matches_attribute = folded_name_attribute
|
|
45
44
|
|
|
46
45
|
if options[:wild].present?
|
|
47
46
|
matches_attribute.matches("%#{escaped_tag(tag)}%", "!", MakeTaggable.strict_case_match)
|
|
@@ -51,8 +50,7 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
def tags_match_type
|
|
54
|
-
matches_attribute =
|
|
55
|
-
matches_attribute = matches_attribute.lower unless MakeTaggable.strict_case_match
|
|
53
|
+
matches_attribute = folded_name_attribute
|
|
56
54
|
|
|
57
55
|
if options[:wild].present?
|
|
58
56
|
matches_attribute.matches_any(tag_list.map { |tag| "%#{escaped_tag(tag)}%" }, "!", MakeTaggable.strict_case_match)
|
|
@@ -85,7 +83,7 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
85
83
|
end
|
|
86
84
|
|
|
87
85
|
if options[:on].present?
|
|
88
|
-
condition = condition.and(
|
|
86
|
+
condition = condition.and(context_predicate)
|
|
89
87
|
end
|
|
90
88
|
|
|
91
89
|
if (owner = options[:owned_by]).present?
|
|
@@ -101,6 +99,32 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
101
99
|
"(SELECT count(*) FROM #{tagging_model.table_name} WHERE #{matching_taggings.to_sql}) desc"
|
|
102
100
|
end
|
|
103
101
|
|
|
102
|
+
# The predicate restricting a query to the context or contexts asked for.
|
|
103
|
+
#
|
|
104
|
+
# `:on` takes one context or several, so a caller can search a subset without either naming a
|
|
105
|
+
# single one or falling back to every context there is.
|
|
106
|
+
def context_predicate(tagging_table = tagging_arel_table)
|
|
107
|
+
contexts = Array(options[:on]).map(&:to_s)
|
|
108
|
+
|
|
109
|
+
contexts.one? ? tagging_table[:context].eq(contexts.first) : tagging_table[:context].in(contexts)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The tag name attribute to match against, folded where the comparison will not fold it itself.
|
|
113
|
+
#
|
|
114
|
+
# PostgreSQL matches with ILIKE, which is already case-insensitive, so wrapping the column in
|
|
115
|
+
# LOWER() there changes nothing and costs everything -- the expression stops being sargable, so
|
|
116
|
+
# no index on tags.name can be used, including a trigram index built for wild searches.
|
|
117
|
+
#
|
|
118
|
+
# The other adapters do need it. The MySQL migration collates tags.name as utf8mb4_bin, which
|
|
119
|
+
# makes LIKE case-sensitive, and folding keeps SQLite consistent with the rest.
|
|
120
|
+
def folded_name_attribute
|
|
121
|
+
attribute = tag_arel_table[:name]
|
|
122
|
+
|
|
123
|
+
return attribute if MakeTaggable.strict_case_match || MakeTaggable::Utils.using_postgresql?
|
|
124
|
+
|
|
125
|
+
attribute.lower
|
|
126
|
+
end
|
|
127
|
+
|
|
104
128
|
def escaped_tag(tag)
|
|
105
129
|
tag = tag.downcase unless MakeTaggable.strict_case_match
|
|
106
130
|
MakeTaggable::Utils.escape_like(tag)
|
data/lib/make_taggable.rb
CHANGED
|
@@ -254,16 +254,40 @@ module MakeTaggable
|
|
|
254
254
|
# `utf8mb4_general_ci`
|
|
255
255
|
# @return [NilClass]
|
|
256
256
|
#
|
|
257
|
+
##
|
|
258
|
+
# Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
|
|
259
|
+
#
|
|
260
|
+
# This is a schema change, and the documented way to reach it is an initializer -- which runs
|
|
261
|
+
# once per process, so once per web worker, background worker, console and rake task. Issuing
|
|
262
|
+
# `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
|
|
263
|
+
# current collation is read first and the statement skipped when it already matches, which
|
|
264
|
+
# turns the common case into one cheap catalogue read.
|
|
265
|
+
#
|
|
266
|
+
# @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
|
|
267
|
+
# @return [void]
|
|
268
|
+
#
|
|
257
269
|
def self.apply_binary_collation(bincoll)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
270
|
+
return unless Utils.using_mysql?
|
|
271
|
+
|
|
272
|
+
collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"
|
|
273
|
+
|
|
274
|
+
# Nothing to apply to yet -- this runs during the first migration, before
|
|
275
|
+
# the table exists.
|
|
276
|
+
return unless Utils.connection.table_exists?(Tag.table_name)
|
|
277
|
+
return if current_tag_name_collation == collation
|
|
278
|
+
|
|
279
|
+
ActiveRecord::Migration.execute(
|
|
280
|
+
"ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
##
|
|
285
|
+
# The collation `tags.name` currently carries, or `nil` where it cannot be read.
|
|
286
|
+
#
|
|
287
|
+
# @return [String, NilClass]
|
|
288
|
+
#
|
|
289
|
+
def self.current_tag_name_collation
|
|
290
|
+
Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
|
|
267
291
|
end
|
|
268
292
|
end
|
|
269
293
|
setup
|