sequenced 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2c19db66c935260344abf853e2e1725a7dc9ef4d
4
- data.tar.gz: 27bc38c11839f113839eaf23b8f213f677fa9024
3
+ metadata.gz: 19469f53054a51802c23c141597b405b2d061b9e
4
+ data.tar.gz: 86e49028b4e37daf29ef770727d68e0edd096634
5
5
  SHA512:
6
- metadata.gz: 1f47927ecd09469be6fc26fb30b9a752a8f94bee42cba8c54c5cb44d4e0fafa328e6c3dad9a69529a20b4461add62e1d6cd14a730230b284db904d8bb97da4be
7
- data.tar.gz: 9e9707f1c891f16bd0f30a7f0dc001a6f212b9006ef3294549c5c037da817029d850f52b219872e0ac6d7ad8551a140492b28ca2a3d79cdf52117df465791366
6
+ metadata.gz: 3e98ddf8640a706f049d2295861792cd7b5e10d21e6c4d1591ed46126bf645590fb8ab00e3b9ded0c882bd5530d60deec9e013824f86d539e73e9c56a7fb9af5
7
+ data.tar.gz: cb58678c8d817b23677162008c60120f564f48b41713d9ecc9a97922ac74c1b7677c3691b5ab1661ce171e9c82c709fefcc48425773c74b5ac4d91caaf619896
@@ -1,4 +1,13 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
- - ruby-head
4
+ - 2.2.3
5
+
6
+ env:
7
+ - DB=sqlite
8
+ - DB=postgresql
9
+
10
+ before_script:
11
+ - rake db:create
12
+
13
+ sudo: false
@@ -1,3 +1,9 @@
1
+ 3.0.0 (November 28, 2015)
2
+ -------------------------
3
+
4
+ * Make this gem thread-safe for PostgreSQL
5
+ (samphilipd, [#16](https://github.com/djreimer/sequenced/pull/16))
6
+
1
7
  2.0.0 (October 24, 2014)
2
8
  ------------------------
3
9
 
data/Gemfile CHANGED
@@ -8,6 +8,12 @@ gemspec
8
8
  # jquery-rails is used by the dummy application
9
9
  gem "jquery-rails"
10
10
 
11
+ group :development, :test do
12
+ gem 'sqlite3'
13
+ # gem 'mysql2'
14
+ gem 'pg'
15
+ end
16
+
11
17
  # Declare any dependencies that are still in development here instead of in
12
18
  # your gemspec. These might include edge Rails or gems from your path or
13
19
  # Git. Remember to move these dependencies to your gemspec before releasing
@@ -1,4 +1,4 @@
1
- Copyright 2011-2014 Derrick Reimer
1
+ Copyright 2015 Derrick Reimer
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -4,20 +4,20 @@
4
4
  [![Code Climate](https://codeclimate.com/github/djreimer/sequenced.png)](https://codeclimate.com/github/djreimer/sequenced)
5
5
  [![Gem Version](https://badge.fury.io/rb/sequenced.png)](http://badge.fury.io/rb/sequenced)
6
6
 
7
- Sequenced is a simple gem that generates scoped sequential IDs for
8
- ActiveRecord models. This gem provides an `acts_as_sequenced` macro that
9
- automatically assigns a unique, sequential ID to each record. The sequential ID is
10
- not a replacement for the database primary key, but rather adds another way to
7
+ Sequenced is a simple gem that generates scoped sequential IDs for
8
+ ActiveRecord models. This gem provides an `acts_as_sequenced` macro that
9
+ automatically assigns a unique, sequential ID to each record. The sequential ID is
10
+ not a replacement for the database primary key, but rather adds another way to
11
11
  retrieve the object without exposing the primary key.
12
12
 
13
13
  ## Purpose
14
14
 
15
- It's generally a bad practice to expose your primary keys to the world
16
- in your URLs. However, it is often appropriate to number objects in sequence
15
+ It's generally a bad practice to expose your primary keys to the world
16
+ in your URLs. However, it is often appropriate to number objects in sequence
17
17
  (in the context of a parent object).
18
18
 
19
19
  For example, given a Question model that has many Answers, it makes sense
20
- to number answers sequentially for each individual question. You can achieve
20
+ to number answers sequentially for each individual question. You can achieve
21
21
  this with Sequenced in one line of code:
22
22
 
23
23
  ```ruby
@@ -34,7 +34,7 @@ end
34
34
  ## Installation
35
35
 
36
36
  Add the gem to your Gemfile:
37
-
37
+
38
38
  gem 'sequenced'
39
39
 
40
40
  Install the gem with bundler:
@@ -70,12 +70,36 @@ class Answer < ActiveRecord::Base
70
70
  end
71
71
  ```
72
72
 
73
+ ## Schema and data integrity
74
+
75
+ **This gem is only concurrent-safe for PostgreSQL databases**. For other database systems, unexpected behavior may occur if you attempt to create records concurrently.
76
+
77
+ You can mitigate this somewhat by applying a unique index to your sequential ID column (or a multicolumn unique index on sequential ID and scope columns, if you are using scopes). This will ensure that you can never have duplicate sequential IDs within a scope, causing concurrent updates to instead raise a uniqueness error at the database-level.
78
+
79
+ It is also a good idea to apply a not-null constraint to your sequential ID column as well if you never intend to skip it.
80
+
81
+ Here is an example migration for a model that has a `sequential_id` scoped to a `burrow_id`:
82
+
83
+ ```ruby
84
+ # app/db/migrations/20151120190645_create_badgers.rb
85
+ class CreateBadgers < ActiveRecord::Migration
86
+ def change
87
+ create_table :badgers do |t|
88
+ t.integer :sequential_id, null: false
89
+ t.integer :burrow_id
90
+ end
91
+
92
+ add_index :badgers, [:sequential_id, :burrow_id], unique: true
93
+ end
94
+ end
95
+ ```
96
+
73
97
  ## Configuration
74
98
 
75
99
  ### Overriding the default sequential ID column
76
100
 
77
- By default, Sequenced uses the `sequential_id` column and assumes it already
78
- exists. If you wish to store the sequential ID in different integer column,
101
+ By default, Sequenced uses the `sequential_id` column and assumes it already
102
+ exists. If you wish to store the sequential ID in different integer column,
79
103
  simply specify the column name with the `column` option:
80
104
 
81
105
  ```ruby
@@ -84,7 +108,7 @@ acts_as_sequenced scope: :question_id, column: :my_sequential_id
84
108
 
85
109
  ### Starting the sequence at a specific number
86
110
 
87
- By default, Sequenced begins sequences with 1. To start at a different
111
+ By default, Sequenced begins sequences with 1. To start at a different
88
112
  integer, simply set the `start_at` option:
89
113
 
90
114
  ```ruby
@@ -113,7 +137,7 @@ acts_as_sequenced skip: lambda { |r| r.score == 0 }
113
137
 
114
138
  ## Example
115
139
 
116
- Suppose you have a question model that has many answers. This example
140
+ Suppose you have a question model that has many answers. This example
117
141
  demonstrates how to use Sequenced to enable access to the nested answer
118
142
  resource via its sequential ID.
119
143
 
@@ -127,7 +151,7 @@ end
127
151
  class Answer < ActiveRecord::Base
128
152
  belongs_to :question
129
153
  acts_as_sequenced scope: :question_id
130
-
154
+
131
155
  # Automatically use the sequential ID in URLs
132
156
  def to_param
133
157
  self.sequential_id
@@ -141,48 +165,25 @@ end
141
165
 
142
166
  # app/controllers/answers_controller.rb
143
167
  class AnswersController < ApplicationController
144
- before_filter :load_question
145
- before_filter :load_answer, only: [:show, :edit, :update, :destroy]
146
-
147
- private
148
-
149
- def load_question
168
+ def show
150
169
  @question = Question.find(params[:question_id])
151
- end
152
-
153
- def load_answer
154
- @answer = @question.answers.where(:sequential_id => params[:id]).first
170
+ @answer = @question.answers.find_by(sequential_id: params[:id])
155
171
  end
156
172
  end
157
173
  ```
158
174
 
159
175
  Now, answers are accessible via their sequential IDs:
160
176
 
161
- http://example.com/questions/5/answers/1 # Good
177
+ http://example.com/questions/5/answers/1 # Good
162
178
 
163
179
  instead of by their primary keys:
164
180
 
165
181
  http://example.com/questions/5/answer/32454 # Bad
166
182
 
167
- ## License
168
-
169
- Copyright &copy; 2011-2014 Derrick Reimer
170
-
171
- Permission is hereby granted, free of charge, to any person obtaining
172
- a copy of this software and associated documentation files (the
173
- "Software"), to deal in the Software without restriction, including
174
- without limitation the rights to use, copy, modify, merge, publish,
175
- distribute, sublicense, and/or sell copies of the Software, and to
176
- permit persons to whom the Software is furnished to do so, subject to
177
- the following conditions:
178
-
179
- The above copyright notice and this permission notice shall be
180
- included in all copies or substantial portions of the Software.
183
+ ## Contributing
181
184
 
182
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
183
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
184
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
185
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
186
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
187
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
188
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
185
+ 1. Fork it
186
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
187
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
188
+ 4. Push to the branch (`git push origin my-new-feature`)
189
+ 5. Create new Pull Request
data/Rakefile CHANGED
@@ -31,5 +31,29 @@ Rake::TestTask.new(:test) do |t|
31
31
  t.verbose = false
32
32
  end
33
33
 
34
-
35
34
  task :default => :test
35
+
36
+ namespace :db do
37
+ task :create do
38
+ # File.expand_path is executed directory of generated Rails app
39
+ rakefile = File.expand_path('Rakefile', 'test/dummy/')
40
+ command = "rake -f '%s' db:create" % rakefile
41
+ sh(command)
42
+ end
43
+
44
+ task :drop do
45
+ # File.expand_path is executed directory of generated Rails app
46
+ rakefile = File.expand_path('Rakefile', 'test/dummy/')
47
+ command = "rake -f '%s' db:drop" % rakefile
48
+ sh(command)
49
+ end
50
+
51
+ namespace :test do
52
+ task :prepare do
53
+ # File.expand_path is executed directory of generated Rails app
54
+ rakefile = File.expand_path('Rakefile', 'test/dummy/')
55
+ command = "rake -f '%s' db:test:prepare" % rakefile
56
+ sh(command)
57
+ end
58
+ end
59
+ end
@@ -11,7 +11,9 @@ module Sequenced
11
11
  end
12
12
 
13
13
  def set
14
- record.send(:"#{column}=", next_id) unless id_set? || skip?
14
+ return if id_set? || skip?
15
+ lock_table
16
+ record.send(:"#{column}=", next_id)
15
17
  end
16
18
 
17
19
  def id_set?
@@ -44,6 +46,17 @@ module Sequenced
44
46
 
45
47
  private
46
48
 
49
+ def lock_table
50
+ if postgresql?
51
+ record.class.connection.execute("LOCK TABLE #{record.class.table_name} IN EXCLUSIVE MODE")
52
+ end
53
+ end
54
+
55
+ def postgresql?
56
+ defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) &&
57
+ record.class.connection.instance_of?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
58
+ end
59
+
47
60
  def base_relation
48
61
  record.class.base_class.unscoped
49
62
  end
@@ -67,4 +80,4 @@ module Sequenced
67
80
  end
68
81
 
69
82
  end
70
- end
83
+ end
@@ -1,3 +1,3 @@
1
1
  module Sequenced
2
- VERSION = "2.0.0"
2
+ VERSION = "3.0.0"
3
3
  end
@@ -17,5 +17,4 @@ Gem::Specification.new do |s|
17
17
  s.add_dependency "activesupport", ">= 3.0"
18
18
  s.add_dependency "activerecord", ">= 3.0"
19
19
  s.add_development_dependency "rails", ">= 3.1"
20
- s.add_development_dependency "sqlite3"
21
20
  end
@@ -0,0 +1,82 @@
1
+ require 'test_helper'
2
+
3
+ # Test Models:
4
+ #
5
+ # Answer - :scope => :question_id
6
+ # Comment - :scope => :question_id (with an AR default scope)
7
+ # Invoice - :scope => :account_id, :start_at => 1000
8
+ # Product - :scope => :account_id, :start_at => lambda { |r| r.computed_start_value }
9
+ # Order - :scope => :non_existent_column
10
+ # User - :scope => :account_id, :column => :custom_sequential_id
11
+ # Address - :scope => :account_id ('sequential_id' does not exist)
12
+ # Email - :scope => [:emailable_id, :emailable_type]
13
+ # Subscription - no options
14
+ # Rating - :scope => :comment_id, skip: { |r| r.score == 0 }
15
+ # Monster - no options
16
+ # Zombie - STI, inherits from Monster
17
+ # Werewolf - STI, inherits from Monster
18
+
19
+ # ConcurrentBadger - scope: :concurrent_burrow_id,
20
+ # NOT NULL constraint on sequential_id,
21
+ # UNIQUE constraint on sequential_id within concurrent_burrow_id scope
22
+
23
+ if ENV['DB'] == 'postgresql'
24
+ class ConcurrencyTest < ActiveSupport::TestCase
25
+ self.use_transactional_fixtures = false
26
+
27
+ def setup
28
+ ConcurrentBadger.delete_all
29
+ end
30
+
31
+ def teardown
32
+ ConcurrentBadger.delete_all
33
+ end
34
+
35
+ test "creates records concurrently without data races" do
36
+ Thread.abort_on_exception = true
37
+ range = (1..50)
38
+
39
+ threads = []
40
+ range.each do
41
+ threads << Thread.new do
42
+ ConcurrentBadger.create!(burrow_id: 1)
43
+ end
44
+ end
45
+
46
+ threads.each(&:join)
47
+
48
+ sequential_ids = ConcurrentBadger.pluck(:sequential_id)
49
+ assert_equal range.to_a, sequential_ids
50
+ end
51
+
52
+ test "does not affect saving multiple records within a transaction" do
53
+ range = (1..10)
54
+
55
+ ConcurrentBadger.transaction do
56
+ range.each do
57
+ ConcurrentBadger.create!(burrow_id: 1)
58
+ end
59
+ end
60
+
61
+ sequential_ids = ConcurrentBadger.pluck(:sequential_id)
62
+ assert_equal range.to_a, sequential_ids
63
+ end
64
+
65
+ test "does not affect saving multiple records within nested transactons" do
66
+ range = (1..10)
67
+
68
+ ConcurrentBadger.transaction do
69
+ ConcurrentBadger.transaction do
70
+ ConcurrentBadger.transaction do
71
+ range.each do
72
+ ConcurrentBadger.create!(burrow_id: 1)
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ sequential_ids = ConcurrentBadger.pluck(:sequential_id)
79
+ assert_equal range.to_a, sequential_ids
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,3 @@
1
+ class ConcurrentBadger < ActiveRecord::Base
2
+ acts_as_sequenced scope: :burrow_id
3
+ end
@@ -1,25 +1,29 @@
1
- # SQLite version 3.x
2
- # gem install sqlite3
3
- #
4
- # Ensure the SQLite 3 gem is defined in your Gemfile
5
- # gem 'sqlite3'
6
- development:
7
- adapter: sqlite3
8
- database: db/development.sqlite3
9
- pool: 5
10
- timeout: 5000
11
-
12
- # Warning: The database defined as "test" will be erased and
13
- # re-generated from your development database when you run "rake".
14
- # Do not set this db to the same as development or production.
15
- test:
1
+ # example_travisci_multi/config/database.yml
2
+ sqlite: &sqlite
16
3
  adapter: sqlite3
17
4
  database: db/test.sqlite3
18
- pool: 5
19
- timeout: 5000
20
5
 
21
- production:
22
- adapter: sqlite3
23
- database: db/production.sqlite3
24
- pool: 5
6
+ mysql: &mysql
7
+ adapter: mysql2
8
+ username: root
9
+ password:
10
+
11
+ postgresql: &postgresql
12
+ adapter: postgresql
13
+ username: postgres
14
+ password:
15
+ min_messages: ERROR
16
+
17
+ defaults: &defaults
18
+ pool: 100
25
19
  timeout: 5000
20
+ host: localhost
21
+ <<: *<%= ENV['DB'] || "sqlite" %>
22
+
23
+ development:
24
+ database: sequenced_test
25
+ <<: *defaults
26
+
27
+ test:
28
+ database: sequenced_test
29
+ <<: *defaults
@@ -22,16 +22,12 @@ Dummy::Application.configure do
22
22
  # Only use best-standards-support built into browsers
23
23
  config.action_dispatch.best_standards_support = :builtin
24
24
 
25
- # Raise exception on mass assignment protection for Active Record models
26
- config.active_record.mass_assignment_sanitizer = :strict
27
-
28
- # Log the query plan for queries taking more than this (works
29
- # with SQLite, MySQL, and PostgreSQL)
30
- config.active_record.auto_explain_threshold_in_seconds = 0.5
31
-
32
25
  # Do not compress assets
33
26
  config.assets.compress = false
34
27
 
35
28
  # Expands the lines which load the assets
36
29
  config.assets.debug = true
30
+
31
+ # Required for Rails >= 4.2
32
+ config.eager_load = true
37
33
  end
@@ -35,4 +35,7 @@ Dummy::Application.configure do
35
35
 
36
36
  # Print deprecation notices to the stderr
37
37
  config.active_support.deprecation = :stderr
38
+
39
+ # Required for Rails >= 4.2
40
+ config.eager_load = true
38
41
  end
@@ -0,0 +1,10 @@
1
+ class CreateConcurrentBadgers < ActiveRecord::Migration
2
+ def change
3
+ create_table :concurrent_badgers do |t|
4
+ t.integer :sequential_id, null: false
5
+ t.integer :burrow_id
6
+ end
7
+
8
+ add_index :concurrent_badgers, [:sequential_id, :burrow_id], unique: true, name: 'unique_concurrent'
9
+ end
10
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequenced
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Derrick Reimer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-10-24 00:00:00.000000000 Z
11
+ date: 2015-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -52,20 +52,6 @@ dependencies:
52
52
  - - '>='
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.1'
55
- - !ruby/object:Gem::Dependency
56
- name: sqlite3
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - '>='
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- type: :development
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - '>='
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
55
  description: Sequenced is a gem that generates scoped sequential IDs for ActiveRecord
70
56
  models.
71
57
  email:
@@ -81,13 +67,13 @@ files:
81
67
  - MIT-LICENSE
82
68
  - README.md
83
69
  - Rakefile
84
- - TODO.md
85
70
  - lib/sequenced.rb
86
71
  - lib/sequenced/acts_as_sequenced.rb
87
72
  - lib/sequenced/generator.rb
88
73
  - lib/sequenced/version.rb
89
74
  - sequenced.gemspec
90
75
  - test/acts_as_sequenced_test.rb
76
+ - test/concurrency_test.rb
91
77
  - test/dummy/README.rdoc
92
78
  - test/dummy/Rakefile
93
79
  - test/dummy/app/assets/javascripts/application.js
@@ -100,6 +86,7 @@ files:
100
86
  - test/dummy/app/models/address.rb
101
87
  - test/dummy/app/models/answer.rb
102
88
  - test/dummy/app/models/comment.rb
89
+ - test/dummy/app/models/concurrent_badger.rb
103
90
  - test/dummy/app/models/email.rb
104
91
  - test/dummy/app/models/invoice.rb
105
92
  - test/dummy/app/models/monster.rb
@@ -144,6 +131,7 @@ files:
144
131
  - test/dummy/db/migrate/20130730004055_create_products.rb
145
132
  - test/dummy/db/migrate/20131226000000_create_monsters.rb
146
133
  - test/dummy/db/migrate/20140404195334_create_policemen.rb
134
+ - test/dummy/db/migrate/20151120190645_create_concurrent_badgers.rb
147
135
  - test/dummy/db/schema.rb
148
136
  - test/dummy/db/test.sqlite3
149
137
  - test/dummy/lib/assets/.gitkeep
@@ -182,6 +170,7 @@ specification_version: 4
182
170
  summary: Generate scoped sequential IDs for ActiveRecord models
183
171
  test_files:
184
172
  - test/acts_as_sequenced_test.rb
173
+ - test/concurrency_test.rb
185
174
  - test/dummy/app/assets/javascripts/application.js
186
175
  - test/dummy/app/assets/stylesheets/application.css
187
176
  - test/dummy/app/controllers/application_controller.rb
@@ -190,6 +179,7 @@ test_files:
190
179
  - test/dummy/app/models/address.rb
191
180
  - test/dummy/app/models/answer.rb
192
181
  - test/dummy/app/models/comment.rb
182
+ - test/dummy/app/models/concurrent_badger.rb
193
183
  - test/dummy/app/models/email.rb
194
184
  - test/dummy/app/models/invoice.rb
195
185
  - test/dummy/app/models/monster.rb
@@ -234,6 +224,7 @@ test_files:
234
224
  - test/dummy/db/migrate/20130730004055_create_products.rb
235
225
  - test/dummy/db/migrate/20131226000000_create_monsters.rb
236
226
  - test/dummy/db/migrate/20140404195334_create_policemen.rb
227
+ - test/dummy/db/migrate/20151120190645_create_concurrent_badgers.rb
237
228
  - test/dummy/db/schema.rb
238
229
  - test/dummy/db/test.sqlite3
239
230
  - test/dummy/log/development.log
@@ -246,3 +237,4 @@ test_files:
246
237
  - test/dummy/README.rdoc
247
238
  - test/dummy/script/rails
248
239
  - test/test_helper.rb
240
+ has_rdoc:
data/TODO.md DELETED
@@ -1,7 +0,0 @@
1
- TODO
2
- ====
3
-
4
- * Add extensions for other ORM/ODMs
5
- * Mongoid
6
- * Mongomapper
7
- * Datamapper