atomically 1.0.1 → 1.0.2

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
  SHA256:
3
- metadata.gz: d38cf83ab4f1e792cfefbb2c01ac897377fb7af2a81da6f09f932df4d5fa76f2
4
- data.tar.gz: 88640837b4c69ef162d036b2a3e6ad61087e94a6baa79cde026fb748f09b58f1
3
+ metadata.gz: 94187458ce758fe40e71ba068f03dad4e2d8cebb0757099b8cecad1f1a8da017
4
+ data.tar.gz: c79301b92a0436536feb4f850ba7e1b5f8fb8d589c17e50b9b6c20a38022826c
5
5
  SHA512:
6
- metadata.gz: cd968dcc060705bc3d0174daf532fa4c80b8dfadc5845a7406bcf68d930744c6ce78e2f04b97091eb4cea27818ed44df1e4b3c47eefac5e0643ab492b9bed21b
7
- data.tar.gz: 7577dd5ef6f32a4d3753c71637d218358d48aeac8b7c675c9b780d906af56f08bd9cf5cb14d7b98fd12a9275d28f6d45ec8a5baaccb3b177257e300a04c0c890
6
+ metadata.gz: 5163ddf343c94d028e7ea2b6fadd62baccf65e49a9d57ef08ff0894a1a3113c14048af6a410c7129604292c5d9d39d3c81ff94a8293f24b0eb08c170039beb59
7
+ data.tar.gz: 5d02fedd2c17837517874714a23ffedd0a801a93cb12adf1217ed59cf8af483fd1793320f6fb7bbbf3aa35257ba9efda034245b194728949b8c526eaf07c07e2
@@ -1013,7 +1013,8 @@ Layout/SpaceInsideBlockBraces:
1013
1013
  Checks that block braces have or don't have surrounding space.
1014
1014
  For blocks taking parameters, checks that the left brace has
1015
1015
  or doesn't have trailing space.
1016
- Enabled: false
1016
+ SpaceBeforeBlockParameters: false
1017
+ Enabled: true
1017
1018
 
1018
1019
  Layout/SpaceAroundBlockParameters:
1019
1020
  Description: 'Checks the spacing inside and after block parameters pipes.'
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Atomically
2
2
 
