friendly_id 5.4.2 → 5.5.1

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 (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