friendly_id 5.4.2 → 5.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/test.yml +34 -25
  5. data/Changelog.md +6 -0
  6. data/Gemfile +9 -13
  7. data/README.md +21 -0
  8. data/Rakefile +24 -27
  9. data/bench.rb +30 -27
  10. data/certs/parndt.pem +25 -23
  11. data/friendly_id.gemspec +26 -29
  12. data/gemfiles/Gemfile.rails-5.2.rb +11 -16
  13. data/gemfiles/Gemfile.rails-6.0.rb +11 -16
  14. data/gemfiles/Gemfile.rails-6.1.rb +22 -0
  15. data/gemfiles/Gemfile.rails-7.0.rb +22 -0
  16. data/guide.rb +5 -5
  17. data/lib/friendly_id/base.rb +57 -60
  18. data/lib/friendly_id/candidates.rb +9 -11
  19. data/lib/friendly_id/configuration.rb +6 -7
  20. data/lib/friendly_id/finder_methods.rb +26 -11
  21. data/lib/friendly_id/finders.rb +63 -66
  22. data/lib/friendly_id/history.rb +59 -63
  23. data/lib/friendly_id/initializer.rb +4 -4
  24. data/lib/friendly_id/migration.rb +6 -6
  25. data/lib/friendly_id/object_utils.rb +2 -2
  26. data/lib/friendly_id/reserved.rb +28 -32
  27. data/lib/friendly_id/scoped.rb +97 -102
  28. data/lib/friendly_id/sequentially_slugged/calculator.rb +69 -0
  29. data/lib/friendly_id/sequentially_slugged.rb +17 -64
  30. data/lib/friendly_id/simple_i18n.rb +75 -69
  31. data/lib/friendly_id/slug.rb +1 -2
  32. data/lib/friendly_id/slug_generator.rb +1 -3
  33. data/lib/friendly_id/slugged.rb +234 -238
  34. data/lib/friendly_id/version.rb +1 -1
  35. data/lib/friendly_id.rb +41 -45
  36. data/lib/generators/friendly_id_generator.rb +9 -9
  37. data/test/base_test.rb +10 -13
  38. data/test/benchmarks/finders.rb +28 -26
  39. data/test/benchmarks/object_utils.rb +13 -13
  40. data/test/candidates_test.rb +17 -18
  41. data/test/configuration_test.rb +7 -11
  42. data/test/core_test.rb +1 -2
  43. data/test/databases.yml +4 -3
  44. data/test/finders_test.rb +52 -5
  45. data/test/generator_test.rb +16 -26
  46. data/test/helper.rb +29 -22
  47. data/test/history_test.rb +70 -74
  48. data/test/numeric_slug_test.rb +4 -4
  49. data/test/object_utils_test.rb +0 -2
  50. data/test/reserved_test.rb +9 -11
  51. data/test/schema.rb +5 -4
  52. data/test/scoped_test.rb +18 -20
  53. data/test/sequentially_slugged_test.rb +65 -50
  54. data/test/shared.rb +15 -16
  55. data/test/simple_i18n_test.rb +22 -12
  56. data/test/slugged_test.rb +102 -121
  57. data/test/sti_test.rb +19 -21
  58. data.tar.gz.sig +0 -0
  59. metadata +35 -30
  60. metadata.gz.sig +1 -1
@@ -1,59 +1,55 @@
1
1
  module FriendlyId