3
3
  [![Gem Version](https://img.shields.io/gem/v/atomically.svg?style=flat)](https://rubygems.org/gems/atomically)
4
- [![Build Status](https://travis-ci.org/khiav223577/atomically.svg?branch=master)](https://travis-ci.org/khiav223577/atomically)
4
+ [![Build Status](https://api.travis-ci.com/khiav223577/atomically.svg?branch=master)](https://travis-ci.com/khiav223577/atomically)
5
5
  [![RubyGems](http://img.shields.io/gem/dt/atomically.svg?style=flat)](https://rubygems.org/gems/atomically)
6
6
  [![Code Climate](https://codeclimate.com/github/khiav223577/atomically/badges/gpa.svg)](https://codeclimate.com/github/khiav223577/atomically)
7
7
  [![Test Coverage](https://codeclimate.com/github/khiav223577/atomically/badges/coverage.svg)](https://codeclimate.com/github/khiav223577/atomically/coverage)
@@ -36,6 +36,12 @@ It is useful to add `items` to `user` when `user_items` may not exist.
36
36
  First two args (columns, values) are the same with the [import](https://github.com/zdennis/activerecord-import#columns-and-arrays) method.
37
37
 
38
38
  Example:
39
+ ```rb
40
+ user = User.find(2)
41
+ item1 = Item.find(1)
42
+ item2 = Item.find(2)
43
+ ```
44
+
39
45
  ```rb
40
46
  columns = [:user_id, :item_id, :quantity]
41
47
  values = [[user.id, item1.id, 3], [user.id, item2.id, 2]]
@@ -44,7 +50,15 @@ on_duplicate_update_columns = [:quantity]
44
50
  UserItem.atomically.create_or_plus(columns, values, on_duplicate_update_columns)
45
51
  ```
46
52
 
53
+ #### before
54
+ ![before](https://user-images.githubusercontent.com/4011729/48998921-ff430600-f18f-11e8-8eeb-e8a71bbf5802.png)
55
+
56
+ #### after
57
+ ![image](https://user-images.githubusercontent.com/4011729/48999092-8d1ef100-f190-11e8-8372-86e2e99cbe08.png)
58
+
59
+
47
60
  ### pay_all
61
+
48
62
  Reduce the quantity of items and return how many rows and updated if all of them is enough.
49
63
  Do nothing and return zero if any of them is not enough.
50
64
 
@@ -53,6 +67,32 @@ Example:
53
67
  user.user_items.atomically.pay_all({ item1.id => 4, item2.id => 3 }, [:quantity], primary_key: :item_id)
54
68
  ```
55
69
 
70
+ ### update
71
+
72
+ Updates the attributes of the model from the passed-in hash and saves the record. The difference between this method and [ActiveRecord#update](https://apidock.com/rails/ActiveRecord/Persistence/update) is that it will add extra WHERE conditions to prevent race condition.
73
+
74
+ Example:
75
+ ```rb
76
+ class Arena < ApplicationRecord
77
+ def atomically_close!
78
+ atomically.update(closed_at: Time.now)
79
+ end
80
+
81
+ def close!
82
+ update(closed_at: Time.now)
83
+ end
84
+ end
85
+ ```
86
+ ```sql
87
+ # arena.atomically_close!
88
+ UPDATE `arenas` SET `arenas`.`closed_at` = '2018-11-27 03:44:25', `updated_at` = '2018-11-27 03:44:25'
89
+ WHERE `arenas`.`id` = 1752 AND `arenas`.`closed_at` IS NULL
90
+
91
+ # arena.close!
92
+ UPDATE `arenas` SET `arenas`.`closed_at` = '2018-11-27 03:44:25', `updated_at` = '2018-11-27 03:44:25'
93
+ WHERE `arenas`.`id` = 1752
94
+ ```
95
+
56
96
  ## Development
57
97
 
58
98
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test DB=mysql` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
- require "bundler/gem_tasks"
2
- require "rake/testtask"
1
+ require 'bundler/gem_tasks'
2
+ require 'rake/testtask'
3
3
 
4
4
  Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
5
+ t.libs << 'test'
6
+ t.libs << 'lib'
7
7
  t.test_files = FileList['test/**/*_test.rb']
8
8
  end
9
9
 
10
- task :default => :test
10
+ task default: :test
@@ -4,37 +4,38 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'atomically/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = "atomically"
7
+ spec.name = 'atomically'
8
8
  spec.version = Atomically::VERSION
9
- spec.authors = ["khiav reoy"]
10
- spec.email = ["mrtmrt15xn@yahoo.com.tw"]
9
+ spec.authors = ['khiav reoy']
10
+ spec.email = ['mrtmrt15xn@yahoo.com.tw']
11
11
 
12
- spec.summary = %q{}
13
- spec.description = %q{}
14
- spec.homepage = "https://github.com/khiav223577/atomically"
15
- spec.license = "MIT"
12
+ spec.summary = ''
13
+ spec.description = ''
14
+ spec.homepage = 'https://github.com/khiav223577/atomically'
15
+ spec.license = 'MIT'
16
16
 
17
17
  # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
18
  # delete this section to allow pushing this gem to any host.
19
- #if spec.respond_to?(:metadata)
20
- # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
21
- #else
22
- # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
- #end
19
+ # if spec.respond_to?(:metadata)
20
+ # spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
21
+ # else
22
+ # raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ # end
24
24
 
25
- spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
- spec.bindir = "exe"
27
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
- spec.require_paths = ["lib"]
25
+ spec.files = `git ls-files -z`.split("\x0").reject{|f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{^exe/}){|f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
29
 
30
- spec.add_development_dependency "bundler", "~> 1.11"
31
- spec.add_development_dependency "rake", "~> 12.0"
32
- spec.add_development_dependency "sqlite3", "~> 1.3"
33
- spec.add_development_dependency "minitest", "~> 5.0"
34
- spec.add_development_dependency "mysql2", ">= 0.3"
35
- spec.add_development_dependency "pluck_all", ">= 2.0.3"
30
+ spec.add_development_dependency 'bundler', '~> 1.11'
31
+ spec.add_development_dependency 'rake', '~> 12.0'
32
+ spec.add_development_dependency 'sqlite3', '~> 1.3'
33
+ spec.add_development_dependency 'minitest', '~> 5.0'
34
+ spec.add_development_dependency 'mysql2', '>= 0.3'
35
+ spec.add_development_dependency 'pluck_all', '>= 2.0.3'
36
+ spec.add_development_dependency 'timecop', '~> 0.9.1'
36
37
 
37
- spec.add_dependency "activerecord", ">= 3"
38
- spec.add_dependency "activerecord-import", ">= 0.27.0"
39
- spec.add_dependency "rails_or", ">= 1.1.8"
38
+ spec.add_dependency 'activerecord', '>= 3'
39
+ spec.add_dependency 'activerecord-import', '>= 0.27.0'
40
+ spec.add_dependency 'rails_or', '>= 1.1.8'
40
41
  end
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
4
- require "atomically"
3
+ require 'bundler/setup'
4
+ require 'atomically'
5
5
 
6
6
  # You can add fixtures and/or initialization code here to make experimenting
7
7
  # with your gem easier. You can also use a different console, if you like.
@@ -10,5 +10,5 @@ require "atomically"
10
10
  # require "pry"
11
11
  # Pry.start
12
12
 
13
- require "irb"
13
+ require 'irb'
14
14
  IRB.start
@@ -1,15 +1,16 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem "activerecord", "~> 3.2.0"
3
+ gem 'activerecord', '~> 3.2.0'
4
4
 
5
5
  group :test do
6
6
  case ENV['DB']
7
- when "mysql" ; gem 'mysql2' , '0.3.21'
8
- when "postgres" ; gem 'pg', '~> 0.18'
7
+ when 'mysql' ; gem 'mysql2', '0.3.21'
8
+ when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
- gem "simplecov"
11
- gem "codeclimate-test-reporter", "~> 1.0.0"
10
+ gem 'simplecov'
11
+ gem 'codeclimate-test-reporter', '~> 1.0.0'
12
+ gem 'pluck_all', '>= 2.0.3'
13
+ gem 'timecop', '~> 0.9.1'
12
14
  end
13
15
 
14
- gemspec :path => "../"
15
-
16
+ gemspec path: '../'
@@ -1,15 +1,16 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem "activerecord", "~> 4.2.0"
3
+ gem 'activerecord', '~> 4.2.0'
4
4
 
5
5
  group :test do
6
6
  case ENV['DB']
7
- when "mysql" ; gem 'mysql2' , '0.3.21'
8
- when "postgres" ; gem 'pg', '~> 0.18'
7
+ when 'mysql' ; gem 'mysql2', '0.3.21'
8
+ when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
- gem "simplecov"
11
- gem "codeclimate-test-reporter", "~> 1.0.0"
10
+ gem 'simplecov'
11
+ gem 'codeclimate-test-reporter', '~> 1.0.0'
12
+ gem 'pluck_all', '>= 2.0.3'
13
+ gem 'timecop', '~> 0.9.1'
12
14
  end
13
15
 
14
- gemspec :path => "../"
15
-
16
+ gemspec path: '../'
@@ -1,15 +1,16 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem "activerecord", "~> 5.0.0"
3
+ gem 'activerecord', '~> 5.0.0'
4
4
 
5
5
  group :test do
6
6
  case ENV['DB']
7
- when "mysql" ; gem 'mysql2' , '0.3.21'
8
- when "postgres" ; gem 'pg', '~> 0.18'
7
+ when 'mysql' ; gem 'mysql2', '0.3.21'
8
+ when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
- gem "simplecov"
11
- gem "codeclimate-test-reporter", "~> 1.0.0"
10
+ gem 'simplecov'
11
+ gem 'codeclimate-test-reporter', '~> 1.0.0'
12
+ gem 'pluck_all', '>= 2.0.3'
13
+ gem 'timecop', '~> 0.9.1'
12
14
  end
13
15
 
14
- gemspec :path => "../"
15
-
16
+ gemspec path: '../'
@@ -1,15 +1,16 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem "activerecord", "~> 5.1.0"
3
+ gem 'activerecord', '~> 5.1.0'
4
4
 
5
5
  group :test do
6
6
  case ENV['DB']
7
- when "mysql" ; gem 'mysql2' , '0.3.21'
8
- when "postgres" ; gem 'pg', '~> 0.18'
7
+ when 'mysql' ; gem 'mysql2', '0.3.21'
8
+ when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
- gem "simplecov"
11
- gem "codeclimate-test-reporter", "~> 1.0.0"
10
+ gem 'simplecov'
11
+ gem 'codeclimate-test-reporter', '~> 1.0.0'
12
+ gem 'pluck_all', '>= 2.0.3'
13
+ gem 'timecop', '~> 0.9.1'
12
14
  end
13
15
 
14
- gemspec :path => "../"
15
-
16
+ gemspec path: '../'
@@ -1,15 +1,16 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem "activerecord", "~> 5.2.0"
3
+ gem 'activerecord', '~> 5.2.0'
4
4
 
5
5
  group :test do
6
6
  case ENV['DB']
7
- when "mysql" ; gem 'mysql2' , '0.5.1'
8
- when "postgres" ; gem 'pg', '~> 0.18'
7
+ when 'mysql' ; gem 'mysql2', '0.5.1'
8
+ when 'postgres' ; gem 'pg', '~> 0.18'
9
9
  end
10
- gem "simplecov"
11
- gem "codeclimate-test-reporter", "~> 1.0.0"
10
+ gem 'simplecov'
11
+ gem 'codeclimate-test-reporter', '~> 1.0.0'
12
+ gem 'pluck_all', '>= 2.0.3'
13
+ gem 'timecop', '~> 0.9.1'
12
14
  end
13
15
 
14
- gemspec :path => "../"
15
-
16
+ gemspec path: '../'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'atomically/version'
2
4
  require 'atomically/active_record/extension'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require 'atomically/query_service'
3
5
 
@@ -11,4 +13,8 @@ class ActiveRecord::Base
11
13
  def self.atomically
12
14
  Atomically::QueryService.new(self)
13
15
  end
16
+
17
+ def atomically
18
+ Atomically::QueryService.new(self.class, model: self)
19
+ end
14
20
  end
@@ -0,0 +1,17 @@
1
+ module ActiveModel
2
+ module Dirty
3
+ private
4
+
5
+ alias attributes_changed_by_setter changed_attributes # :nodoc:
6
+
7
+ # Force an attribute to have a particular "before" value
8
+ def set_attribute_was(attr, old_value)
9
+ attributes_changed_by_setter[attr] = old_value
10
+ end
11
+
12
+ # Remove changes information for the provided attributes.
13
+ def clear_attribute_changes(attributes) # :doc:
14
+ attributes_changed_by_setter.except!(*attributes)
15
+ end
16
+ end
17
+ end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
 
3
5
  class << ActiveRecord::Base
4
6
  def from(value) # For Rails 3
5
- value = "(#{value.to_sql}) subquery" if value.is_a?( ActiveRecord::Relation )
7
+ value = "(#{value.to_sql}) subquery" if value.is_a?(ActiveRecord::Relation)
6
8
  return super
7
9
  end
8
10
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
 
3
5
  class << ActiveRecord::Base
@@ -2,13 +2,16 @@
2
2
 
3
3
  require 'activerecord-import'
4
4
  require 'rails_or'
5
+ require 'atomically/update_all_scope'
6
+ require 'atomically/patches/clear_attribute_changes' if not ActiveModel::Dirty.method_defined?(:clear_attribute_changes) and not ActiveModel::Dirty.private_method_defined?(:clear_attribute_changes)
5
7
  require 'atomically/patches/none' if not ActiveRecord::Base.respond_to?(:none)
6
8
  require 'atomically/patches/from' if Gem::Version.new(ActiveRecord::VERSION::STRING) < Gem::Version.new('4.0.0')
7
9
 
8
10
  class Atomically::QueryService
9
- def initialize(klass, relation: nil)
11
+ def initialize(klass, relation: nil, model: nil)
10
12
  @klass = klass
11
13
  @relation = relation || @klass
14
+ @model = model
12
15
  end
13
16
 
14
17
  def create_or_plus(columns, data, update_columns)
@@ -36,9 +39,15 @@ class Atomically::QueryService
36
39
  .update_all(update_sqls.join(', '))
37
40
  end
38
41
 
42
+ def update(attrs, from: :not_set)
43
+ success = update_and_return_number_of_updated_rows(attrs, from) == 1
44
+ assign_without_changes(attrs) if success
45
+ return success
46
+ end
47
+
39
48
  private
40
49
 
41
- def on_duplicate_key_plus_sql( columns)
50
+ def on_duplicate_key_plus_sql(columns)
42
51
  columns.lazy.map(&method(:quote_column)).map{|s| "#{s} = #{s} + VALUES(#{s})" }.force.join(', ')
43
52
  end
44
53
 
@@ -49,4 +58,27 @@ class Atomically::QueryService
49
58
  def sanitize(value)
50
59
  @klass.connection.quote(value)
51
60
  end
61
+
62
+ def update_and_return_number_of_updated_rows(attrs, from)
63
+ model = @model
64
+ return open_update_all_scope do
65
+ update(updated_at: Time.now)
66
+ attrs.each do |column, value|
67
+ old_value = (from == :not_set ? model[column] : from)
68
+ where(column => old_value).update(column => value) if old_value != value
69
+ end
70
+ end
71
+ end
72
+
73
+ def open_update_all_scope(&block)
74
+ return 0 if @model == nil
75
+ scope = UpdateAllScope.new(model: @model)
76
+ scope.instance_exec(&block)
77
+ return scope.do_query!
78
+ end
79
+
80
+ def assign_without_changes(attributes)
81
+ @model.assign_attributes(attributes)
82
+ @model.send(:clear_attribute_changes, attributes.keys)
83
+ end
52
84
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class UpdateAllScope
4
+ def initialize(model: nil, relation: nil)
5
+ @queries = []
6
+ @relation = relation || model.class.where(id: model.id)
7
+ end
8
+
9
+ def where(*args)
10
+ @relation = @relation.where(*args)
11
+ return self
12
+ end
13
+
14
+ def update(query, *binding_values)
15
+ args = binding_values.size > 0 ? [[query, *binding_values]] : [query]
16
+ @queries << klass.send(:sanitize_sql_for_assignment, *args)
17
+ return self
18
+ end
19
+
20
+ def do_query!
21
+ return 0 if @queries.empty?
22
+ return @relation.update_all(@queries.join(','))
23
+ end
24
+
25
+ def klass
26
+ @relation.klass
27
+ end
28
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Atomically
2
- VERSION = '1.0.1'
4
+ VERSION = '1.0.2'
3
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: atomically
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - khiav reoy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-11-22 00:00:00.000000000 Z
11
+ date: 2018-11-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: 2.0.3
97
+ - !ruby/object:Gem::Dependency
98
+ name: timecop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.9.1
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.9.1
97
111
  - !ruby/object:Gem::Dependency
98
112
  name: activerecord
99
113
  requirement: !ruby/object:Gem::Requirement
@@ -163,9 +177,11 @@ files:
163
177
  - gemfiles/5.2.gemfile
164
178
  - lib/atomically.rb
165
179
  - lib/atomically/active_record/extension.rb
180
+ - lib/atomically/patches/clear_attribute_changes.rb
166
181
  - lib/atomically/patches/from.rb
167
182
  - lib/atomically/patches/none.rb
168
183
  - lib/atomically/query_service.rb
184
+ - lib/atomically/update_all_scope.rb
169
185
  - lib/atomically/version.rb
170
186
  homepage: https://github.com/khiav223577/atomically
171
187
  licenses: