friendly_id 5.4.2 → 5.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) 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 +38 -25
  5. data/.yardopts +2 -0
  6. data/Changelog.md +10 -0
  7. data/Gemfile +9 -13
  8. data/README.md +21 -0
  9. data/Rakefile +24 -27
  10. data/bench.rb +30 -27
  11. data/certs/parndt.pem +25 -23
  12. data/friendly_id.gemspec +26 -29
  13. data/gemfiles/Gemfile.rails-5.2.rb +11 -16
  14. data/gemfiles/Gemfile.rails-6.0.rb +11 -16
  15. data/gemfiles/Gemfile.rails-6.1.rb +22 -0
  16. data/gemfiles/Gemfile.rails-7.0.rb +22 -0
  17. data/guide.rb +13 -6
  18. data/lib/friendly_id/base.rb +59 -60
  19. data/lib/friendly_id/candidates.rb +9 -11
  20. data/lib/friendly_id/configuration.rb +6 -7
  21. data/lib/friendly_id/finder_methods.rb +26 -11
  22. data/lib/friendly_id/finders.rb +66 -66
  23. data/lib/friendly_id/history.rb +62 -63
  24. data/lib/friendly_id/initializer.rb +4 -4
  25. data/lib/friendly_id/migration.rb +6 -6
  26. data/lib/friendly_id/object_utils.rb +2 -2
  27. data/lib/friendly_id/reserved.rb +30 -32
  28. data/lib/friendly_id/scoped.rb +99 -102
  29. data/lib/friendly_id/sequentially_slugged/calculator.rb +69 -0
  30. data/lib/friendly_id/sequentially_slugged.rb +17 -64
  31. data/lib/friendly_id/simple_i18n.rb +78 -69
  32. data/lib/friendly_id/slug.rb +1 -2
  33. data/lib/friendly_id/slug_generator.rb +1 -3
  34. data/lib/friendly_id/slugged.rb +236 -238
  35. data/lib/friendly_id/version.rb +1 -1
  36. data/lib/friendly_id.rb +47 -49
  37. data/lib/generators/friendly_id_generator.rb +9 -9
  38. data/test/base_test.rb +10 -13
  39. data/test/benchmarks/finders.rb +28 -26
  40. data/test/benchmarks/object_utils.rb +13 -13
  41. data/test/candidates_test.rb +17 -18
  42. data/test/configuration_test.rb +7 -11
  43. data/test/core_test.rb +1 -2
  44. data/test/databases.yml +4 -3
  45. data/test/finders_test.rb +52 -5
  46. data/test/generator_test.rb +16 -26
  47. data/test/helper.rb +31 -24
  48. data/test/history_test.rb +70 -74
  49. data/test/numeric_slug_test.rb +4 -4
  50. data/test/object_utils_test.rb +0 -2
  51. data/test/reserved_test.rb +9 -11
  52. data/test/schema.rb +5 -4
  53. data/test/scoped_test.rb +18 -20
  54. data/test/sequentially_slugged_test.rb +65 -50
  55. data/test/shared.rb +15 -16
  56. data/test/simple_i18n_test.rb +22 -12
  57. data/test/slugged_test.rb +102 -121
  58. data/test/sti_test.rb +19 -21
  59. data.tar.gz.sig +0 -0
  60. metadata +37 -32
  61. metadata.gz.sig +0 -0
@@ -1,59 +1,58 @@
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
+ # @guide begin
3
+ #
4
+ # ## History: Avoiding 404's When Slugs Change
5
+ #
6
+ # FriendlyId's {FriendlyId::History History} module adds the ability to store a
7
+ # log of a model's slugs, so that when its friendly id changes, it's still
8
+ # possible to perform finds by the old id.
9
+ #
10
+ # The primary use case for this is avoiding broken URLs.
11
+ #
12
+ # ### Setup
13
+ #
14
+ # In order to use this module, you must add a table to your database schema to
15
+ # store the slug records. FriendlyId provides a generator for this purpose:
16
+ #
17
+ # rails generate friendly_id
18
+ # rake db:migrate
19
+ #
20
+ # This will add a table named `friendly_id_slugs`, used by the {FriendlyId::Slug}
21
+ # model.
22
+ #
23
+ # ### Considerations
24
+ #
25
+ # Because recording slug history requires creating additional database records,
26
+ # this module has an impact on the performance of the associated model's `create`
27
+ # method.
28
+ #
29
+ # ### Example
30
+ #
31
+ # class Post < ActiveRecord::Base
32
+ # extend FriendlyId
33
+ # friendly_id :title, :use => :history
34
+ # end
35
+ #
36
+ # class PostsController < ApplicationController
37
+ #
38
+ # before_filter :find_post
39
+ #
40
+ # ...
41
+ #
42
+ # def find_post
43
+ # @post = Post.friendly.find params[:id]
44
+ #
45
+ # # If an old id or a numeric id was used to find the record, then
46
+ # # the request slug will not match the current slug, and we should do
47
+ # # a 301 redirect to the new path
48
+ # if params[:id] != @post.slug
49
+ # return redirect_to @post, :status => :moved_permanently
50
+ # end
51
+ # end
52
+ # end
53
+ #
54
+ # @guide end
55
55
  module History
