Class: MakeTaggable::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/make_taggable.rb

Overview

The library's settings.

Reach these through setup or directly on MakeTaggable, which forwards to the single instance held here.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeMakeTaggable::Configuration

Builds the configuration with the library's defaults.



189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/make_taggable.rb', line 189

def initialize
  @delimiter = ","
  @force_lowercase = false
  @force_parameterize = false
  @strict_case_match = false
  @remove_unused_tags = false
  @tags_counter = true
  @default_parser = DefaultParser
  @force_binary_collation = false
  @tags_table = :tags
  @taggings_table = :taggings
  @tag_class = "MakeTaggable::Tag"
end

Instance Attribute Details

#default_parserClass

The class used to turn tag input into a TagList.

Returns:



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/make_taggable.rb', line 177

class Configuration
  attr_accessor :force_lowercase, :force_parameterize,
    :remove_unused_tags, :default_parser,
    :tags_counter, :tags_table,
    :taggings_table
  attr_reader :delimiter, :strict_case_match, :tag_class

  ##
  # Builds the configuration with the library's defaults.
  #
  # @return [MakeTaggable::Configuration]
  #
  def initialize
    @delimiter = ","
    @force_lowercase = false
    @force_parameterize = false
    @strict_case_match = false
    @remove_unused_tags = false
    @tags_counter = true
    @default_parser = DefaultParser
    @force_binary_collation = false
    @tags_table = :tags
    @taggings_table = :taggings
    @tag_class = "MakeTaggable::Tag"
  end

  ##
  # Sets the class tags are read, written and returned as.
  #
  # Must be a String. A model constant cannot be referenced while initializers run -- Zeitwerk
  # has not defined it yet, and eager loading in production would try to resolve it before the
  # class exists.
  #
  # @param class_name [String] the name of a class inheriting from {MakeTaggable::Tag}
  # @return [String]
  # @raise [ArgumentError] when given anything but a String
  #
  def tag_class=(class_name)
    unless class_name.is_a?(String)
      raise ArgumentError,
        "tag_class must be a String, got #{class_name.inspect}. " \
        "Naming the class rather than the constant is what lets it be set in an initializer, " \
        "before Zeitwerk has defined it."
    end

    @tag_class = class_name
  end

  ##
  # Sets whether tag lookups are case sensitive.
  #
  # Ignored while {#force_binary_collation=} is on, which already implies case sensitivity.
  #
  # @param force_cs [TrueClass, FalseClass] whether to match case exactly
  # @return [TrueClass, FalseClass]
  #
  def strict_case_match=(force_cs)
    @strict_case_match = force_cs unless @force_binary_collation
  end

  ##
  # Sets the delimiter separating tags in a string.
  #
  # @deprecated Configure a {#default_parser} instead. This will be removed in a future release.
  #
  # @param string [String, Array<String>] one delimiter, or several
  # @return [String, Array<String>]
  #
  # @example Several delimiters, matched literally
  #   MakeTaggable.delimiter = [",", "|"]
  #
  def delimiter=(string)
    # Active Record does not always have a logger -- a plain Active Record
    # process, or an initializer running before the logger is assigned, will
    # both leave it nil.
    ActiveRecord::Base.logger&.warn(
      "MakeTaggable.delimiter is deprecated and will be removed in a future " \
      "release. Configure a MakeTaggable.default_parser instead."
    )
    @delimiter = string
  end

  ##
  # Switches the MySQL tag name column between a binary and a case-insensitive collation.
  #
  # A binary collation makes tag names compare exactly, including accented and other multi-byte
  # characters, and forces {#strict_case_match} on. Does nothing on other adapters.
  #
  # @param force_bin [TrueClass, FalseClass] whether to apply the binary collation
  # @return [TrueClass, FalseClass]
  #
  def force_binary_collation=(force_bin)
    if Utils.using_mysql?
      if force_bin
        Configuration.apply_binary_collation(true)
        @force_binary_collation = true
        @strict_case_match = true
      else
        Configuration.apply_binary_collation(false)
        @force_binary_collation = false
      end
    end
  end

  ##
  # Alters the MySQL tag name column to the requested collation.
  #
  # Failures are reported rather than raised: the column does not exist yet the first time the
  # migrations run, and that is not an error.
  #
  # @param bincoll [TrueClass, FalseClass] `true` for `utf8mb4_bin`, `false` for
  #   `utf8mb4_general_ci`
  # @return [NilClass]
  #
  ##
  # Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
  #
  # This is a schema change, and the documented way to reach it is an initializer -- which runs
  # once per process, so once per web worker, background worker, console and rake task. Issuing
  # `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
  # current collation is read first and the statement skipped when it already matches, which
  # turns the common case into one cheap catalogue read.
  #
  # @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
  # @return [void]
  #
  def self.apply_binary_collation(bincoll)
    return unless Utils.using_mysql?

    collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

    # Nothing to apply to yet -- this runs during the first migration, before
    # the table exists.
    return unless Utils.connection.table_exists?(Tag.table_name)
    return if current_tag_name_collation == collation

    ActiveRecord::Migration.execute(
      "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
    )
  end

  ##
  # The collation `tags.name` currently carries, or `nil` where it cannot be read.
  #
  # @return [String, NilClass]
  #
  def self.current_tag_name_collation
    Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
  end
end

#delimiterString+

The delimiter, or delimiters, separating tags in a string. Metacharacters are escaped, so each one is matched literally.

Returns:

  • (String, Array<String>)

    defaults to ","



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/make_taggable.rb', line 177

