auto_increment 1.6.1 → 1.7.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.
@@ -4,46 +4,47 @@
4
4
  module AutoIncrement
5
5
  # +AutoIncrement::Incrementor+
6
6
  class Incrementor
7
- def initialize(column = nil, options = {})
8
- if column.is_a? Hash
9
- options = column
10
- column = nil
11
- end
12
-
13
- @column = column || options[:column] || :code
14
- @options = options.reverse_merge initial: 1, force: false
15
- @options[:scope] = [@options[:scope]].compact unless @options[:scope].is_a?(Array)
16
- @options[:model_scope] = [@options[:model_scope]].compact unless @options[:model_scope].is_a?(Array)
7
+ def initialize(record, column = nil, **options)
8
+ @record = record
9
+ @column = column || options.fetch(:column, :code)
10
+ @initial = resolve_initial(options)
11
+ @force = options.fetch(:force, false)
12
+ @scope = Array.wrap(options[:scope]).compact
13
+ @model_scope = Array.wrap(options[:model_scope]).compact
14
+ @lock = options.fetch(:lock, false)
17
15
  end
18
16
 
19
- def before_create(record)
20
- @record = record
17
+ def run
21
18
  write if can_write?
22
19
  end
23
20
 
24
- alias before_validation before_create
25
- alias before_save before_create
26
-
27
21
  private
28
22
 
29
23
  def can_write?
30
- @record.send(@column).blank? || @options[:force]
24
+ @record.send(@column).blank? || @force
31
25
  end
32
26
 
33
27
  def write
34
28
  @record.send :write_attribute, @column, increment
35
29
  end
36
30
 
31
+ def maximum_query
32
+ query = build_scopes(build_model_scope(@record.class))
33
+ query = query.lock if lock?
34
+
35
+ query
36
+ end
37
+
37
38
  def build_scopes(query)
38
- @options[:scope].each do |scope|
39
- query = query.where(scope => @record.send(scope)) if scope.present? && @record.respond_to?(scope)
39
+ @scope.each do |scope|
40
+ query = query.where(scope => @record.send(scope)) if @record.respond_to?(scope)
40
41
  end
41
42
 
42
43
  query
43
44
  end
44
45
 
45
46
  def build_model_scope(query)
46
- @options[:model_scope].reject(&:nil?).each do |scope|
47
+ @model_scope.each do |scope|
47
48
  query = query.send(scope)
48
49
  end
49
50
 
@@ -51,30 +52,37 @@ module AutoIncrement
51
52
  end
52
53
 
53
54
  def maximum
54
- query = build_scopes(build_model_scope(@record.class))
55
- query.lock if lock?
55
+ query = maximum_query
56
56
 
57
- if string?
58
- query.select("#{@column} max")
59
- .order(Arel.sql("LENGTH(#{@column}) DESC, #{@column} DESC"))
60
- .first.try :max
57
+ if column_string?
58
+ quoted_column = @record.class.connection.quote_column_name(@column)
59
+ query.select("#{quoted_column} max")
60
+ .order(Arel.sql("LENGTH(#{quoted_column}) DESC, #{quoted_column} DESC"))
61
+ .first.try :max
61
62
  else
62
63
  query.maximum @column
63
64
  end
64
65
  end
65
66
 
66
67
  def lock?
67
- @options[:lock] == true
68
+ @lock == true
68
69
  end
69
70
 
70
71
  def increment
71
72
  max = maximum
72
73
 
73
- max.blank? ? @options[:initial] : max.next
74
+ max.blank? ? @initial : max.next
75
+ end
76
+
77
+ def resolve_initial(options)
78
+ return options[:initial] if options.key?(:initial)
79
+
80
+ column_string? ? "1" : 1
74
81
  end
75
82
 
76
- def string?
77
- @options[:initial].instance_of?(String)
83
+ def column_string?
84
+ col = @record.class.columns_hash[@column.to_s]
85
+ col&.type&.in?(%i[string text])
78
86
  end