56
-
57
56
  module Configuration
58
57
  def dependent_value
59
58
  dependent.nil? ? :destroy : dependent
@@ -72,10 +71,10 @@ method.
72
71
  # Configures the model instance to use the History add-on.
73
72
  def self.included(model_class)
74
73
  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
74
+ has_many :slugs, -> { order(id: :desc) }, **{
75
+ as: :sluggable,
76
+ dependent: @friendly_id_config.dependent_value,
77
+ class_name: Slug.to_s
79
78
  }
80
79
 
81
80
  after_save :create_slug
@@ -96,7 +95,7 @@ method.
96
95
  end
97
96
 
98
97
  def slug_table_record(id)
99
- select(quoted_table_name + '.*').joins(:slugs).where(slug_history_clause(id)).order(Slug.arel_table[:id].desc).first
98
+ select(quoted_table_name + ".*").joins(:slugs).where(slug_history_clause(id)).order(Slug.arel_table[:id].desc).first
100
99
  end
101
100
 
102
101
  def slug_history_clause(id)
@@ -112,7 +111,7 @@ method.
112
111
  def scope_for_slug_generator
113
112
  relation = super.joins(:slugs)
114
113
  unless new_record?
115
- relation = relation.merge(Slug.where('sluggable_id <> ?', id))
114
+ relation = relation.merge(Slug.where("sluggable_id <> ?", id))
116
115
  end
117
116
  if friendly_id_config.uses?(:scoped)
118
117
  relation = relation.where(Slug.arel_table[:scope].eq(serialized_scope))
@@ -124,9 +123,9 @@ method.
124
123
  return unless friendly_id
125
124
  return if history_is_up_to_date?
126
125
  # Allow reversion back to a previously used slug
127
- relation = slugs.where(:slug => friendly_id)
126
+ relation = slugs.where(slug: friendly_id)
128
127
  if friendly_id_config.uses?(:scoped)
129
- relation = relation.where(:scope => serialized_scope)
128
+ relation = relation.where(scope: serialized_scope)
130
129
  end
131
130
  relation.destroy_all unless relation.empty?
132
131
  slugs.create! do |record|
@@ -139,7 +138,7 @@ method.
139
138
  latest_history = slugs.first
140
139
  check = latest_history.try(:slug) == friendly_id
141
140
  if friendly_id_config.uses?(:scoped)
142
- check = check && latest_history.scope == serialized_scope
141
+ check &&= latest_history.scope == serialized_scope
143
142
  end
144
143
  check
145
144
  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,40 @@
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
+ # @guide begin
3
+ #
4
+ # ## Reserved Words
5
+ #
6
+ # The {FriendlyId::Reserved Reserved} module adds the ability to exclude a list of
7
+ # words from use as FriendlyId slugs.
8
+ #
9
+ # With Ruby on Rails, FriendlyId's generator generates an initializer that
10
+ # reserves some words such as "new" and "edit" using {FriendlyId.defaults
11
+ # FriendlyId.defaults}.
12
+ #
13
+ # Note that the error messages for fields will appear on the field
14
+ # `:friendly_id`. If you are using Rails's scaffolded form errors display, then
15
+ # it will have no field to highlight. If you'd like to change this so that
16
+ # scaffolding works as expected, one way to accomplish this is to move the error
17
+ # message to a different field. For example:
18
+ #
19
+ # class Person < ActiveRecord::Base
20
+ # extend FriendlyId
21
+ # friendly_id :name, use: :slugged
22
+ #
23
+ # after_validation :move_friendly_id_error_to_name
24
+ #
25
+ # def move_friendly_id_error_to_name
26
+ # errors.add :name, *errors.delete(:friendly_id) if errors[:friendly_id].present?
27
+ # end
28
+ # end
29
+ #
30
+ # @guide end
32
31
  module Reserved
33
-
34
32
  # When included, this module adds configuration options to the model class's
35
33
  # friendly_id_config.
36
34
  def self.included(model_class)
37
35
  model_class.class_eval do
38
36
  friendly_id_config.class.send :include, Reserved::Configuration
