upsert 1.1.6 → 1.1.7

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,3 +1,10 @@
1
+ 1.1.7 / 2013-01-15
2
+
3
+ * Enhancements
4
+
5
+ * :assume_function_exists option to avoid creating same merge function over and over
6
+ * Don't die on first occurrence of "tuple concurrently updated"
7
+
1
8
  1.1.6 / 2012-12-20
2
9
 
3
10
  * Bug fixes
data/README.md CHANGED
@@ -64,7 +64,7 @@ Pet.upsert({:name => 'Jerry'}, :breed => 'beagle')
64
64
  Pull requests for any of these would be greatly appreciated:
65
65
 
66
66
  1. Cache JDBC PreparedStatement objects.
67
- 1. Optional "assume-merge-function-exists" mode. Currently, the fact that a merge function has been created is memoized per-process.
67
+ 1. Allow "true" upserting like `upsert.row({name: 'Jerry'}, counter: Upsert.sql('counter+1'))`
68
68
  1. Sanity check my three benchmarks (four if you include activerecord-import on MySQL). Do they accurately represent optimized alternatives?
69
69
  1. Provide `require 'upsert/debug'` that will make sure you are selecting on columns that have unique indexes
70
70
  1. Test that `Upsert` instances accept arbitrary columns, even within a batch, which is what people probably expect.
data/lib/upsert.rb CHANGED
@@ -67,8 +67,8 @@ class Upsert
67
67
  # upsert.row({:name => 'Jerry'}, :breed => 'beagle')
68
68
  # upsert.row({:name => 'Pierre'}, :breed => 'tabby')
69
69
  # end
70
- def batch(connection, table_name)
71
- upsert = new connection, table_name
70
+ def batch(connection, table_name, options = {})
71
+ upsert = new connection, table_name, options
72
72
  yield upsert
73
73
  end
74
74
 
@@ -172,9 +172,16 @@ class Upsert
172
172
  # @private
173
173
  attr_reader :adapter
174
174
 
175
+ # @private
176
+ def assume_function_exists?
177
+ @assume_function_exists
178
+ end
179
+
175
180
  # @param [Mysql2::Client,Sqlite3::Database,PG::Connection,#metal] connection A supported database connection.
176
181
  # @param [String,Symbol] table_name The name of the table into which you will be upserting.
177
- def initialize(connection, table_name)
182
+ # @param [Hash] options
183
+ # @option options [TrueClass,FalseClass] :assume_function_exists (false) Assume the function has already been defined correctly by another process.
184
+ def initialize(connection, table_name, options = {})
178
185
  @table_name = table_name.to_s
179
186
  metal = Upsert.metal connection
180
187
  @flavor = Upsert.flavor metal
@@ -185,6 +192,7 @@ class Upsert
185
192
  end
186
193
  @connection = Connection.const_get(adapter).new self, metal
187
194
  @merge_function_class = MergeFunction.const_get adapter
195
+ @assume_function_exists = options.fetch :assume_function_exists, false
188
196
  end
189
197
 
190
198
  # Upsert a row given a selector and a setter.
@@ -34,7 +34,7 @@ class Upsert
34
34
  selector_keys = row.selector.keys
35
35
  setter_keys = row.setter.keys
36
36
  key = [controller.table_name, selector_keys, setter_keys]
37
- @lookup[key] ||= new(controller, selector_keys, setter_keys)
37
+ @lookup[key] ||= new(controller, selector_keys, setter_keys, controller.assume_function_exists?)
38
38
  end
39
39
  end
40
40
 
@@ -42,11 +42,11 @@ class Upsert
42
42
  attr_reader :selector_keys
43
43
  attr_reader :setter_keys
44
44
 
45
- def initialize(controller, selector_keys, setter_keys)
45
+ def initialize(controller, selector_keys, setter_keys, assume_function_exists)
46
46
  @controller = controller
47
47
  @selector_keys = selector_keys
48
48
  @setter_keys = setter_keys
49
- create!
49
+ create! unless assume_function_exists
50
50
  end
51
51
 
52
52
  def name
@@ -53,6 +53,7 @@ class Upsert
53
53
  Upsert.logger.info "[upsert] Creating or replacing database function #{name.inspect} on table #{table_name.inspect} for selector #{selector_keys.map(&:inspect).join(', ')} and setter #{setter_keys.map(&:inspect).join(', ')}"
54
54
  selector_column_definitions = column_definitions.select { |cd| selector_keys.include?(cd.name) }
55
55
  setter_column_definitions = column_definitions.select { |cd| setter_keys.include?(cd.name) }
56
+ first_try = true
56
57
  connection.execute(%{
57
58
  CREATE OR REPLACE FUNCTION #{name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg)).join(', ')}) RETURNS VOID AS
58
59
  $$
@@ -86,6 +87,13 @@ class Upsert
86
87
  $$
87
88
  LANGUAGE plpgsql;
88
89
  })