79
87
  end
80
88
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  # +AutoIncrement::VERSION+
4
4
  module AutoIncrement
5
- VERSION = '1.6.1'
5
+ VERSION = "1.7.0"
6
6
  end
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
4
- require 'i18n'
5
- require 'active_record'
6
- require 'active_support'
7
- require 'active_support/time_with_zone'
8
- require 'auto_increment/version'
3
+ require "date"
4
+ require "i18n"
5
+ require "active_record"
6
+ require "active_support"
7
+ require "active_support/time_with_zone"
8
+ require "auto_increment/version"
9
9
 
10
10
  # +AutoIncrement+
11
11
  module AutoIncrement
12
- autoload :Incrementor, 'auto_increment/incrementor'
13
- autoload :ActiveRecord, 'auto_increment/active_record'
12
+ autoload :Incrementor, "auto_increment/incrementor"
13
+ autoload :ActiveRecord, "auto_increment/active_record"
14
14
  end
15
15
 
16
16
  ActiveRecord::Base.include AutoIncrement::ActiveRecord
@@ -1,48 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
4
- require 'models/account'
5
- require 'models/user'
3
+ require "spec_helper"
4
+ require "models/account"
5
+ require "models/user"
6
+ require "models/post"
6
7
 
7
8
  describe AutoIncrement do
8
9
  before :all do
9
- @account1 = Account.create name: 'My Account'
10
- @account2 = Account.create name: 'Another Account', code: 50
10
+ @account1 = Account.create name: "My Account"
11
+ @account2 = Account.create name: "Another Account", code: 50
11
12
 
12
- @user1_account1 = @account1.users.create name: 'Felipe', letter_code: 'Z'
13
- @user1_account2 = @account2.users.create name: 'Daniel'
14
- @user2_account2 = @account2.users.create name: 'Mark'
15
- @user3_account2 = @account2.users.create name: 'Robert'
13
+ @user1_account1 = @account1.users.create name: "Felipe", letter_code: "Z"
14
+ @user1_account2 = @account2.users.create name: "Daniel"
15
+ @user2_account2 = @account2.users.create name: "Mark"
16
+ @user3_account2 = @account2.users.create name: "Robert"
16
17
  end
17
18
 
18
- describe 'initial' do
19
+ after :all do
20
+ Account.delete_all
21
+ User.delete_all
22
+ end
23
+
24
+ describe "initial" do
19
25
  it { expect(@account1.code).to eq 1 }
20
- it { expect(@user1_account1.letter_code).to eq 'A' }
26
+ it { expect(@user1_account1.letter_code).to eq "A" }
21
27
  end
22
28
 
23
- describe 'do not increment outside scope' do
24
- it { expect(@user1_account2.letter_code).to eq 'A' }
29
+ describe "do not increment outside scope" do
30
+ it { expect(@user1_account2.letter_code).to eq "A" }
25
31
  end
26
32
 
27
- describe 'not set column if is already set' do
33
+ describe "not set column if is already set" do
28
34
  it { expect(@account2.code).to eq 50 }
29
35
  end
30
36
 
31
- describe 'set column if option force is used' do
32
- it { expect(@user1_account1.letter_code).to eq 'A' }
37
+ describe "set column if option force is used" do
38
+ it { expect(@user1_account1.letter_code).to eq "A" }
33
39
  end
34
40
 
35
- describe 'locks query for increment' do
41
+ describe "locks query for increment" do
36
42
  before :all do
37
43
  threads = []
38
44
  lock = Mutex.new
39
- @account = Account.create name: 'Another Account', code: 50
45
+ @account = Account.create name: "Another Account", code: 50
40
46
  @accounts = []
41
47
  5.times do |_t|
42
48
  threads << Thread.new do
43
49
  lock.synchronize do
44
50
  5.times do |_thr|
45
- @accounts << (@account.users.create name: 'Daniel')
51
+ @accounts << (@account.users.create name: "Daniel")
46
52
  end
47
53
  end