class Configuration
  attr_accessor :force_lowercase, :force_parameterize,
    :remove_unused_tags, :default_parser,
    :tags_counter, :tags_table,
    :taggings_table
  attr_reader :delimiter, :strict_case_match, :tag_class

  ##
  # Builds the configuration with the library's defaults.
  #
  # @return [MakeTaggable::Configuration]
  #
  def initialize
    @delimiter = ","
    @force_lowercase = false
    @force_parameterize = false
    @strict_case_match = false
    @remove_unused_tags = false
    @tags_counter = true
    @default_parser = DefaultParser
    @force_binary_collation = false
    @tags_table = :tags
    @taggings_table = :taggings
    @tag_class = "MakeTaggable::Tag"
  end

  ##
  # Sets the class tags are read, written and returned as.
  #
  # Must be a String. A model constant cannot be referenced while initializers run -- Zeitwerk
  # has not defined it yet, and eager loading in production would try to resolve it before the
  # class exists.
  #
  # @param class_name [String] the name of a class inheriting from {MakeTaggable::Tag}
  # @return [String]
  # @raise [ArgumentError] when given anything but a String
  #
  def tag_class=(class_name)
    unless class_name.is_a?(String)
      raise ArgumentError,
        "tag_class must be a String, got #{class_name.inspect}. " \
        "Naming the class rather than the constant is what lets it be set in an initializer, " \
        "before Zeitwerk has defined it."
    end

    @tag_class = class_name
  end

  ##
  # Sets whether tag lookups are case sensitive.
  #
  # Ignored while {#force_binary_collation=} is on, which already implies case sensitivity.
  #
  # @param force_cs [TrueClass, FalseClass] whether to match case exactly
  # @return [TrueClass, FalseClass]
  #
  def strict_case_match=(force_cs)
    @strict_case_match = force_cs unless @force_binary_collation
  end

  ##
  # Sets the delimiter separating tags in a string.
  #
  # @deprecated Configure a {#default_parser} instead. This will be removed in a future release.
  #
  # @param string [String, Array<String>] one delimiter, or several
  # @return [String, Array<String>]
  #
  # @example Several delimiters, matched literally
  #   MakeTaggable.delimiter = [",", "|"]
  #
  def delimiter=(string)
    # Active Record does not always have a logger -- a plain Active Record
    # process, or an initializer running before the logger is assigned, will
    # both leave it nil.
    ActiveRecord::Base.logger&.warn(
      "MakeTaggable.delimiter is deprecated and will be removed in a future " \
      "release. Configure a MakeTaggable.default_parser instead."
    )
    @delimiter = string
  end

  ##
  # Switches the MySQL tag name column between a binary and a case-insensitive collation.
  #
  # A binary collation makes tag names compare exactly, including accented and other multi-byte
  # characters, and forces {#strict_case_match} on. Does nothing on other adapters.
  #
  # @param force_bin [TrueClass, FalseClass] whether to apply the binary collation
  # @return [TrueClass, FalseClass]
  #
  def force_binary_collation=(force_bin)
    if Utils.using_mysql?
      if force_bin
        Configuration.apply_binary_collation(true)
        @force_binary_collation = true
        @strict_case_match = true
      else
        Configuration.apply_binary_collation(false)
        @force_binary_collation = false
      end
    end
  end

  ##
  # Alters the MySQL tag name column to the requested collation.
  #
  # Failures are reported rather than raised: the column does not exist yet the first time the
  # migrations run, and that is not an error.
  #
  # @param bincoll [TrueClass, FalseClass] `true` for `utf8mb4_bin`, `false` for
  #   `utf8mb4_general_ci`
  # @return [NilClass]
  #
  ##
  # Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
  #
  # This is a schema change, and the documented way to reach it is an initializer -- which runs
  # once per process, so once per web worker, background worker, console and rake task. Issuing
  # `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
  # current collation is read first and the statement skipped when it already matches, which
  # turns the common case into one cheap catalogue read.
  #
  # @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
  # @return [void]
  #
  def self.apply_binary_collation(bincoll)
    return unless Utils.using_mysql?

    collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

    # Nothing to apply to yet -- this runs during the first migration, before
    # the table exists.
    return unless Utils.connection.table_exists?(Tag.table_name)
    return if current_tag_name_collation == collation

    ActiveRecord::Migration.execute(
      "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
    )
  end

  ##
  # The collation `tags.name` currently carries, or `nil` where it cannot be read.
  #
  # @return [String, NilClass]
  #
  def self.current_tag_name_collation
    Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
  end
end

#force_lowercaseTrueClass, FalseClass

Whether tag names are downcased before they are saved.

Returns:

  • (TrueClass, FalseClass)

    defaults to false



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/make_taggable.rb', line 177