90
+ rescue
91
+ if first_try and $!.message =~ /tuple concurrently updated/
92
+ first_try = false
93
+ retry
94
+ else
95
+ raise $!
96
+ end
89
97
  end
90
98
  end
91
99
  end
@@ -1,3 +1,3 @@
1
1
  class Upsert
2
- VERSION = "1.1.6"
2
+ VERSION = "1.1.7"
3
3
  end
@@ -2,6 +2,7 @@ require 'spec_helper'
2
2
  require 'stringio'
3
3
  describe Upsert do
4
4
  describe 'database functions' do
5
+
5
6
  it "re-uses merge functions across connections" do
6
7
  begin
7
8
  io = StringIO.new
@@ -30,5 +31,30 @@ describe Upsert do
30
31
  Upsert.logger = old_logger
31
32
  end
32
33
  end
34
+
35
+ it "assumes function exists if told to" do
36
+ begin
37
+ io = StringIO.new
38
+ old_logger = Upsert.logger
39
+ Upsert.logger = Logger.new io, Logger::INFO
40
+
41
+ # clear
42
+ Upsert.clear_database_functions($conn_factory.new_connection)
43
+
44
+ # tries, "went missing", creates
45
+ Upsert.new($conn_factory.new_connection, :pets, :assume_function_exists => true).row :name => 'hello'
46
+
47
+ # just works
48
+ Upsert.new($conn_factory.new_connection, :pets, :assume_function_exists => true).row :name => 'hello'
49
+
50
+ io.rewind
51
+ lines = io.read.split("\n")
52
+ lines.grep(/went missing/).length.should == 1
53
+ lines.grep(/Creating or replacing/).length.should == 1
54
+ ensure
55
+ Upsert.logger = old_logger
56
+ end
57
+ end
58
+
33
59
  end
34
60
  end if %w{ postgresql mysql }.include?(ENV['DB'])
data/spec/hstore_spec.rb CHANGED
@@ -1,10 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
1
2
  require 'spec_helper'
2
3
  describe Upsert do
3
4
  describe 'hstore on pg' do
5
+ require 'pg_hstore'
6
+ Pet.connection.execute 'CREATE EXTENSION HSTORE'
7
+ Pet.connection.execute "ALTER TABLE pets ADD COLUMN crazy HSTORE"
8
+
9
+ it "works for ugly text" do
10
+ upsert = Upsert.new $conn, :pets
11
+ uggy = <<-EOS
12
+ {"results":[{"locations":[],"providedLocation":{"location":"3001 STRATTON WAY, MADISON, WI 53719 UNITED STATES"}}],"options":{"ignoreLatLngInput":true,"maxResults":1,"thumbMaps":false},"info":{"copyright":{"text":"© 2012 MapQuest, Inc.","imageUrl":"http://api.mqcdn.com/res/mqlogo.gif","imageAltText":"© 2012 MapQuest, Inc."},"statuscode":0,"messages":[]}}
13
+ EOS
14
+ upsert.row({:name => 'Uggy'}, crazy: {uggy: uggy})
15
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Uggy'})
16
+ crazy = PgHstore.parse row['crazy']
17
+ crazy.should == { uggy: uggy }
18
+ end
19
+
4
20
  it "just works" do
5
- require 'pg_hstore'
6
- Pet.connection.execute 'CREATE EXTENSION HSTORE'
7
- Pet.connection.execute "ALTER TABLE pets ADD COLUMN crazy HSTORE"
8
21
  upsert = Upsert.new $conn, :pets
9
22
 
10
23
  upsert.row({name: 'Bill'}, crazy: nil)
data/upsert.gemspec CHANGED
@@ -28,7 +28,7 @@ Gem::Specification.new do |gem|
28
28
  gem.add_development_dependency 'yard'
29
29
  gem.add_development_dependency 'activerecord-import'
30
30
  gem.add_development_dependency 'pry'
31
- gem.add_development_dependency 'pg-hstore', ">=1.1.2"
31
+ gem.add_development_dependency 'pg-hstore', ">=1.1.3"
32
32
 
33
33
  unless RUBY_VERSION >= '1.9'
34
34
  gem.add_development_dependency 'orderedhash'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: upsert
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.6
4
+ version: 1.1.7
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-12-20 00:00:00.000000000 Z
12
+ date: 2013-01-15 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec-core
@@ -162,7 +162,7 @@ dependencies:
162
162
  requirements:
163
163
  - - ! '>='
164
164
  - !ruby/object:Gem::Version
165
- version: 1.1.2
165
+ version: 1.1.3
166
166
  type: :development
167
167
  prerelease: false
168
168
  version_requirements: !ruby/object:Gem::Requirement
@@ -170,7 +170,7 @@ dependencies:
170
170
  requirements:
171
171
  - - ! '>='
172
172
  - !ruby/object:Gem::Version
173
- version: 1.1.2
173
+ version: 1.1.3
174
174
  - !ruby/object:Gem::Dependency
175
175
  name: sqlite3
176
176
  requirement: !ruby/object:Gem::Requirement