friendly_id 5.4.2 → 5.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.
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