class Configuration
  attr_accessor :force_lowercase, :force_parameterize,
    :remove_unused_tags, :default_parser,
    :tags_counter, :tags_table,
    :taggings_table
  attr_reader :delimiter, :strict_case_match, :tag_class

  ##
  # Builds the configuration with the library's defaults.
  #
  # @return [MakeTaggable::Configuration]
  #
  def initialize
    @delimiter = ","
    @force_lowercase = false
    @force_parameterize = false
    @strict_case_match = false
    @remove_unused_tags = false
    @tags_counter = true
    @default_parser = DefaultParser
    @force_binary_collation = false
    @tags_table = :tags
    @taggings_table = :taggings
    @tag_class = "MakeTaggable::Tag"
  end

  ##
  # Sets the class tags are read, written and returned as.
  #
  # Must be a String. A model constant cannot be referenced while initializers run -- Zeitwerk
  # has not defined it yet, and eager loading in production would try to resolve it before the
  # class exists.
  #
  # @param class_name [String] the name of a class inheriting from {MakeTaggable::Tag}
  # @return [String]
  # @raise [ArgumentError] when given anything but a String
  #
  def tag_class=(class_name)
    unless class_name.is_a?(String)
      raise ArgumentError,
        "tag_class must be a String, got #{class_name.inspect}. " \
        "Naming the class rather than the constant is what lets it be set in an initializer, " \
        "before Zeitwerk has defined it."
    end

    @tag_class = class_name
  end

  ##
  # Sets whether tag lookups are case sensitive.
  #
  # Ignored while {#force_binary_collation=} is on, which already implies case sensitivity.
  #
  # @param force_cs [TrueClass, FalseClass] whether to match case exactly
  # @return [TrueClass, FalseClass]
  #
  def strict_case_match=(force_cs)
    @strict_case_match = force_cs unless @force_binary_collation
  end

  ##
  # Sets the delimiter separating tags in a string.
  #
  # @deprecated Configure a {#default_parser} instead. This will be removed in a future release.
  #
  # @param string [String, Array<String>] one delimiter, or several
  # @return [String, Array<String>]
  #
  # @example Several delimiters, matched literally
  #   MakeTaggable.delimiter = [",", "|"]
  #
  def delimiter=(string)
    # Active Record does not always have a logger -- a plain Active Record
    # process, or an initializer running before the logger is assigned, will
    # both leave it nil.
    ActiveRecord::Base.logger&.warn(
      "MakeTaggable.delimiter is deprecated and will be removed in a future " \
      "release. Configure a MakeTaggable.default_parser instead."
    )
    @delimiter = string
  end

  ##
  # Switches the MySQL tag name column between a binary and a case-insensitive collation.
  #
  # A binary collation makes tag names compare exactly, including accented and other multi-byte
  # characters, and forces {#strict_case_match} on. Does nothing on other adapters.
  #
  # @param force_bin [TrueClass, FalseClass] whether to apply the binary collation
  # @return [TrueClass, FalseClass]
  #
  def force_binary_collation=(force_bin)
    if Utils.using_mysql?
      if force_bin
        Configuration.apply_binary_collation(true)
        @force_binary_collation = true
        @strict_case_match = true
      else
        Configuration.apply_binary_collation(false)
        @force_binary_collation = false
      end
    end
  end

  ##
  # Alters the MySQL tag name column to the requested collation.
  #
  # Failures are reported rather than raised: the column does not exist yet the first time the
  # migrations run, and that is not an error.
  #
  # @param bincoll [TrueClass, FalseClass] `true` for `utf8mb4_bin`, `false` for
  #   `utf8mb4_general_ci`
  # @return [NilClass]
  #
  ##
  # Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
  #
  # This is a schema change, and the documented way to reach it is an initializer -- which runs
  # once per process, so once per web worker, background worker, console and rake task. Issuing
  # `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
  # current collation is read first and the statement skipped when it already matches, which
  # turns the common case into one cheap catalogue read.
  #
  # @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
  # @return [void]
  #
  def self.apply_binary_collation(bincoll)
    return unless Utils.using_mysql?

    collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

    # Nothing to apply to yet -- this runs during the first migration, before
    # the table exists.
    return unless Utils.connection.table_exists?(Tag.table_name)
    return if current_tag_name_collation == collation

    ActiveRecord::Migration.execute(
      "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
    )
  end

  ##
  # The collation `tags.name` currently carries, or `nil` where it cannot be read.
  #
  # @return [String, NilClass]
  #
  def self.current_tag_name_collation
    Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
  end
end

#force_parameterizeTrueClass, FalseClass

Whether tag names are parameterized before they are saved.

Returns:

  • (TrueClass, FalseClass)

    defaults to false



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/make_taggable.rb', line 177

class Configuration
  attr_accessor :force_lowercase, :force_parameterize,
    :remove_unused_tags, :default_parser,
    :tags_counter, :tags_table,
    :taggings_table
  attr_reader :delimiter, :strict_case_match, :tag_class

  ##
  # Builds the configuration with the library's defaults.
  #
  # @return [MakeTaggable::Configuration]
  #
  def initialize
    @delimiter = ","
    @force_lowercase = false
    @force_parameterize = false
    @strict_case_match = false
    @remove_unused_tags = false
    @tags_counter = true
    @default_parser = DefaultParser
    @force_binary_collation = false
    @tags_table = :tags
    @taggings_table = :taggings
    @tag_class = "MakeTaggable::Tag"
  end

  ##
  # Sets the class tags are read, written and returned as.
  #
  # Must be a String. A model constant cannot be referenced while initializers run -- Zeitwerk
  # has not defined it yet, and eager loading in production would try to resolve it before the
  # class exists.
  #
  # @param class_name [String] the name of a class inheriting from {MakeTaggable::Tag}
  # @return [String]
  # @raise [ArgumentError] when given anything but a String
  #
  def tag_class=(class_name)
    unless class_name.is_a?(String)
      raise ArgumentError,
        "tag_class must be a String, got #{class_name.inspect}. " \
        "Naming the class rather than the constant is what lets it be set in an initializer, " \
        "before Zeitwerk has defined it."
    end

    @tag_class = class_name
  end

  ##
  # Sets whether tag lookups are case sensitive.
  #
  # Ignored while {#force_binary_collation=} is on, which already implies case sensitivity.
  #
  # @param force_cs [TrueClass, FalseClass] whether to match case exactly
  # @return [TrueClass, FalseClass]
  #
  def strict_case_match=(force_cs)
    @strict_case_match = force_cs unless @force_binary_collation
  end

  ##
  # Sets the delimiter separating tags in a string.
  #
  # @deprecated Configure a {#default_parser} instead. This will be removed in a future release.
  #
  # @param string [String, Array<String>] one delimiter, or several
  # @return [String, Array<String>]
  #
  # @example Several delimiters, matched literally
  #   MakeTaggable.delimiter = [",", "|"]
  #
  def delimiter=(string)
    # Active Record does not always have a logger -- a plain Active Record
    # process, or an initializer running before the logger is assigned, will
    # both leave it nil.
    ActiveRecord::Base.logger&.warn(
      "MakeTaggable.delimiter is deprecated and will be removed in a future " \
      "release. Configure a MakeTaggable.default_parser instead."
    )
    @delimiter = string
  end

  ##
  # Switches the MySQL tag name column between a binary and a case-insensitive collation.
  #
  # A binary collation makes tag names compare exactly, including accented and other multi-byte
  # characters, and forces {#strict_case_match} on. Does nothing on other adapters.
  #
  # @param force_bin [TrueClass, FalseClass] whether to apply the binary collation
  # @return [TrueClass, FalseClass]
  #
  def force_binary_collation=(force_bin)
    if Utils.using_mysql?
      if force_bin
        Configuration.apply_binary_collation(true)
        @force_binary_collation = true
        @strict_case_match = true
      else
        Configuration.apply_binary_collation(false)
        @force_binary_collation = false
      end
    end
  end

  ##
  # Alters the MySQL tag name column to the requested collation.
  #
  # Failures are reported rather than raised: the column does not exist yet the first time the
  # migrations run, and that is not an error.
  #
  # @param bincoll [TrueClass, FalseClass] `true` for `utf8mb4_bin`, `false` for
  #   `utf8mb4_general_ci`
  # @return [NilClass]
  #
  ##
  # Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
  #
  # This is a schema change, and the documented way to reach it is an initializer -- which runs
  # once per process, so once per web worker, background worker, console and rake task. Issuing
  # `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
  # current collation is read first and the statement skipped when it already matches, which
  # turns the common case into one cheap catalogue read.
  #
  # @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
  # @return [void]
  #
  def self.apply_binary_collation(bincoll)
    return unless Utils.using_mysql?

    collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

    # Nothing to apply to yet -- this runs during the first migration, before
    # the table exists.
    return unless Utils.connection.table_exists?(Tag.table_name)
    return if current_tag_name_collation == collation

    ActiveRecord::Migration.execute(
      "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
    )
  end

  ##
  # The collation `tags.name` currently carries, or `nil` where it cannot be read.
  #
  # @return [String, NilClass]
  #
  def self.current_tag_name_collation
    Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
  end