39
- validates_exclusion_of :friendly_id, :in => ->(_) {
37
+ validates_exclusion_of :friendly_id, in: ->(_) {
40
38
  friendly_id_config.reserved_words || []
41
39
  }
42
40
  end
@@ -1,108 +1,106 @@
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
+ # @guide begin
5
+ #
6
+ # ## Unique Slugs by Scope
7
+ #
8
+ # The {FriendlyId::Scoped} module allows FriendlyId to generate unique slugs
9
+ # within a scope.
10
+ #
11
+ # This allows, for example, two restaurants in different cities to have the slug
12
+ # `joes-diner`:
13
+ #
14
+ # class Restaurant < ActiveRecord::Base
15
+ # extend FriendlyId
16
+ # belongs_to :city
17
+ # friendly_id :name, :use => :scoped, :scope => :city
18
+ # end
19
+ #
20
+ # class City < ActiveRecord::Base
21
+ # extend FriendlyId
22
+ # has_many :restaurants
23
+ # friendly_id :name, :use => :slugged
24
+ # end
25
+ #
26
+ # City.friendly.find("seattle").restaurants.friendly.find("joes-diner")
27
+ # City.friendly.find("chicago").restaurants.friendly.find("joes-diner")
28
+ #
29
+ # Without :scoped in this case, one of the restaurants would have the slug
30
+ # `joes-diner` and the other would have `joes-diner-f9f3789a-daec-4156-af1d-fab81aa16ee5`.
31
+ #
32
+ # The value for the `:scope` option can be the name of a `belongs_to` relation, or
33
+ # a column.
34
+ #
35
+ # Additionally, the `:scope` option can receive an array of scope values:
36
+ #
37
+ # class Cuisine < ActiveRecord::Base
38
+ # extend FriendlyId
39
+ # has_many :restaurants
40
+ # friendly_id :name, :use => :slugged
41
+ # end
42
+ #
43
+ # class City < ActiveRecord::Base
44
+ # extend FriendlyId
45
+ # has_many :restaurants
46
+ # friendly_id :name, :use => :slugged
47
+ # end
48
+ #
49
+ # class Restaurant < ActiveRecord::Base
50
+ # extend FriendlyId
51
+ # belongs_to :city
52
+ # friendly_id :name, :use => :scoped, :scope => [:city, :cuisine]
53
+ # end
54
+ #
55
+ # All supplied values will be used to determine scope.
56
+ #
57
+ # ### Finding Records by Friendly ID
58
+ #
59
+ # If you are using scopes your friendly ids may not be unique, so a simple find
60
+ # like:
61
+ #
62
+ # Restaurant.friendly.find("joes-diner")
63
+ #
64
+ # may return the wrong record. In these cases it's best to query through the
65
+ # relation:
66
+ #
67
+ # @city.restaurants.friendly.find("joes-diner")
68
+ #
69
+ # Alternatively, you could pass the scope value as a query parameter:
70
+ #
71
+ # Restaurant.where(:city_id => @city.id).friendly.find("joes-diner")
72
+ #
73
+ #
74
+ # ### Finding All Records That Match a Scoped ID
75
+ #
76
+ # Query the slug column directly:
77
+ #
78
+ # Restaurant.where(:slug => "joes-diner")
79
+ #
80
+ # ### Routes for Scoped Models
81
+ #
82
+ # Recall that FriendlyId is a database-centric library, and does not set up any
83
+ # routes for scoped models. You must do this yourself in your application. Here's
84
+ # an example of one way to set this up:
85
+ #
86
+ # # in routes.rb
87
+ # resources :cities do
88
+ # resources :restaurants
89
+ # end
90
+ #
91
+ # # in views
92
+ # <%= link_to 'Show', [@city, @restaurant] %>
93
+ #
94
+ # # in controllers
95
+ # @city = City.friendly.find(params[:city_id])
96
+ # @restaurant = @city.restaurants.friendly.find(params[:id])
97
+ #
98
+ # # URLs:
99
+ # http://example.org/cities/seattle/restaurants/joes-diner
100
+ # http://example.org/cities/chicago/restaurants/joes-diner
101
+ #
102
+ # @guide end
104
103
  module Scoped
105
-
106
104
  # FriendlyId::Config.use will invoke this method when present, to allow
107
105
  # loading dependent modules prior to overriding them when necessary.
108
106
  def self.setup(model_class)
@@ -146,7 +144,6 @@ an example of one way to set this up:
146
144
  # This module adds the `:scope` configuration option to
147
145
  # {FriendlyId::Configuration FriendlyId::Configuration}.
148
146
  module Configuration
149
-
150
147
  # Gets the scope value.
151
148
  #
152
149
  # 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