48
54
  end
@@ -55,17 +61,73 @@ describe AutoIncrement do
55
61
  end
56
62
 
57
63
  it { expect(@accounts.size).to eq 25 }
58
- it { expect(account_last_letter_code).to eq 'Y' }
64
+ it { expect(account_last_letter_code).to eq "Y" }
59
65
  end
60
66
 
61
- describe 'set before validation' do
67
+ describe "set before validation" do
62
68
  account3 = Account.new
63
69
  account3.valid?
64
70
 
65
71
  it { expect(account3.code).not_to be_nil }
66
72
  end
67
73
 
68
- describe 'uses model scopes' do
69
- it { expect(@user3_account2.letter_code).to eq('C') }
74
+ describe "uses model scopes" do
75
+ it { expect(@user3_account2.letter_code).to eq("C") }
76
+ end
77
+
78
+ describe "string column with integer initial" do
79
+ it "increments correctly past the 9-to-10 boundary" do
80
+ 15.times do |i|
81
+ post = Post.create!
82
+ expect(post.ref.to_i).to eq(i + 1)
83
+ end
84
+ end
85
+ end
86
+
87
+ describe "deprecation warning" do
88
+ it "warns when initial is a string on an integer column" do
89
+ expect {
90
+ Class.new(ActiveRecord::Base) do
91
+ self.table_name = "accounts"
92
+ auto_increment :code, initial: "A"
93
+ end
94
+ }.to output(/\[DEPRECATION\] The initial value type \(String\) does not match the column type \(integer\) for column 'code'.*raise an error in the future/).to_stderr
95
+ end
96
+
97
+ it "warns when initial is an integer on a string column" do
98
+ expect {
99
+ Class.new(ActiveRecord::Base) do
100
+ self.table_name = "posts"
101
+ auto_increment :ref, initial: 1
102
+ end
103
+ }.to output(/\[DEPRECATION\] The initial value type \(Integer\) does not match the column type \(string\) for column 'ref'.*raise an error in the future/).to_stderr
104
+ end
105
+
106
+ it "does not warn when types match (integer column, integer initial)" do
107
+ expect {
108
+ Class.new(ActiveRecord::Base) do
109
+ self.table_name = "accounts"
110
+ auto_increment :code, initial: 100
111
+ end
112
+ }.not_to output(/\[DEPRECATION\]/).to_stderr
113
+ end
114
+
115
+ it "does not warn when types match (string column, string initial)" do
116
+ expect {
117
+ Class.new(ActiveRecord::Base) do
118
+ self.table_name = "posts"
119
+ auto_increment :ref, initial: "X"
120
+ end
121
+ }.not_to output(/\[DEPRECATION\]/).to_stderr
122
+ end
123
+
124
+ it "does not warn when initial is omitted on a string column (auto-detects)" do
125
+ expect {
126
+ Class.new(ActiveRecord::Base) do
127
+ self.table_name = "posts"
128
+ auto_increment :ref
129
+ end
130
+ }.not_to output(/\[DEPRECATION\]/).to_stderr
131
+ end
70
132
  end
71
133
  end
@@ -1,33 +1,151 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'spec_helper'
3
+ require "spec_helper"
4
+ require "models/account"
5
+ require "models/user"
6
+ require "models/post"
4
7
 
5
8
  describe AutoIncrement::Incrementor do