end

#remove_unused_tagsTrueClass, FalseClass

Whether a tag row is destroyed once its last tagging goes away. Works with or without tags_counter; without it the check costs one extra query per destroyed tagging.

Returns:

  • (TrueClass, FalseClass)

    defaults to false



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/make_taggable.rb', line 177

class Configuration
  attr_accessor :force_lowercase, :force_parameterize,
    :remove_unused_tags, :default_parser,
    :tags_counter, :tags_table,
    :taggings_table
  attr_reader :delimiter, :strict_case_match, :tag_class

  ##
  # Builds the configuration with the library's defaults.
  #
  # @return [MakeTaggable::Configuration]
  #
  def initialize
    @delimiter = ","
    @force_lowercase = false
    @force_parameterize = false
    @strict_case_match = false
    @remove_unused_tags = false
    @tags_counter = true
    @default_parser = DefaultParser
    @force_binary_collation = false
    @tags_table = :tags
    @taggings_table = :taggings
    @tag_class = "MakeTaggable::Tag"
  end

  ##
  # Sets the class tags are read, written and returned as.
  #
  # Must be a String. A model constant cannot be referenced while initializers run -- Zeitwerk
  # has not defined it yet, and eager loading in production would try to resolve it before the
  # class exists.
  #
  # @param class_name [String] the name of a class inheriting from {MakeTaggable::Tag}
  # @return [String]
  # @raise [ArgumentError] when given anything but a String
  #
  def tag_class=(class_name)
    unless class_name.is_a?(String)
      raise ArgumentError,
        "tag_class must be a String, got #{class_name.inspect}. " \
        "Naming the class rather than the constant is what lets it be set in an initializer, " \
        "before Zeitwerk has defined it."
    end

    @tag_class = class_name
  end

  ##
  # Sets whether tag lookups are case sensitive.
  #
  # Ignored while {#force_binary_collation=} is on, which already implies case sensitivity.
  #
  # @param force_cs [TrueClass, FalseClass] whether to match case exactly
  # @return [TrueClass, FalseClass]
  #
  def strict_case_match=(force_cs)
    @strict_case_match = force_cs unless @force_binary_collation
  end

  ##
  # Sets the delimiter separating tags in a string.
  #
  # @deprecated Configure a {#default_parser} instead. This will be removed in a future release.
  #
  # @param string [String, Array<String>] one delimiter, or several
  # @return [String, Array<String>]
  #
  # @example Several delimiters, matched literally
  #   MakeTaggable.delimiter = [",", "|"]
  #
  def delimiter=(string)
    # Active Record does not always have a logger -- a plain Active Record
    # process, or an initializer running before the logger is assigned, will
    # both leave it nil.
    ActiveRecord::Base.logger&.warn(
      "MakeTaggable.delimiter is deprecated and will be removed in a future " \
      "release. Configure a MakeTaggable.default_parser instead."
    )
    @delimiter = string
  end

  ##
  # Switches the MySQL tag name column between a binary and a case-insensitive collation.
  #
  # A binary collation makes tag names compare exactly, including accented and other multi-byte
  # characters, and forces {#strict_case_match} on. Does nothing on other adapters.
  #
  # @param force_bin [TrueClass, FalseClass] whether to apply the binary collation
  # @return [TrueClass, FalseClass]
  #
  def force_binary_collation=(force_bin)
    if Utils.using_mysql?
      if force_bin
        Configuration.apply_binary_collation(true)
        @force_binary_collation = true
        @strict_case_match = true
      else
        Configuration.apply_binary_collation(false)
        @force_binary_collation = false
      end
    end
  end

  ##
  # Alters the MySQL tag name column to the requested collation.
  #
  # Failures are reported rather than raised: the column does not exist yet the first time the
  # migrations run, and that is not an error.
  #
  # @param bincoll [TrueClass, FalseClass] `true` for `utf8mb4_bin`, `false` for
  #   `utf8mb4_general_ci`
  # @return [NilClass]
  #
  ##
  # Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
  #
  # This is a schema change, and the documented way to reach it is an initializer -- which runs
  # once per process, so once per web worker, background worker, console and rake task. Issuing
  # `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
  # current collation is read first and the statement skipped when it already matches, which
  # turns the common case into one cheap catalogue read.
  #
  # @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
  # @return [void]
  #
  def self.apply_binary_collation(bincoll)
    return unless Utils.using_mysql?

    collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

    # Nothing to apply to yet -- this runs during the first migration, before
    # the table exists.
    return unless Utils.connection.table_exists?(Tag.table_name)
    return if current_tag_name_collation == collation

    ActiveRecord::Migration.execute(
      "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
    )
  end

  ##
  # The collation `tags.name` currently carries, or `nil` where it cannot be read.
  #
  # @return [String, NilClass]
  #
  def self.current_tag_name_collation
    Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
  end
