upsert 1.1.6 → 1.1.7

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