2
-
3
- =begin
4
-
5
- ## History: Avoiding 404's When Slugs Change
6
-
7
- FriendlyId's {FriendlyId::History History} module adds the ability to store a
8
- log of a model's slugs, so that when its friendly id changes, it's still
9
- possible to perform finds by the old id.
10
-
11
- The primary use case for this is avoiding broken URLs.
12
-
13
- ### Setup
14
-
15
- In order to use this module, you must add a table to your database schema to
16
- store the slug records. FriendlyId provides a generator for this purpose:
17
-
18
- rails generate friendly_id
19
- rake db:migrate
20
-
21
- This will add a table named `friendly_id_slugs`, used by the {FriendlyId::Slug}
22
- model.
23
-
24
- ### Considerations
25
-
26
- Because recording slug history requires creating additional database records,
27
- this module has an impact on the performance of the associated model's `create`
28
- method.
29
-
30
- ### Example
31
-
32
- class Post < ActiveRecord::Base
33
- extend FriendlyId
34
- friendly_id :title, :use => :history
35
- end
36
-
37
- class PostsController < ApplicationController
38
-
39
- before_filter :find_post
40
-
41
- ...
42
-
43
- def find_post
44
- @post = Post.friendly.find params[:id]
45
-
46
- # If an old id or a numeric id was used to find the record, then
47
- # the request slug will not match the current slug, and we should do
48
- # a 301 redirect to the new path
49
- if params[:id] != @post.slug
50
- return redirect_to @post, :status => :moved_permanently
51
- end
52
- end
53
- end
54
- =end
2
+ #
3
+ ## History: Avoiding 404's When Slugs Change
4
+ #
5
+ # FriendlyId's {FriendlyId::History History} module adds the ability to store a
6
+ # log of a model's slugs, so that when its friendly id changes, it's still
7
+ # possible to perform finds by the old id.
8
+ #
9
+ # The primary use case for this is avoiding broken URLs.
10
+ #
11
+ ### Setup
12
+ #
13
+ # In order to use this module, you must add a table to your database schema to
14
+ # store the slug records. FriendlyId provides a generator for this purpose:
15
+ #
16
+ # rails generate friendly_id
17
+ # rake db:migrate
18
+ #
19
+ # This will add a table named `friendly_id_slugs`, used by the {FriendlyId::Slug}
20
+ # model.
21
+ #
22
+ ### Considerations
23
+ #
24
+ # Because recording slug history requires creating additional database records,
25
+ # this module has an impact on the performance of the associated model's `create`
26
+ # method.
27
+ #
28
+ ### Example
29
+ #
30
+ # class Post < ActiveRecord::Base
31
+ # extend FriendlyId
32
+ # friendly_id :title, :use => :history
33
+ # end
34
+ #
35
+ # class PostsController < ApplicationController
36
+ #
37
+ # before_filter :find_post
38
+ #
39
+ # ...
40
+ #
41
+ # def find_post
42
+ # @post = Post.friendly.find params[:id]
43
+ #
44
+ # # If an old id or a numeric id was used to find the record, then
45
+ # # the request slug will not match the current slug, and we should do
46
+ # # a 301 redirect to the new path
47
+ # if params[:id] != @post.slug
48
+ # return redirect_to @post, :status => :moved_permanently
49
+ # end
50
+ # end
51
+ # end
55
52
  module History
56
-
57
53
  module Configuration
58
54
  def dependent_value
59
55
  dependent.nil? ? :destroy : dependent
@@ -72,10 +68,10 @@ method.
72
68
  # Configures the model instance to use the History add-on.
73
69
  def self.included(model_class)
74
70
  model_class.class_eval do