end

#strict_case_matchTrueClass, FalseClass

Whether tag lookups are case sensitive.

Returns:

  • (TrueClass, FalseClass)

    defaults to false



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/make_taggable.rb', line 177

class Configuration
  attr_accessor :force_lowercase, :force_parameterize,
    :remove_unused_tags, :default_parser,
    :tags_counter, :tags_table,
    :taggings_table
  attr_reader :delimiter, :strict_case_match, :tag_class

  ##
  # Builds the configuration with the library's defaults.
  #
  # @return [MakeTaggable::Configuration]
  #
  def initialize
    @delimiter = ","
    @force_lowercase = false
    @force_parameterize = false
    @strict_case_match = false
    @remove_unused_tags = false
    @tags_counter = true
    @default_parser = DefaultParser
    @force_binary_collation = false
    @tags_table = :tags
    @taggings_table = :taggings
    @tag_class = "MakeTaggable::Tag"
  end

  ##
  # Sets the class tags are read, written and returned as.
  #
  # Must be a String. A model constant cannot be referenced while initializers run -- Zeitwerk
  # has not defined it yet, and eager loading in production would try to resolve it before the
  # class exists.
  #
  # @param class_name [String] the name of a class inheriting from {MakeTaggable::Tag}
  # @return [String]
  # @raise [ArgumentError] when given anything but a String
  #
  def tag_class=(class_name)
    unless class_name.is_a?(String)
      raise ArgumentError,
        "tag_class must be a String, got #{class_name.inspect}. " \
        "Naming the class rather than the constant is what lets it be set in an initializer, " \
        "before Zeitwerk has defined it."
    end

    @tag_class = class_name
  end

  ##
  # Sets whether tag lookups are case sensitive.
  #
  # Ignored while {#force_binary_collation=} is on, which already implies case sensitivity.
  #
  # @param force_cs [TrueClass, FalseClass] whether to match case exactly
  # @return [TrueClass, FalseClass]
  #
  def strict_case_match=(force_cs)
    @strict_case_match = force_cs unless @force_binary_collation
  end

  ##
  # Sets the delimiter separating tags in a string.
  #
  # @deprecated Configure a {#default_parser} instead. This will be removed in a future release.
  #
  # @param string [String, Array<String>] one delimiter, or several
  # @return [String, Array<String>]
  #
  # @example Several delimiters, matched literally
  #   MakeTaggable.delimiter = [",", "|"]
  #
  def delimiter=(string)
    # Active Record does not always have a logger -- a plain Active Record
    # process, or an initializer running before the logger is assigned, will
    # both leave it nil.
    ActiveRecord::Base.logger&.warn(
      "MakeTaggable.delimiter is deprecated and will be removed in a future " \
      "release. Configure a MakeTaggable.default_parser instead."
    )
    @delimiter = string
  end

  ##
  # Switches the MySQL tag name column between a binary and a case-insensitive collation.
  #
  # A binary collation makes tag names compare exactly, including accented and other multi-byte
  # characters, and forces {#strict_case_match} on. Does nothing on other adapters.
  #
  # @param force_bin [TrueClass, FalseClass] whether to apply the binary collation
  # @return [TrueClass, FalseClass]
  #
  def force_binary_collation=(force_bin)
    if Utils.using_mysql?
      if force_bin
        Configuration.apply_binary_collation(true)
        @force_binary_collation = true
        @strict_case_match = true
      else
        Configuration.apply_binary_collation(false)
        @force_binary_collation = false
      end
    end
  end

  ##
  # Alters the MySQL tag name column to the requested collation.
  #
  # Failures are reported rather than raised: the column does not exist yet the first time the
  # migrations run, and that is not an error.
  #
  # @param bincoll [TrueClass, FalseClass] `true` for `utf8mb4_bin`, `false` for
  #   `utf8mb4_general_ci`
  # @return [NilClass]
  #
  ##
  # Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
  #
  # This is a schema change, and the documented way to reach it is an initializer -- which runs
  # once per process, so once per web worker, background worker, console and rake task. Issuing
  # `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
  # current collation is read first and the statement skipped when it already matches, which
  # turns the common case into one cheap catalogue read.
  #
  # @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
  # @return [void]
  #
  def self.apply_binary_collation(bincoll)
    return unless Utils.using_mysql?

    collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

    # Nothing to apply to yet -- this runs during the first migration, before
    # the table exists.
    return unless Utils.connection.table_exists?(Tag.table_name)
    return if current_tag_name_collation == collation

    ActiveRecord::Migration.execute(
      "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
    )
  end

  ##
  # The collation `tags.name` currently carries, or `nil` where it cannot be read.
  #
  # @return [String, NilClass]
  #
  def self.current_tag_name_collation
    Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
  end