6
- {
7
- nil => 1,
8
- 0 => 1,
9
- 1 => 2,
10
- 'A' => 'B',
11
- 'Z' => 'AA',
12
- 'AA' => 'AB',
13
- 'AAAAA' => 'AAAAB'
14
- }.each do |previous_value, next_value|
15
- describe "increment value for #{previous_value}" do
16
- it do
17
- allow(subject).to receive(:maximum) { previous_value }
18
- expect(subject.send(:increment)).to eq next_value
9
+ def create_account(code:, name: "seed")
10
+ Account.create!(name: name, code: code)
11
+ end
12
+
13
+ # Raw SQL is required because User has `force: true` — the auto_increment
14
+ # callback would otherwise overwrite the value on `create!`.
15
+ def create_user(code:, name: "seed")
16
+ conn = ActiveRecord::Base.connection
17
+ conn.execute "INSERT INTO users (name, letter_code) VALUES (#{conn.quote(name)}, #{conn.quote(code)})"
18
+ end
19
+
20
+ describe "#run" do
21
+ describe "integer column" do
22
+ it "auto-detects initial value 1 when no initial is given" do
23
+ account = Account.new
24
+ AutoIncrement::Incrementor.new(account).run
25
+ expect(account.code).to eq 1
26
+ end
27
+
28
+ {
29
+ 0 => 1,
30
+ 1 => 2,
31
+ 9 => 10
32
+ }.each do |previous_value, next_value|
33
+ it "increments #{previous_value} to #{next_value}" do
34
+ create_account(code: previous_value)
35
+
36
+ account = Account.new
37
+ AutoIncrement::Incrementor.new(account).run
38
+ expect(account.code).to eq next_value
39
+ end
40
+ end
41
+ end
42
+
43
+ describe "string column" do
44
+ it "sets initial value when no records exist" do
45
+ user = User.new
46
+ AutoIncrement::Incrementor.new(user, column: :letter_code, initial: "A").run
47
+ expect(user.letter_code).to eq "A"
48
+ end
49
+
50
+ it "auto-detects initial value '1' when no initial is given" do
51
+ post = Post.new
52
+ AutoIncrement::Incrementor.new(post, column: :ref).run
53
+ expect(post.ref).to eq "1"
54
+ end
55
+
56
+ {
57
+ "A" => "B",
58
+ "Z" => "AA",
59
+ "AA" => "AB",
60
+ "AAAAA" => "AAAAB"
61
+ }.each do |previous_value, next_value|
62
+ it "increments #{previous_value} to #{next_value}" do
63
+ create_user(code: previous_value)
64
+
65
+ user = User.new
66
+ AutoIncrement::Incrementor.new(user, column: :letter_code, initial: "A").run
67
+ expect(user.letter_code).to eq next_value
68
+ end
69
+ end
70
+
71
+ it "uses length-aware ordering inferred from the column schema" do
72
+ %w[1 2 3 4 5 6 7 8 9 10].each { |v| create_user(code: v) }
73
+
74
+ user = User.new
75
+ AutoIncrement::Incrementor.new(user, column: :letter_code, initial: 1).run
76
+ expect(user.letter_code).to eq "11"
77
+ end
78
+ end
79
+
80
+ describe "force" do
81
+ it "does not overwrite an existing value when force is false" do
82
+ account = Account.new(code: 5)
83
+ expect { AutoIncrement::Incrementor.new(account).run }
84
+ .not_to change { account.code }
85
+ end
86
+
87
+ it "overwrites an existing value when force is true" do
88
+ create_account(code: 10)
89
+ account = Account.new(code: 5)
90
+ AutoIncrement::Incrementor.new(account, force: true).run
91
+ expect(account.code).to eq 11
92
+ end
93
+
94
+ it "sets the initial value when force is true on an empty table" do
95
+ account = Account.new(code: 5)
96
+ AutoIncrement::Incrementor.new(account, force: true).run
97
+ expect(account.code).to eq 1
98
+ end
99
+ end
100
+
101
+ describe "scope" do
102
+ it "only considers records within the same scope" do
103
+ create_account(code: 10, name: "other")
104
+
105
+ account = Account.new(name: "mine")
106
+ AutoIncrement::Incrementor.new(account, scope: :name).run
107
+ expect(account.code).to eq 1
19
108
  end
20
109
  end
21
- end
22
110
 