75
- has_many :slugs, -> {order(id: :desc)}, **{
76
- :as => :sluggable,
77
- :dependent => @friendly_id_config.dependent_value,
78
- :class_name => Slug.to_s
71
+ has_many :slugs, -> { order(id: :desc) }, **{
72
+ as: :sluggable,
73
+ dependent: @friendly_id_config.dependent_value,
74
+ class_name: Slug.to_s
79
75
  }
80
76
 
81
77
  after_save :create_slug
@@ -96,7 +92,7 @@ method.
96
92
  end
97
93
 
98
94
  def slug_table_record(id)
99
- select(quoted_table_name + '.*').joins(:slugs).where(slug_history_clause(id)).order(Slug.arel_table[:id].desc).first
95
+ select(quoted_table_name + ".*").joins(:slugs).where(slug_history_clause(id)).order(Slug.arel_table[:id].desc).first
100
96
  end
101
97
 
102
98
  def slug_history_clause(id)
@@ -112,7 +108,7 @@ method.
112
108
  def scope_for_slug_generator
113
109
  relation = super.joins(:slugs)
114
110
  unless new_record?
115
- relation = relation.merge(Slug.where('sluggable_id <> ?', id))
111
+ relation = relation.merge(Slug.where("sluggable_id <> ?", id))
116
112
  end
117
113
  if friendly_id_config.uses?(:scoped)
118
114
  relation = relation.where(Slug.arel_table[:scope].eq(serialized_scope))
@@ -124,9 +120,9 @@ method.
124
120
  return unless friendly_id
125
121
  return if history_is_up_to_date?
126
122
  # Allow reversion back to a previously used slug
127
- relation = slugs.where(:slug => friendly_id)
123
+ relation = slugs.where(slug: friendly_id)
128
124
  if friendly_id_config.uses?(:scoped)
129
- relation = relation.where(:scope => serialized_scope)
125
+ relation = relation.where(scope: serialized_scope)
130
126
  end
131
127
  relation.destroy_all unless relation.empty?
132
128
  slugs.create! do |record|
@@ -139,7 +135,7 @@ method.
139
135
  latest_history = slugs.first
140
136
  check = latest_history.try(:slug) == friendly_id
141
137
  if friendly_id_config.uses?(:scoped)
142
- check = check && latest_history.scope == serialized_scope
138
+ check &&= latest_history.scope == serialized_scope
143
139
  end
144
140
  check
145
141
  end
@@ -16,9 +16,9 @@ FriendlyId.defaults do |config|
16
16
  # undesirable to allow as slugs. Edit this list as needed for your app.
17
17
  config.use :reserved
18
18
 
19
- config.reserved_words = %w(new edit index session login logout users admin
20
- stylesheets assets javascripts images)
21
-
19
+ config.reserved_words = %w[new edit index session login logout users admin
20
+ stylesheets assets javascripts images]
21
+
22
22
  # This adds an option to treat reserved words as conflicts rather than exceptions.
23
23
  # When there is no good candidate, a UUID will be appended, matching the existing
24
24
  # conflict behavior.
@@ -37,7 +37,7 @@ FriendlyId.defaults do |config|
37
37
  # MyModel.find('foo')
38
38
  #
39
39
  # This is significantly more convenient but may not be appropriate for
40
- # all applications, so you must explicity opt-in to this behavior. You can
40
+ # all applications, so you must explicitly opt-in to this behavior. You can
41
41
  # always also configure it on a per-model basis if you prefer.
42
42
  #
43
43
  # Something else to consider is that using the :finders addon boosts
@@ -8,14 +8,14 @@ MIGRATION_CLASS =
8
8
  class CreateFriendlyIdSlugs < MIGRATION_CLASS
9
9
  def change
10
10
  create_table :friendly_id_slugs do |t|
11
- t.string :slug, :null => false
12
- t.integer :sluggable_id, :null => false
13
- t.string :sluggable_type, :limit => 50
14
- t.string :scope
11
+ t.string :slug, null: false
12
+ t.integer :sluggable_id, null: false
13
+ t.string :sluggable_type, limit: 50
14
+ t.string :scope
15
15
  t.datetime :created_at
16
16
  end
17
17
  add_index :friendly_id_slugs, [:sluggable_type, :sluggable_id]
18
- add_index :friendly_id_slugs, [:slug, :sluggable_type], length: { slug: 140, sluggable_type: 50 }
19
- add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: { slug: 70, sluggable_type: 50, scope: 70 }, unique: true
18
+ add_index :friendly_id_slugs, [:slug, :sluggable_type], length: {slug: 140, sluggable_type: 50}
19
+ add_index :friendly_id_slugs, [:slug, :sluggable_type, :scope], length: {slug: 70, sluggable_type: 50, scope: 70}, unique: true
20
20
  end
21
21
  end
@@ -19,7 +19,6 @@ module FriendlyId
19
19
  # names that unambigously refer to the library of their origin, which should
20
20
  # be sufficient to avoid conflicts with other libraries.
21
21
  module ObjectUtils
22
-
23
22
  # True if the id is definitely friendly, false if definitely unfriendly,
24
23
  # else nil.
25
24
  #
@@ -45,7 +44,8 @@ module FriendlyId
45
44
  # True if the id is definitely unfriendly, false if definitely friendly,
46
45
  # else nil.
47
46
  def unfriendly_id?
48
- val = friendly_id? ; !val unless val.nil?
47
+ val = friendly_id?
48
+ !val unless val.nil?
49
49
  end
50
50
  end
51
51
 
@@ -1,42 +1,38 @@
1
1
  module FriendlyId
2
-
3
- =begin
4
-
5
- ## Reserved Words
6
-
7
- The {FriendlyId::Reserved Reserved} module adds the ability to exclude a list of
8
- words from use as FriendlyId slugs.
9
-
10
- With Ruby on Rails, FriendlyId's generator generates an initializer that
11
- reserves some words such as "new" and "edit" using {FriendlyId.defaults
12
- FriendlyId.defaults}.
13
-
14
- Note that the error messages for fields will appear on the field
15
- `:friendly_id`. If you are using Rails's scaffolded form errors display, then
16
- it will have no field to highlight. If you'd like to change this so that
17
- scaffolding works as expected, one way to accomplish this is to move the error
18
- message to a different field. For example:
19
-
20
- class Person < ActiveRecord::Base
21
- extend FriendlyId
22
- friendly_id :name, use: :slugged
23
-
24
- after_validation :move_friendly_id_error_to_name
25
-
26
- def move_friendly_id_error_to_name
27
- errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
28
- end
29
- end
30
-
31
- =end
2
+ #
3
+ ## Reserved Words
4
+ #
5
+ # The {FriendlyId::Reserved Reserved} module adds the ability to exclude a list of
6
+ # words from use as FriendlyId slugs.
7
+ #
8
+ # With Ruby on Rails, FriendlyId's generator generates an initializer that
9
+ # reserves some words such as "new" and "edit" using {FriendlyId.defaults
10
+ # FriendlyId.defaults}.
11
+ #
12
+ # Note that the error messages for fields will appear on the field
13
+ # `:friendly_id`. If you are using Rails's scaffolded form errors display, then
14
+ # it will have no field to highlight. If you'd like to change this so that
15
+ # scaffolding works as expected, one way to accomplish this is to move the error
16
+ # message to a different field. For example:
17
+ #
18
+ # class Person < ActiveRecord::Base
19
+ # extend FriendlyId
20
+ # friendly_id :name, use: :slugged
21
+ #
22
+ # after_validation :move_friendly_id_error_to_name
23
+ #
24
+ # def move_friendly_id_error_to_name
25
+ # errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
26
+ # end
27
+ # end
28
+ #
32
29
  module Reserved
33
-
34
30
  # When included, this module adds configuration options to the model class's
35
31
  # friendly_id_config.
36
32
  def self.included(model_class)
37
33
  model_class.class_eval do
38
34
  friendly_id_config.class.send :include, Reserved::Configuration
39
- validates_exclusion_of :friendly_id, :in => ->(_) {
35
+ validates_exclusion_of :friendly_id, in: ->(_) {
40
36
  friendly_id_config.reserved_words || []
41
37
  }
42
38
  end
@@ -1,108 +1,104 @@
1
1
  require "friendly_id/slugged"
2
2
 
3
3
  module FriendlyId
4
-
5
- =begin
6
-
7
- ## Unique Slugs by Scope
8
-
9
- The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
10
- within a scope.
11
-
12
- This allows, for example, two restaurants in different cities to have the slug
13
- `joes-diner`:
14
-
15
- class Restaurant < ActiveRecord::Base
16
- extend FriendlyId
17
- belongs_to :city
18
- friendly_id :name, :use => :scoped, :scope => :city
19
- end
20
-
21
- class City < ActiveRecord::Base
22
- extend FriendlyId
23
- has_many :restaurants
24
- friendly_id :name, :use => :slugged
25
- end
26
-
27
- City.friendly.find("seattle").restaurants.friendly.find("joes-diner")
28
- City.friendly.find("chicago").restaurants.friendly.find("joes-diner")
29
-
30
- Without :scoped in this case, one of the restaurants would have the slug
31
- `joes-diner` and the other would have `joes-diner-f9f3789a-daec-4156-af1d-fab81aa16ee5`.
32
-
33
- The value for the `:scope` option can be the name of a `belongs_to` relation, or
34
- a column.
35
-
36
- Additionally, the `:scope` option can receive an array of scope values:
37
-
38
- class Cuisine < ActiveRecord::Base
39
- extend FriendlyId
40
- has_many :restaurants
41
- friendly_id :name, :use => :slugged
42
- end
43
-
44
- class City < ActiveRecord::Base
45
- extend FriendlyId
46
- has_many :restaurants
47
- friendly_id :name, :use => :slugged
48
- end
49
-
50
- class Restaurant < ActiveRecord::Base
51
- extend FriendlyId
52
- belongs_to :city
53
- friendly_id :name, :use => :scoped, :scope => [:city, :cuisine]
54
- end
55
-
56
- All supplied values will be used to determine scope.
57
-
58
- ### Finding Records by Friendly ID
59
-
60
- If you are using scopes your friendly ids may not be unique, so a simple find
61
- like:
62
-
63
- Restaurant.friendly.find("joes-diner")
64
-
65
- may return the wrong record. In these cases it's best to query through the
66
- relation:
67
-
68
- @city.restaurants.friendly.find("joes-diner")
69
-
70
- Alternatively, you could pass the scope value as a query parameter:
71
-
72
- Restaurant.where(:city_id => @city.id).friendly.find("joes-diner")
73
-
74
-
75
- ### Finding All Records That Match a Scoped ID
76
-
77
- Query the slug column directly:
78
-
79
- Restaurant.where(:slug => "joes-diner")
80
-
81
- ### Routes for Scoped Models
82
-
83
- Recall that FriendlyId is a database-centric library, and does not set up any
84
- routes for scoped models. You must do this yourself in your application. Here's
85
- an example of one way to set this up:
86
-
87
- # in routes.rb
88
- resources :cities do
89
- resources :restaurants
90
- end
91
-
92
- # in views
93
- <%= link_to 'Show', [@city, @restaurant] %>
94
-
95
- # in controllers
96
- @city = City.friendly.find(params[:city_id])
97
- @restaurant = @city.restaurants.friendly.find(params[:id])
98
-
99
- # URLs:
100
- http://example.org/cities/seattle/restaurants/joes-diner
101
- http://example.org/cities/chicago/restaurants/joes-diner
102
-
103
- =end
4
+ #
5
+ ## Unique Slugs by Scope
6
+ #
7
+ # The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
8
+ # within a scope.
9
+ #
10
+ # This allows, for example, two restaurants in different cities to have the slug
11
+ # `joes-diner`:
12
+ #
13
+ # class Restaurant < ActiveRecord::Base
14
+ # extend FriendlyId
15
+ # belongs_to :city
16
+ # friendly_id :name, :use => :scoped, :scope => :city
17
+ # end
18
+ #
19
+ # class City < ActiveRecord::Base
20
+ # extend FriendlyId
21
+ # has_many :restaurants
22
+ # friendly_id :name, :use => :slugged
23
+ # end
24
+ #
25
+ # City.friendly.find("seattle").restaurants.friendly.find("joes-diner")
26
+ # City.friendly.find("chicago").restaurants.friendly.find("joes-diner")
27
+ #
28
+ # Without :scoped in this case, one of the restaurants would have the slug
29
+ # `joes-diner` and the other would have `joes-diner-f9f3789a-daec-4156-af1d-fab81aa16ee5`.
30
+ #
31
+ # The value for the `:scope` option can be the name of a `belongs_to` relation, or
32
+ # a column.
33
+ #
34
+ # Additionally, the `:scope` option can receive an array of scope values:
35
+ #
36
+ # class Cuisine < ActiveRecord::Base
37
+ # extend FriendlyId
38
+ # has_many :restaurants
39
+ # friendly_id :name, :use => :slugged
40
+ # end
41
+ #
42
+ # class City < ActiveRecord::Base
43
+ # extend FriendlyId
44
+ # has_many :restaurants
45
+ # friendly_id :name, :use => :slugged
46
+ # end
47
+ #
48
+ # class Restaurant < ActiveRecord::Base
49
+ # extend FriendlyId
50
+ # belongs_to :city
51
+ # friendly_id :name, :use => :scoped, :scope => [:city, :cuisine]
52
+ # end
53
+ #
54
+ # All supplied values will be used to determine scope.
55
+ #
56
+ ### Finding Records by Friendly ID
57
+ #
58
+ # If you are using scopes your friendly ids may not be unique, so a simple find
59
+ # like:
60
+ #
61
+ # Restaurant.friendly.find("joes-diner")
62
+ #
63
+ # may return the wrong record. In these cases it's best to query through the
64
+ # relation:
65
+ #
66
+ # @city.restaurants.friendly.find("joes-diner")
67
+ #
68
+ # Alternatively, you could pass the scope value as a query parameter:
69
+ #
70
+ # Restaurant.where(:city_id => @city.id).friendly.find("joes-diner")
71
+ #
72
+ #
73
+ ### Finding All Records That Match a Scoped ID
74
+ #
75
+ # Query the slug column directly:
76
+ #
77
+ # Restaurant.where(:slug => "joes-diner")
78
+ #
79
+ ### Routes for Scoped Models
80
+ #
81
+ # Recall that FriendlyId is a database-centric library, and does not set up any
82
+ # routes for scoped models. You must do this yourself in your application. Here's
83
+ # an example of one way to set this up:
84
+ #
85
+ # # in routes.rb
86
+ # resources :cities do
87
+ # resources :restaurants
88
+ # end
89
+ #
90
+ # # in views
91
+ # <%= link_to 'Show', [@city, @restaurant] %>
92
+ #
93
+ # # in controllers
94
+ # @city = City.friendly.find(params[:city_id])
95
+ # @restaurant = @city.restaurants.friendly.find(params[:id])
96
+ #
97
+ # # URLs:
98
+ # http://example.org/cities/seattle/restaurants/joes-diner
99
+ # http://example.org/cities/chicago/restaurants/joes-diner
100
+ #
104
101
  module Scoped
105
-
106
102
  # FriendlyId::Config.use will invoke this method when present, to allow
107
103
  # loading dependent modules prior to overriding them when necessary.
108
104
  def self.setup(model_class)
@@ -146,7 +142,6 @@ an example of one way to set this up:
146
142
  # This module adds the `:scope` configuration option to
147
143
  # {FriendlyId::Configuration FriendlyId::Configuration}.
148
144
  module Configuration
149
-
150
145
  # Gets the scope value.
151
146
  #
152
147
  # When setting this value, the argument should be a symbol referencing a
@@ -0,0 +1,69 @@
1
+ module FriendlyId
2
+ module SequentiallySlugged
3
+ class Calculator
4
+ attr_accessor :scope, :slug, :slug_column, :sequence_separator
5
+
6
+ def initialize(scope, slug, slug_column, sequence_separator, base_class)
7
+ @scope = scope
8
+ @slug = slug
9
+ table_name = scope.connection.quote_table_name(base_class.arel_table.name)
10
+ @slug_column = "#{table_name}.#{scope.connection.quote_column_name(slug_column)}"
11
+ @sequence_separator = sequence_separator
12
+ end
13
+
14
+ def next_slug
15
+ slug + sequence_separator + next_sequence_number.to_s
16
+ end
17
+
18
+ private
19
+
20
+ def conflict_query
21
+ base = "#{slug_column} = ? OR #{slug_column} LIKE ?"
22
+ # Awful hack for SQLite3, which does not pick up '\' as the escape character
23
+ # without this.
24
+ base << " ESCAPE '\\'" if /sqlite/i.match?(scope.connection.adapter_name)
25
+ base
26
+ end
27
+
28
+ def next_sequence_number
29
+ last_sequence_number ? last_sequence_number + 1 : 2
30
+ end
31
+
32
+ def last_sequence_number
33
+ # Reject slug_conflicts that doesn't come from the first_candidate
34
+ # Map all sequence numbers and take the maximum
35
+ slug_conflicts
36
+ .reject { |slug_conflict| !regexp.match(slug_conflict) }
37
+ .map { |slug_conflict| regexp.match(slug_conflict)[1].to_i }
38
+ .max
39
+ end
40
+
41
+ # Return the unnumbered (shortest) slug first, followed by the numbered ones
42
+ # in ascending order.
43
+ def ordering_query
44
+ "#{sql_length}(#{slug_column}) ASC, #{slug_column} ASC"
45
+ end
46
+
47
+ def regexp
48
+ /#{slug}#{sequence_separator}(\d+)\z/
49
+ end
50
+
51
+ def sequential_slug_matcher
52
+ # Underscores (matching a single character) and percent signs (matching
53
+ # any number of characters) need to be escaped. While this looks like
54
+ # an excessive number of backslashes, it is correct.
55
+ "#{slug}#{sequence_separator}".gsub(/[_%]/, '\\\\\&') + "%"
56
+ end
57
+
58
+ def slug_conflicts
59
+ scope
60
+ .where(conflict_query, slug, sequential_slug_matcher)
61
+ .order(Arel.sql(ordering_query)).pluck(Arel.sql(slug_column))
62
+ end
63
+
64
+ def sql_length
65
+ /sqlserver/i.match?(scope.connection.adapter_name) ? "LEN" : "LENGTH"
66
+ end
67
+ end
68
+ end
69
+ end