end

#tag_classObject

Returns the value of attribute tag_class.



182
183
184
# File 'lib/make_taggable.rb', line 182

def tag_class
  @tag_class
end

#taggings_tableSymbol, String

The table backing Tagging.

Returns:

  • (Symbol, String)

    defaults to :taggings



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/make_taggable.rb', line 177

class Configuration
  attr_accessor :force_lowercase, :force_parameterize,
    :remove_unused_tags, :default_parser,
    :tags_counter, :tags_table,
    :taggings_table
  attr_reader :delimiter, :strict_case_match, :tag_class

  ##
  # Builds the configuration with the library's defaults.
  #
  # @return [MakeTaggable::Configuration]
  #
  def initialize
    @delimiter = ","
    @force_lowercase = false
    @force_parameterize = false
    @strict_case_match = false
    @remove_unused_tags = false
    @tags_counter = true
    @default_parser = DefaultParser
    @force_binary_collation = false
    @tags_table = :tags
    @taggings_table = :taggings
    @tag_class = "MakeTaggable::Tag"
  end

  ##
  # Sets the class tags are read, written and returned as.
  #
  # Must be a String. A model constant cannot be referenced while initializers run -- Zeitwerk
  # has not defined it yet, and eager loading in production would try to resolve it before the
  # class exists.
  #
  # @param class_name [String] the name of a class inheriting from {MakeTaggable::Tag}
  # @return [String]
  # @raise [ArgumentError] when given anything but a String
  #
  def tag_class=(class_name)
    unless class_name.is_a?(String)
      raise ArgumentError,
        "tag_class must be a String, got #{class_name.inspect}. " \
        "Naming the class rather than the constant is what lets it be set in an initializer, " \
        "before Zeitwerk has defined it."
    end

    @tag_class = class_name
  end

  ##
  # Sets whether tag lookups are case sensitive.
  #
  # Ignored while {#force_binary_collation=} is on, which already implies case sensitivity.
  #
  # @param force_cs [TrueClass, FalseClass] whether to match case exactly
  # @return [TrueClass, FalseClass]
  #
  def strict_case_match=(force_cs)
    @strict_case_match = force_cs unless @force_binary_collation
  end

  ##
  # Sets the delimiter separating tags in a string.
  #
  # @deprecated Configure a {#default_parser} instead. This will be removed in a future release.
  #
  # @param string [String, Array<String>] one delimiter, or several
  # @return [String, Array<String>]
  #
  # @example Several delimiters, matched literally
  #   MakeTaggable.delimiter = [",", "|"]
  #
  def delimiter=(string)
    # Active Record does not always have a logger -- a plain Active Record
    # process, or an initializer running before the logger is assigned, will
    # both leave it nil.
    ActiveRecord::Base.logger&.warn(
      "MakeTaggable.delimiter is deprecated and will be removed in a future " \
      "release. Configure a MakeTaggable.default_parser instead."
    )
    @delimiter = string
  end

  ##
  # Switches the MySQL tag name column between a binary and a case-insensitive collation.
  #
  # A binary collation makes tag names compare exactly, including accented and other multi-byte
  # characters, and forces {#strict_case_match} on. Does nothing on other adapters.
  #
  # @param force_bin [TrueClass, FalseClass] whether to apply the binary collation
  # @return [TrueClass, FalseClass]
  #
  def force_binary_collation=(force_bin)
    if Utils.using_mysql?
      if force_bin
        Configuration.apply_binary_collation(true)
        @force_binary_collation = true
        @strict_case_match = true
      else
        Configuration.apply_binary_collation(false)
        @force_binary_collation = false
      end
    end
  end

  ##
  # Alters the MySQL tag name column to the requested collation.
  #
  # Failures are reported rather than raised: the column does not exist yet the first time the
  # migrations run, and that is not an error.
  #
  # @param bincoll [TrueClass, FalseClass] `true` for `utf8mb4_bin`, `false` for
  #   `utf8mb4_general_ci`
  # @return [NilClass]
  #
  ##
  # Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
  #
  # This is a schema change, and the documented way to reach it is an initializer -- which runs
  # once per process, so once per web worker, background worker, console and rake task. Issuing
  # `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
  # current collation is read first and the statement skipped when it already matches, which
  # turns the common case into one cheap catalogue read.
  #
  # @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
  # @return [void]
  #
  def self.apply_binary_collation(bincoll)
    return unless Utils.using_mysql?

    collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

    # Nothing to apply to yet -- this runs during the first migration, before
    # the table exists.
    return unless Utils.connection.table_exists?(Tag.table_name)
    return if current_tag_name_collation == collation

    ActiveRecord::Migration.execute(
      "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
    )
  end

  ##
  # The collation `tags.name` currently carries, or `nil` where it cannot be read.
  #
  # @return [String, NilClass]
  #
  def self.current_tag_name_collation
    Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
  end
end

#tags_counterTrueClass, FalseClass

Whether taggings maintain a counter cache on their tag.

Returns:

  • (TrueClass, FalseClass)

    defaults to true



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/make_taggable.rb', line 177