23
- describe 'initial value of string' do
24
- subject do
25
- AutoIncrement::Incrementor.new initial: 'A'
111
+ describe "model scope" do
112
+ it "bypasses default_scope to see all records" do
113
+ create_user(code: "C", name: "Mark")
114
+
115
+ user = User.new
116
+ AutoIncrement::Incrementor.new(user, column: :letter_code, initial: "A", model_scope: :with_mark).run
117
+ expect(user.letter_code).to eq "D"
118
+ end
119
+
120
+ it "applies model scopes when building the query" do
121
+ create_user(code: "C", name: "Mark")
122
+ create_user(code: "A", name: "Other")
123
+
124
+ user = User.new(name: "Mark")
125
+ AutoIncrement::Incrementor.new(user, column: :letter_code, initial: "A", model_scope: :with_mark).run
126
+ expect(user.letter_code).to eq "D"
127
+ end
128
+
129
+ it "only considers records matching the model scope" do
130
+ create_account(code: 10, name: "Mark")
131
+ create_account(code: 5, name: "Other")
132
+ account = Account.new(name: "Mark")
133
+ AutoIncrement::Incrementor.new(account, column: :code, initial: 1, model_scope: :only_mark).run
134
+ expect(account.code).to eq 11
135
+ end
26
136
  end
27
137
 
28
- it do
29
- allow(subject).to receive(:maximum) { nil }
30
- expect(subject.send(:increment)).to eq 'A'
138
+ describe "lock" do
139
+ it "increments correctly with lock enabled" do
140
+ create_account(code: 10)
141
+ account = Account.new
142
+ incrementor = AutoIncrement::Incrementor.new(account, lock: true)
143
+
144
+ expect(incrementor.send(:maximum_query).lock_value).to eq true
145
+
146
+ incrementor.run
147
+ expect(account.code).to eq 11
148
+ end
31
149
  end
32
150
  end
33
151
  end
@@ -5,4 +5,6 @@ class Account < ActiveRecord::Base
5
5
  auto_increment :code, before: :validation
6
6
 
7
7
  has_many :users
8
+
9
+ scope :only_mark, -> { where(name: "Mark") }
8
10
  end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Spec +Post+ — string column with default integer initial
4
+ class Post < ActiveRecord::Base
5
+ auto_increment :ref
6
+ end
data/spec/models/user.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # + Spec +User+
3
+ # Spec +User+
4
4
  class User < ActiveRecord::Base
5
- auto_increment :letter_code, scope: :account_id, initial: 'A', force: true,
6
- lock: true, model_scope: :with_mark
5
+ auto_increment :letter_code, scope: :account_id, initial: "A", force: true,
6
+ lock: true, model_scope: :with_mark
7
7
 
8
8
  belongs_to :account
9
9
 
10
- default_scope -> { where 'name <> ?', 'Mark' }
10
+ default_scope -> { where "name <> ?", "Mark" }
11
11
 
12
12
  scope :with_mark, -> { unscoped }
13
13
  end
data/spec/spec_helper.rb CHANGED
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'pry'
4
- require 'auto_increment'
5
- require 'database_cleaner'
3
+ require "pry"
4
+ require "auto_increment"
5
+ require "database_cleaner"
6
6
 
7
- ActiveRecord::Base.establish_connection adapter: 'sqlite3',
8
- database: 'spec/db/sync.db',
9
- timeout: 5000
7
+ db_path = "spec/db/sync.db"
10
8
 
11
- # require 'support/active_record'
12
- require 'support/database_cleaner'
9
+ File.delete(db_path) if File.exist?(db_path)
10
+
11
+ ActiveRecord::Base.establish_connection(
12
+ adapter: "sqlite3",
13
+ database: db_path,
14
+ timeout: 5000
15
+ )
16
+
17
+ require "support/active_record"
18
+ require "support/database_cleaner"
@@ -12,3 +12,8 @@ ActiveRecord::Migration.create_table :users do |t|
12
12
  t.integer :account_id
13
13
  t.string :letter_code
14
14
  end
15
+
16
+ # +ActiveRecord+ migration for Posts (string column, integer initial)
17
+ ActiveRecord::Migration.create_table :posts do |t|
18
+ t.string :ref
19
+ end