class Configuration
  attr_accessor :force_lowercase, :force_parameterize,
    :remove_unused_tags, :default_parser,
    :tags_counter, :tags_table,
    :taggings_table
  attr_reader :delimiter, :strict_case_match, :tag_class

  ##
  # Builds the configuration with the library's defaults.
  #
  # @return [MakeTaggable::Configuration]
  #
  def initialize
    @delimiter = ","
    @force_lowercase = false
    @force_parameterize = false
    @strict_case_match = false
    @remove_unused_tags = false
    @tags_counter = true
    @default_parser = DefaultParser
    @force_binary_collation = false
    @tags_table = :tags
    @taggings_table = :taggings
    @tag_class = "MakeTaggable::Tag"
  end

  ##
  # Sets the class tags are read, written and returned as.
  #
  # Must be a String. A model constant cannot be referenced while initializers run -- Zeitwerk
  # has not defined it yet, and eager loading in production would try to resolve it before the
  # class exists.
  #
  # @param class_name [String] the name of a class inheriting from {MakeTaggable::Tag}
  # @return [String]
  # @raise [ArgumentError] when given anything but a String
  #
  def tag_class=(class_name)
    unless class_name.is_a?(String)
      raise ArgumentError,
        "tag_class must be a String, got #{class_name.inspect}. " \
        "Naming the class rather than the constant is what lets it be set in an initializer, " \
        "before Zeitwerk has defined it."
    end

    @tag_class = class_name
  end

  ##
  # Sets whether tag lookups are case sensitive.
  #
  # Ignored while {#force_binary_collation=} is on, which already implies case sensitivity.
  #
  # @param force_cs [TrueClass, FalseClass] whether to match case exactly
  # @return [TrueClass, FalseClass]
  #
  def strict_case_match=(force_cs)
    @strict_case_match = force_cs unless @force_binary_collation
  end

  ##
  # Sets the delimiter separating tags in a string.
  #
  # @deprecated Configure a {#default_parser} instead. This will be removed in a future release.
  #
  # @param string [String, Array<String>] one delimiter, or several
  # @return [String, Array<String>]
  #
  # @example Several delimiters, matched literally
  #   MakeTaggable.delimiter = [",", "|"]
  #
  def delimiter=(string)
    # Active Record does not always have a logger -- a plain Active Record
    # process, or an initializer running before the logger is assigned, will
    # both leave it nil.
    ActiveRecord::Base.logger&.warn(
      "MakeTaggable.delimiter is deprecated and will be removed in a future " \
      "release. Configure a MakeTaggable.default_parser instead."
    )
    @delimiter = string
  end

  ##
  # Switches the MySQL tag name column between a binary and a case-insensitive collation.
  #
  # A binary collation makes tag names compare exactly, including accented and other multi-byte
  # characters, and forces {#strict_case_match} on. Does nothing on other adapters.
  #
  # @param force_bin [TrueClass, FalseClass] whether to apply the binary collation
  # @return [TrueClass, FalseClass]
  #
  def force_binary_collation=(force_bin)
    if Utils.using_mysql?
      if force_bin
        Configuration.apply_binary_collation(true)
        @force_binary_collation = true
        @strict_case_match = true
      else
        Configuration.apply_binary_collation(false)
        @force_binary_collation = false
      end
    end
  end

  ##
  # Alters the MySQL tag name column to the requested collation.
  #
  # Failures are reported rather than raised: the column does not exist yet the first time the
  # migrations run, and that is not an error.
  #
  # @param bincoll [TrueClass, FalseClass] `true` for `utf8mb4_bin`, `false` for
  #   `utf8mb4_general_ci`
  # @return [NilClass]
  #
  ##
  # Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
  #
  # This is a schema change, and the documented way to reach it is an initializer -- which runs
  # once per process, so once per web worker, background worker, console and rake task. Issuing
  # `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
  # current collation is read first and the statement skipped when it already matches, which
  # turns the common case into one cheap catalogue read.
  #
  # @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
  # @return [void]
  #
  def self.apply_binary_collation(bincoll)
    return unless Utils.using_mysql?

    collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

    # Nothing to apply to yet -- this runs during the first migration, before
    # the table exists.
    return unless Utils.connection.table_exists?(Tag.table_name)
    return if current_tag_name_collation == collation

    ActiveRecord::Migration.execute(
      "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
    )
  end

  ##
  # The collation `tags.name` currently carries, or `nil` where it cannot be read.
  #
  # @return [String, NilClass]
  #
  def self.current_tag_name_collation
    Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
  end
end

#tags_tableSymbol, String

The table backing Tag.

Returns:

  • (Symbol, String)

    defaults to :tags



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# File 'lib/make_taggable.rb', line 177

class Configuration
  attr_accessor :force_lowercase, :force_parameterize,
    :remove_unused_tags, :default_parser,
    :tags_counter, :tags_table,
    :taggings_table
  attr_reader :delimiter, :strict_case_match, :tag_class

  ##
  # Builds the configuration with the library's defaults.
  #
  # @return [MakeTaggable::Configuration]
  #
  def initialize
    @delimiter = ","
    @force_lowercase = false
    @force_parameterize = false
    @strict_case_match = false
    @remove_unused_tags = false
    @tags_counter = true
    @default_parser = DefaultParser
    @force_binary_collation = false
    @tags_table = :tags
    @taggings_table = :taggings
    @tag_class = "MakeTaggable::Tag"
  end

  ##
  # Sets the class tags are read, written and returned as.
  #
  # Must be a String. A model constant cannot be referenced while initializers run -- Zeitwerk
  # has not defined it yet, and eager loading in production would try to resolve it before the
  # class exists.
  #
  # @param class_name [String] the name of a class inheriting from {MakeTaggable::Tag}
  # @return [String]
  # @raise [ArgumentError] when given anything but a String
  #
  def tag_class=(class_name)
    unless class_name.is_a?(String)
      raise ArgumentError,
        "tag_class must be a String, got #{class_name.inspect}. " \
        "Naming the class rather than the constant is what lets it be set in an initializer, " \
        "before Zeitwerk has defined it."
    end

    @tag_class = class_name
  end

  ##
  # Sets whether tag lookups are case sensitive.
  #
  # Ignored while {#force_binary_collation=} is on, which already implies case sensitivity.
  #
  # @param force_cs [TrueClass, FalseClass] whether to match case exactly
  # @return [TrueClass, FalseClass]
  #
  def strict_case_match=(force_cs)
    @strict_case_match = force_cs unless @force_binary_collation
  end

  ##
  # Sets the delimiter separating tags in a string.
  #
  # @deprecated Configure a {#default_parser} instead. This will be removed in a future release.
  #
  # @param string [String, Array<String>] one delimiter, or several
  # @return [String, Array<String>]
  #
  # @example Several delimiters, matched literally
  #   MakeTaggable.delimiter = [",", "|"]
  #
  def delimiter=(string)
    # Active Record does not always have a logger -- a plain Active Record
    # process, or an initializer running before the logger is assigned, will
    # both leave it nil.
    ActiveRecord::Base.logger&.warn(
      "MakeTaggable.delimiter is deprecated and will be removed in a future " \
      "release. Configure a MakeTaggable.default_parser instead."
    )
    @delimiter = string
  end

  ##
  # Switches the MySQL tag name column between a binary and a case-insensitive collation.
  #
  # A binary collation makes tag names compare exactly, including accented and other multi-byte
  # characters, and forces {#strict_case_match} on. Does nothing on other adapters.
  #
  # @param force_bin [TrueClass, FalseClass] whether to apply the binary collation
  # @return [TrueClass, FalseClass]
  #
  def force_binary_collation=(force_bin)
    if Utils.using_mysql?
      if force_bin
        Configuration.apply_binary_collation(true)
        @force_binary_collation = true
        @strict_case_match = true
      else
        Configuration.apply_binary_collation(false)
        @force_binary_collation = false
      end
    end
  end

  ##
  # Alters the MySQL tag name column to the requested collation.
  #
  # Failures are reported rather than raised: the column does not exist yet the first time the
  # migrations run, and that is not an error.
  #
  # @param bincoll [TrueClass, FalseClass] `true` for `utf8mb4_bin`, `false` for
  #   `utf8mb4_general_ci`
  # @return [NilClass]
  #
  ##
  # Applies the collation `tags.name` should carry on MySQL, if it does not carry it already.
  #
  # This is a schema change, and the documented way to reach it is an initializer -- which runs
  # once per process, so once per web worker, background worker, console and rake task. Issuing
  # `ALTER TABLE` from each of those takes a metadata lock on the tags table every time. So the
  # current collation is read first and the statement skipped when it already matches, which
  # turns the common case into one cheap catalogue read.
  #
  # @param bincoll [TrueClass, FalseClass] whether to apply the binary collation
  # @return [void]
  #
  def self.apply_binary_collation(bincoll)
    return unless Utils.using_mysql?

    collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

    # Nothing to apply to yet -- this runs during the first migration, before
    # the table exists.
    return unless Utils.connection.table_exists?(Tag.table_name)
    return if current_tag_name_collation == collation

    ActiveRecord::Migration.execute(
      "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
    )
  end

  ##
  # The collation `tags.name` currently carries, or `nil` where it cannot be read.
  #
  # @return [String, NilClass]
  #
  def self.current_tag_name_collation
    Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
  end
end

Class Method Details

.apply_binary_collation(bincoll) ⇒ NilClass, void

Alters the MySQL tag name column to the requested collation.

Failures are reported rather than raised: the column does not exist yet the first time the migrations run, and that is not an error.

Applies the collation tags.name should carry on MySQL, if it does not carry it already.

This is a schema change, and the documented way to reach it is an initializer -- which runs once per process, so once per web worker, background worker, console and rake task. Issuing ALTER TABLE from each of those takes a metadata lock on the tags table every time. So the current collation is read first and the statement skipped when it already matches, which turns the common case into one cheap catalogue read.

Parameters:

  • bincoll (TrueClass, FalseClass)

    true for utf8mb4_bin, false for utf8mb4_general_ci

  • bincoll (TrueClass, FalseClass)

    whether to apply the binary collation

Returns:

  • (NilClass)
  • (void)


303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/make_taggable.rb', line 303

def self.apply_binary_collation(bincoll)
  return unless Utils.using_mysql?

  collation = bincoll ? "utf8mb4_bin" : "utf8mb4_general_ci"

  # Nothing to apply to yet -- this runs during the first migration, before
  # the table exists.
  return unless Utils.connection.table_exists?(Tag.table_name)
  return if current_tag_name_collation == collation

  ActiveRecord::Migration.execute(
    "ALTER TABLE #{Tag.table_name} MODIFY name varchar(255) CHARACTER SET utf8mb4 COLLATE #{collation};"
  )
end

.current_tag_name_collationString, NilClass

The collation tags.name currently carries, or nil where it cannot be read.

Returns:

  • (String, NilClass)


323
324
325
# File 'lib/make_taggable.rb', line 323

def self.current_tag_name_collation
  Utils.connection.columns(Tag.table_name).find { |column| column.name == "name" }&.collation
end

Instance Method Details

#force_binary_collation=(force_bin) ⇒ TrueClass, FalseClass

Switches the MySQL tag name column between a binary and a case-insensitive collation.

A binary collation makes tag names compare exactly, including accented and other multi-byte characters, and forces #strict_case_match on. Does nothing on other adapters.

Parameters:

  • force_bin (TrueClass, FalseClass)

    whether to apply the binary collation

Returns:

  • (TrueClass, FalseClass)


268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/make_taggable.rb', line 268

def force_binary_collation=(force_bin)
  if Utils.using_mysql?
    if force_bin
      Configuration.apply_binary_collation(true)
      @force_binary_collation = true
      @strict_case_match = true
    else
      Configuration.apply_binary_collation(false)
      @force_binary_collation = false
    end
  end
end