upsert 1.2.0 → 2.0.0

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
+ 2.0.0 / 2013-07-24
2
+
3
+ * Breaking changes
4
+
5
+ * For Postgres Hstore, null keys are deliberately deleted - so there's no way to set "foo" => NULL - "foo" will just get deleted as a key
6
+ * Name merge functions "upsert1_2_0" instead of "upsert_"
7
+
1
8
  1.2.0 / 2013-04-04
2
9
 
3
10
  * Breaking changes
data/README.md CHANGED
@@ -66,10 +66,17 @@ Pet.upsert({:name => 'Jerry'}, :breed => 'beagle')
66
66
  Pull requests for any of these would be greatly appreciated:
67
67
 
68
68
  1. Cache JDBC PreparedStatement objects.
69
- 1. Allow "true" upserting like `upsert.row({name: 'Jerry'}, counter: Upsert.sql('counter+1'))`
70
69
  1. Sanity check my three benchmarks (four if you include activerecord-import on MySQL). Do they accurately represent optimized alternatives?
71
70
  1. Provide `require 'upsert/debug'` that will make sure you are selecting on columns that have unique indexes
72
71
  1. Test that `Upsert` instances accept arbitrary columns, even within a batch, which is what people probably expect.
72
+ 1. [@antage](https://github.com/antage)'s idea for "true" upserting: (from https://github.com/seamusabshere/upsert/issues/17)
73
+
74
+ ```ruby
75
+ selector = { id: 15 }
76
+ update_setter = { count: Upsert.sql('count + 1') }
77
+ insert_setter = { count: 1 }
78
+ upsert.row_with_two_setter(update_setter, insert_setter, selector)
79
+ ```
73
80
 
74
81
  ## Real-world usage
75
82
 
data/lib/upsert.rb CHANGED
@@ -153,10 +153,11 @@ class Upsert
153
153
  UTC_TZ = '+00:00'
154
154
  NULL_WORD = 'NULL'
155
155
  METAL_CLASS_ALIAS = {
156
- 'PGConn' => 'PG::Connection',
157
- 'org.sqlite.Conn' => 'Java::OrgSqliteConn' # for some reason, org.sqlite.Conn doesn't have a ruby class name
156
+ 'PGConn' => 'PG::Connection',
157
+ 'org.sqlite.Conn' => 'Java::OrgSqlite::Conn', # for some reason, org.sqlite.Conn doesn't have a ruby class name
158
+ 'Sequel::Postgres::Adapter' => 'PG::Connection', # Only the Postgres adapter needs an alias
158
159
  }
159
- CREATED_COL_REGEX = /\Acreated_(at|on)\Z/
160
+ CREATED_COL_REGEX = /\Acreated_(at|on)\z/
160
161
 
161
162
  # @return [Upsert::Connection]
162
163
  attr_reader :connection
@@ -6,6 +6,9 @@ class Upsert
6
6
  def all(connection, table_name)
7
7
  # activerecord-3.2.13/lib/active_record/connection_adapters/sqlite_adapter.rb
8
8
  connection.execute("PRAGMA table_info(#{connection.quote_ident(table_name)})").map do |row|#, 'SCHEMA').to_hash
9
+ if connection.metal.respond_to?(:results_as_hash) and not connection.metal.results_as_hash
10
+ row = {'name' => row[1], 'type' => row[2], 'dflt_value' => row[4]}
11
+ end
9
12
  default = case row["dflt_value"]
10
13
  when /^null$/i
11
14
  nil
@@ -4,7 +4,7 @@ require 'upsert/connection/sqlite3'
4
4
  class Upsert
5
5
  class Connection
6
6
  # @private
7
- class Java_OrgSqliteConn < Connection
7
+ class Java_OrgSqlite_Conn < Connection
8
8
  include Jdbc
9
9
  include Sqlite3
10
10
 
@@ -5,7 +5,7 @@ class Upsert
5
5
  # @private
6
6
  class SQLite3_Database < Connection
7
7
  include Sqlite3
8
-
8
+
9
9
  def execute(sql, params = nil)
10
10
  if params
11
11
  Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
@@ -8,6 +8,7 @@ class Upsert
8
8
  java.sql.Types::OTHER => 'getString', # ?! i guess unicode text?
9
9
  java.sql.Types::BINARY => 'getBlob',
10
10
  java.sql.Types::LONGVARCHAR => 'getString',
11
+ java.sql.Types::INTEGER => 'getInt',
11
12
  }
12
13
  java.sql.Types.constants.each do |type_name|
13
14
  i = java.sql.Types.const_get type_name
@@ -69,7 +70,11 @@ class Upsert
69
70
  row = {}
70
71
  column_name_and_getter.each do |i, cg|
71
72
  column_name, getter = cg
72
- row[column_name] = raw_result.send(getter, i)
73
+ if getter == 'getNull'
74
+ row[column_name] = nil
75
+ else
76
+ row[column_name] = raw_result.send(getter, i)
77
+ end
73
78
  end
74
79
  result << row
75
80
  end
@@ -4,6 +4,15 @@ class Upsert
4
4
  module Postgresql
5
5
  def bind_value(v)
6
6
  case v
7
+ when Array
8
+ # pg array escaping lifted from https://github.com/tlconnor/activerecord-postgres-array/blob/master/lib/activerecord-postgres-array/array.rb
9
+ '{' + v.map do |vv|
10
+ vv = vv.to_s
11
+ vv.gsub! /\\/, '\&\&'
12
+ vv.gsub! /'/, "''"
13
+ vv.gsub! /"/, '\"'
14
+ %{"#{vv}"}
15
+ end.join(',') + '}'
7
16
  when Hash
8
17
  # you must require 'pg_hstore' from the 'pg-hstore' gem yourself
9
18
  ::PgHstore.dump v, true
@@ -1,9 +1,11 @@
1
1
  require 'zlib'
2
+ require 'upsert/version'
2
3
 
3
4
  class Upsert
4
5
  # @private
5
6
  class MergeFunction
6
7
  MAX_NAME_LENGTH = 62
8
+ NAME_PREFIX = "upsert#{Upsert::VERSION.gsub('.', '_')}"
7
9
 
8
10
  class << self
9
11
  def execute(controller, row)
@@ -13,7 +15,7 @@ class Upsert
13
15
 
14
16
  def unique_name(table_name, selector_keys, setter_keys)
15
17
  parts = [
16
- 'upsert',
18
+ NAME_PREFIX,
17
19
  table_name,
18
20
  'SEL',
19
21
  selector_keys.join('_A_'),
@@ -8,10 +8,14 @@ class Upsert
8
8
 
9
9
  def execute(row)
10
10
  first_try = true
11
- bind_selector_values = row.selector.values.map { |v| connection.bind_value v }
12
- bind_setter_values = row.setter.values.map { |v| connection.bind_value v }
11
+ values = []
12
+ values += row.selector.values
13
+ values += row.setter.values
14
+ hstore_delete_handlers.each do |hstore_delete_handler|
15
+ values << row.hstore_delete_keys.fetch(hstore_delete_handler.name, [])
16
+ end
13
17
  begin
14
- connection.execute sql, (bind_selector_values + bind_setter_values)
18
+ connection.execute sql, values.map { |v| connection.bind_value v }
15
19
  rescue org.postgresql.util.PSQLException => pg_error
16
20
  if pg_error.message =~ /function #{name}.* does not exist/i
17
21
  if first_try
@@ -3,7 +3,7 @@ require 'upsert/merge_function/sqlite3'
3
3
  class Upsert
4
4
  class MergeFunction
5
5
  # @private
6
- class Java_OrgSqliteConn < MergeFunction
6
+ class Java_OrgSqlite_Conn < MergeFunction
7
7
  include Sqlite3
8
8
  end
9
9
  end
@@ -8,10 +8,14 @@ class Upsert
8
8
 
9
9
  def execute(row)
10
10
  first_try = true
11
- bind_selector_values = row.selector.values.map { |v| connection.bind_value v }
12
- bind_setter_values = row.setter.values.map { |v| connection.bind_value v }
11
+ values = []
12
+ values += row.selector.values
13
+ values += row.setter.values
14
+ hstore_delete_handlers.each do |hstore_delete_handler|
15
+ values << row.hstore_delete_keys.fetch(hstore_delete_handler.name, [])
16
+ end
13
17
  begin
14
- connection.execute sql, (bind_selector_values + bind_setter_values)
18
+ connection.execute sql, values.map { |v| connection.bind_value v }
15
19
  rescue PG::Error => pg_error
16
20
  if pg_error.message =~ /function #{name}.* does not exist/i
17
21
  if first_try
@@ -33,7 +37,15 @@ class Upsert
33
37
  def sql
34
38
  @sql ||= begin
35
39
  bind_params = []
36
- 1.upto(selector_keys.length + setter_keys.length) { |i| bind_params << "$#{i}" }
40
+ i = 1
41
+ (selector_keys.length + setter_keys.length).times do
42
+ bind_params << "$#{i}"
43
+ i += 1
44
+ end
45
+ hstore_delete_handlers.length.times do
46
+ bind_params << "$#{i}::text[]"
47
+ i += 1
48
+ end
37
49
  %{SELECT #{name}(#{bind_params.join(', ')})}
38
50
  end
39
51
  end
@@ -9,7 +9,7 @@ class Upsert
9
9
  module ClassMethods
10
10
  # http://stackoverflow.com/questions/733349/list-of-stored-procedures-functions-mysql-command-line
11
11
  def clear(connection)
12
- connection.execute("SHOW PROCEDURE STATUS WHERE Db = DATABASE() AND Name LIKE 'upsert_%'").map do |row|
12
+ connection.execute("SHOW PROCEDURE STATUS WHERE Db = DATABASE() AND Name LIKE '#{MergeFunction::NAME_PREFIX}%'").map do |row|
13
13
  row['Name'] || row['ROUTINE_NAME']
14
14
  end.each do |name|
15
15
  connection.execute "DROP PROCEDURE IF EXISTS #{connection.quote_ident(name)}"
@@ -31,7 +31,7 @@ class Upsert
31
31
  $BODY$
32
32
  LANGUAGE plpgsql;
33
33
  })
34
- connection.execute(%{SELECT proname FROM pg_proc WHERE proname LIKE 'upsert_%'}).each do |row|
34
+ connection.execute(%{SELECT proname FROM pg_proc WHERE proname LIKE '#{MergeFunction::NAME_PREFIX}%'}).each do |row|
35
35
  k = row['proname']
36
36
  next if k == 'upsert_delfunc'
37
37
  Upsert.logger.info %{[upsert] Dropping function #{k.inspect}}
@@ -42,21 +42,69 @@ class Upsert
42
42
 
43
43
  def sql
44
44
  @sql ||= begin
45
- bind_params = Array.new(selector_keys.length + setter_keys.length, '?')
45
+ bind_params = Array.new(selector_keys.length + setter_keys.length, '?') + Array.new(hstore_delete_handlers.length, '?::text[]')
46
46
  %{SELECT #{name}(#{bind_params.join(', ')})}
47
47
  end
48
48
  end
49
49
 
50
+ class HstoreDeleteHandler
51
+ attr_reader :merge_function
52
+ attr_reader :column_definition
53
+ def initialize(merge_function, column_definition)
54
+ @merge_function = merge_function
55
+ @column_definition = column_definition
56
+ end
57
+ def name
58
+ column_definition.name
59
+ end
60
+ def to_arg
61
+ "#{quoted_name} text[]"
62
+ end
63
+ # use coalesce(foo, '{}':text[])
64
+ def to_setter
65
+ "#{column_definition.quoted_name} = DELETE(#{column_definition.quoted_name}, #{quoted_name})"
66
+ end
67
+ def to_pgsql
68
+ %{
69
+ IF array_length(#{quoted_name}, 1) > 0 THEN
70
+ UPDATE #{merge_function.quoted_table_name} SET #{to_setter}
71
+ WHERE #{merge_function.selector_column_definitions.map(&:to_selector).join(' AND ') };
72
+ END IF;
73
+ }.gsub(/\s+/, ' ')
74
+ end
75
+ private
76
+ def quoted_name
77
+ @quoted_name ||= merge_function.connection.quote_ident "_delete_#{column_definition.name}"
78
+ end
79
+ end
80
+
81
+ def hstore_delete_handlers
82
+ @hstore_delete_handlers ||= setter_column_definitions.select do |column_definition|
83
+ column_definition.hstore?
84
+ end.map do |column_definition|
85
+ HstoreDeleteHandler.new self, column_definition
86
+ end
87
+ end
88
+
89
+ def selector_column_definitions
90
+ column_definitions.select { |cd| selector_keys.include?(cd.name) }
91
+ end
92
+
93
+ def setter_column_definitions
94
+ column_definitions.select { |cd| setter_keys.include?(cd.name) }
95
+ end
96
+
97
+ def update_column_definitions
98
+ setter_column_definitions.select { |cd| cd.name !~ CREATED_COL_REGEX }
99
+ end
100
+
50
101
  # the "canonical example" from http://www.postgresql.org/docs/9.1/static/plpgsql-control-structures.html#PLPGSQL-UPSERT-EXAMPLE
51
102
  # differentiate between selector and setter
52
103
  def create!
53
104
  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
- selector_column_definitions = column_definitions.select { |cd| selector_keys.include?(cd.name) }
55
- setter_column_definitions = column_definitions.select { |cd| setter_keys.include?(cd.name) }
56
- update_column_definitions = setter_column_definitions.select { |cd| cd.name !~ CREATED_COL_REGEX }
57
105
  first_try = true
58
106
  connection.execute(%{
59
- CREATE OR REPLACE FUNCTION #{name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg)).join(', ')}) RETURNS VOID AS
107
+ CREATE OR REPLACE FUNCTION #{name}(#{(selector_column_definitions.map(&:to_selector_arg) + setter_column_definitions.map(&:to_setter_arg) + hstore_delete_handlers.map(&:to_arg)).join(', ')}) RETURNS VOID AS
60
108
  $$
61
109
  DECLARE
62
110
  first_try INTEGER := 1;
@@ -66,6 +114,7 @@ class Upsert
66
114
  UPDATE #{quoted_table_name} SET #{update_column_definitions.map(&:to_setter).join(', ')}
67
115
  WHERE #{selector_column_definitions.map(&:to_selector).join(' AND ') };
68
116
  IF found THEN
117
+ #{hstore_delete_handlers.map(&:to_pgsql).join(' ')}
69
118
  RETURN;
70
119
  END IF;
71
120
  -- not there, so try to insert the key
@@ -73,6 +122,7 @@ class Upsert
73
122
  -- we could get a unique-key failure
74
123
  BEGIN
75
124
  INSERT INTO #{quoted_table_name}(#{setter_column_definitions.map(&:quoted_name).join(', ')}) VALUES (#{setter_column_definitions.map(&:to_setter_value).join(', ')});
125
+ #{hstore_delete_handlers.map(&:to_pgsql).join(' ')}
76
126
  RETURN;
77
127
  EXCEPTION WHEN unique_violation THEN
78
128
  -- seamusabshere 9/20/12 only retry once
data/lib/upsert/row.rb CHANGED
@@ -14,7 +14,7 @@ class Upsert
14
14
 
15
15
  attr_reader :selector
16
16
  attr_reader :setter
17
-
17
+ attr_reader :hstore_delete_keys
18
18
 
19
19
  def initialize(raw_selector, raw_setter)
20
20
  @selector = raw_selector.inject({}) do |memo, (k, v)|
@@ -22,8 +22,17 @@ class Upsert
22
22
  memo
23
23
  end
24
24
 
25
+ @hstore_delete_keys = {}
25
26
  @setter = raw_setter.inject({}) do |memo, (k, v)|
26
- memo[k.to_s] = v
27
+ k = k.to_s
28
+ if v.is_a?(::Hash)
29
+ v.each do |kk, vv|
30
+ if vv.nil?
31
+ (@hstore_delete_keys[k] ||= []) << kk
32
+ end
33
+ end
34
+ end
35
+ memo[k] = v
27
36
  memo
28
37
  end
29
38
 
@@ -1,3 +1,3 @@
1
1
  class Upsert
2
- VERSION = "1.2.0"
2
+ VERSION = '2.0.0'
3
3
  end
data/spec/hstore_spec.rb CHANGED
@@ -5,6 +5,11 @@ describe Upsert do
5
5
  require 'pg_hstore'
6
6
  Pet.connection.execute 'CREATE EXTENSION HSTORE'
7
7
  Pet.connection.execute "ALTER TABLE pets ADD COLUMN crazy HSTORE"
8
+ Pet.connection.execute "ALTER TABLE pets ADD COLUMN cool HSTORE"
9
+
10
+ before do
11
+ Pet.delete_all
12
+ end
8
13
 
9
14
  it "works for ugly text" do
10
15
  upsert = Upsert.new $conn, :pets
@@ -53,5 +58,177 @@ EOS
53
58
  crazy = PgHstore.parse row['crazy']
54
59
  crazy.should == { a: '2', whatdat: "D'ONOFRIO" }
55
60
  end
61
+
62
+ it "can nullify entire hstore" do
63
+ upsert = Upsert.new $conn, :pets
64
+
65
+ upsert.row({name: 'Bill'}, crazy: {a: 1})
66
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
67
+ crazy = PgHstore.parse row['crazy']
68
+ crazy.should == { a: '1' }
69
+
70
+ upsert.row({name: 'Bill'}, crazy: nil)
71
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
72
+ row['crazy'].should == nil
73
+ end
74
+
75
+ it "deletes keys that are nil" do
76
+ upsert = Upsert.new $conn, :pets
77
+
78
+ upsert.row({name: 'Bill'}, crazy: nil)
79
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
80
+ row['crazy'].should == nil
81
+
82
+ upsert.row({name: 'Bill'}, crazy: {a: 1})
83
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
84
+ crazy = PgHstore.parse row['crazy']
85
+ crazy.should == { a: '1' }
86
+
87
+ upsert.row({name: 'Bill'}, crazy: {})
88
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
89
+ crazy = PgHstore.parse row['crazy']
90
+ crazy.should == { a: '1' }
91
+
92
+ upsert.row({name: 'Bill'}, crazy: {a: nil})
93
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
94
+ crazy = PgHstore.parse row['crazy']
95
+ crazy.should == {}
96
+
97
+ upsert.row({name: 'Bill'}, crazy: {a: 1, b: 5})
98
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
99
+ crazy = PgHstore.parse row['crazy']
100
+ crazy.should == { a: '1', b: '5' }
101
+
102
+ upsert.row({name: 'Bill'}, crazy: {})
103
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
104
+ crazy = PgHstore.parse row['crazy']
105
+ crazy.should == { a: '1', b: '5' }
106
+
107
+ upsert.row({name: 'Bill'}, crazy: {a: nil})
108
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
109
+ crazy = PgHstore.parse row['crazy']
110
+ crazy.should == { b: '5' }
111
+
112
+ upsert.row({name: 'Bill'}, crazy: {a: 1, b: 5})
113
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
114
+ crazy = PgHstore.parse row['crazy']
115
+ crazy.should == { a: '1', b: '5' }
116
+
117
+ upsert.row({name: 'Bill'}, crazy: {a: nil, b: nil, c: 12})
118
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
119
+ crazy = PgHstore.parse row['crazy']
120
+ crazy.should == { c: '12' }
121
+ end
122
+
123
+ it "takes dangerous keys" do
124
+ upsert = Upsert.new $conn, :pets
125
+
126
+ upsert.row({name: 'Bill'}, crazy: nil)
127
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
128
+ row['crazy'].should == nil
129
+
130
+ upsert.row({name: 'Bill'}, crazy: {:'foo"bar' => 1})
131
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
132
+ crazy = PgHstore.parse row['crazy']
133
+ crazy.should == { :'foo"bar' => '1' }
134
+
135
+ upsert.row({name: 'Bill'}, crazy: {})
136
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
137
+ crazy = PgHstore.parse row['crazy']
138
+ crazy.should == { :'foo"bar' => '1' }
139
+
140
+ upsert.row({name: 'Bill'}, crazy: {:'foo"bar' => nil})
141
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
142
+ crazy = PgHstore.parse row['crazy']
143
+ crazy.should == {}
144
+
145
+ upsert.row({name: 'Bill'}, crazy: {:'foo"bar' => 1, b: 5})
146
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
147
+ crazy = PgHstore.parse row['crazy']
148
+ crazy.should == { :'foo"bar' => '1', b: '5' }
149
+
150
+ upsert.row({name: 'Bill'}, crazy: {})
151
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
152
+ crazy = PgHstore.parse row['crazy']
153
+ crazy.should == { :'foo"bar' => '1', b: '5' }
154
+
155
+ upsert.row({name: 'Bill'}, crazy: {:'foo"bar' => nil})
156
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
157
+ crazy = PgHstore.parse row['crazy']
158
+ crazy.should == { b: '5' }
159
+
160
+ upsert.row({name: 'Bill'}, crazy: {:'foo"bar' => 1, b: 5})
161
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
162
+ crazy = PgHstore.parse row['crazy']
163
+ crazy.should == { :'foo"bar' => '1', b: '5' }
164
+
165
+ upsert.row({name: 'Bill'}, crazy: {:'foo"bar' => nil, b: nil, c: 12})
166
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
167
+ crazy = PgHstore.parse row['crazy']
168
+ crazy.should == { c: '12' }
169
+ end
170
+
171
+ it "handles multiple hstores" do
172
+ upsert = Upsert.new $conn, :pets
173
+ upsert.row({name: 'Bill'}, crazy: {a: 1, b: 9}, cool: {c: 12, d: 19})
174
+ row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
175
+ crazy = PgHstore.parse row['crazy']
176
+ crazy.should == { a: '1', b: '9' }
177
+ cool = PgHstore.parse row['cool']
178
+ cool.should == { c: '12', d: '19' }
179
+ end
180
+
181
+ it "can deletes keys from multiple hstores at once" do
182
+ upsert = Upsert.new $conn, :pets
183
+
184
+ upsert.row({name: 'Bill'}, crazy: {a: 1}, cool: {5 => 9})
185
+ row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
186
+ crazy = PgHstore.parse row['crazy']
187
+ crazy.should == { a: '1' }
188
+ cool = PgHstore.parse row['cool'], false
189
+ cool.should == { '5' => '9' }
190
+
191
+ # NOOP
192
+ upsert.row({name: 'Bill'}, crazy: {}, cool: {})
193
+ row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
194
+ crazy = PgHstore.parse row['crazy']
195
+ crazy.should == { a: '1' }
196
+ cool = PgHstore.parse row['cool'], false
197
+ cool.should == { '5' => '9' }
198
+
199
+ upsert.row({name: 'Bill'}, crazy: {a: nil}, cool: {13 => 17})
200
+ row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
201
+ crazy = PgHstore.parse row['crazy']
202
+ crazy.should == {}
203
+ cool = PgHstore.parse row['cool'], false
204
+ cool.should == { '5' => '9', '13' => '17' }
205
+
206
+ upsert.row({name: 'Bill'}, crazy: {a: 1, b: 5})
207
+ row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
208
+ crazy = PgHstore.parse row['crazy']
209
+ crazy.should == { a: '1', b: '5' }
210
+
211
+ upsert.row({name: 'Bill'}, crazy: {b: nil}, cool: {5 => nil})
212
+ row = Pet.connection.select_one(%{SELECT crazy, cool FROM pets WHERE name = 'Bill'})
213
+ crazy = PgHstore.parse row['crazy']
214
+ crazy.should == {a: '1'}
215
+ cool = PgHstore.parse row['cool'], false
216
+ cool.should == {'13' => '17' }
217
+ end
218
+
219
+ it "deletes keys whether new or existing record" do
220
+ upsert = Upsert.new $conn, :pets
221
+
222
+ upsert.row({name: 'Bill'}, crazy: {z: 1, x: nil})
223
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
224
+ crazy = PgHstore.parse row['crazy'], false
225
+ crazy.should == { 'z' => '1' }
226
+
227
+ upsert.row({name: 'Bill'}, crazy: {a: 1})
228
+ row = Pet.connection.select_one(%{SELECT crazy FROM pets WHERE name = 'Bill'})
229
+ crazy = PgHstore.parse row['crazy'], false
230
+ crazy.should == { 'a' => '1', 'z' => '1' }
231
+ end
232
+
56
233
  end
57
234
  end if ENV['DB'] == 'postgresql'
data/spec/logger_spec.rb CHANGED
@@ -34,9 +34,9 @@ describe Upsert do
34
34
  when /sqlite/i
35
35
  log.should =~ /insert or ignore/i
36
36
  when /mysql/i
37
- log.should =~ /call upsert_pets_SEL_name/i
37
+ log.should =~ /call #{Upsert::MergeFunction::NAME_PREFIX}_pets_SEL_name/i
38
38
  when /p.*g/i
39
- log.should =~ /select upsert_pets_SEL_name/i
39
+ log.should =~ /select #{Upsert::MergeFunction::NAME_PREFIX}_pets_SEL_name/i
40
40
  else
41
41
  raise "not sure"
42
42
  end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'sequel'
3
+
4
+ describe Upsert do
5
+ describe "Plays nice with Sequel" do
6
+ config = ActiveRecord::Base.connection.instance_variable_get(:@config)
7
+ case
8
+ when 'postgresql' == config[:adapter]; config[:adapter] = 'postgres'
9
+ when 'sqlite3' == config[:adapter]; config[:adapter] = 'sqlite'
10
+ end
11
+
12
+ it "Doesn't explode on connection" do
13
+ expect { DB = Sequel.connect config }.to_not raise_error
14
+ end
15
+
16
+ it "Doesn't explode when using DB.pool.hold" do
17
+ DB.pool.hold do |conn|
18
+ expect {
19
+ upsert = Upsert.new(conn, :pets)
20
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
21
+ upsert.row({:name => 'Jerry'}, {:gender => 'male'})
22
+ end
23
+ }.to_not raise_error
24
+ end
25
+ end
26
+
27
+ it "Doesn't explode when using DB.synchronize" do
28
+ DB.synchronize do |conn|
29
+ expect {
30
+ upsert = Upsert.new(conn, :pets)
31
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
32
+ upsert.row({:name => 'Jerry'}, {:gender => 'male'})
33
+ end
34
+ }.to_not raise_error
35
+ end
36
+ end
37
+ end
38
+ end
data/spec/spec_helper.rb CHANGED
@@ -25,7 +25,8 @@ class RawConnectionFactory
25
25
  CONFIG = "jdbc:postgresql://localhost/#{DATABASE}?user=#{CURRENT_USER}"
26
26
  require 'jdbc/postgres'
27
27
  # http://thesymanual.wordpress.com/2011/02/21/connecting-jruby-to-postgresql-with-jdbc-postgre-api/
28
- java.sql.DriverManager.register_driver org.postgresql.Driver.new
28
+ Jdbc::Postgres.load_driver
29
+ # java.sql.DriverManager.register_driver org.postgresql.Driver.new
29
30
  def new_connection
30
31
  java.sql.DriverManager.get_connection CONFIG
31
32
  end
@@ -44,7 +45,8 @@ class RawConnectionFactory
44
45
  if RUBY_PLATFORM == 'java'
45
46
  CONFIG = "jdbc:mysql://127.0.0.1/#{DATABASE}?user=root&password=password"
46
47
  require 'jdbc/mysql'
47
- java.sql.DriverManager.register_driver com.mysql.jdbc.Driver.new
48
+ Jdbc::MySQL.load_driver
49
+ # java.sql.DriverManager.register_driver com.mysql.jdbc.Driver.new
48
50
  def new_connection
49
51
  java.sql.DriverManager.get_connection CONFIG
50
52
  end
@@ -58,7 +60,11 @@ class RawConnectionFactory
58
60
  ActiveRecord::Base.establish_connection "#{RUBY_PLATFORM == 'java' ? 'mysql' : 'mysql2'}://root:password@127.0.0.1/#{DATABASE}"
59
61
 
60
62
  when 'sqlite3'
63
+ CONFIG = { :adapter => 'sqlite3', :database => 'file::memory:?cache=shared' }
61
64
  if RUBY_PLATFORM == 'java'
65
+ # CONFIG = 'jdbc:sqlite://test.sqlite3'
66
+ require 'jdbc/sqlite3'
67
+ Jdbc::SQLite3.load_driver
62
68
  def new_connection
63
69
  ActiveRecord::Base.connection.raw_connection.connection
64
70
  end
@@ -68,7 +74,10 @@ class RawConnectionFactory
68
74
  ActiveRecord::Base.connection.raw_connection
69
75
  end
70
76
  end
71
- ActiveRecord::Base.establish_connection :adapter => 'sqlite3', :database => ':memory:'
77
+ ActiveRecord::Base.establish_connection CONFIG
78
+
79
+ when 'postgres'
80
+ raise "please use DB=postgresql NOT postgres"
72
81
 
73
82
  else
74
83
  raise "not supported"
data/upsert.gemspec CHANGED
@@ -22,13 +22,14 @@ Gem::Specification.new do |gem|
22
22
  gem.add_development_dependency 'rspec-expectations'
23
23
  gem.add_development_dependency 'rspec-mocks'
24
24
 
25
- gem.add_development_dependency 'activerecord'
25
+ gem.add_development_dependency 'activerecord', '~>3'
26
26
  gem.add_development_dependency 'active_record_inline_schema'
27
27
  gem.add_development_dependency 'faker'
28
28
  gem.add_development_dependency 'yard'
29
29
  gem.add_development_dependency 'activerecord-import'
30
30
  gem.add_development_dependency 'pry'
31
31
  gem.add_development_dependency 'pg-hstore', ">=1.1.3"
32
+ gem.add_development_dependency 'sequel'
32
33
 
33
34
  unless RUBY_VERSION >= '1.9'
34
35
  gem.add_development_dependency 'orderedhash'
metadata CHANGED
@@ -1,18 +1,20 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: upsert
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 2.0.0
5
+ prerelease:
5
6
  platform: ruby
6
7
  authors:
7
8
  - Seamus Abshere
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2013-04-04 00:00:00.000000000 Z
12
+ date: 2013-07-24 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: rspec-core
15
16
  requirement: !ruby/object:Gem::Requirement
17
+ none: false
16
18
  requirements:
17
19
  - - ! '>='
18
20
  - !ruby/object:Gem::Version
@@ -20,6 +22,7 @@ dependencies:
20
22
  type: :development
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
23
26
  requirements:
24
27
  - - ! '>='
25
28
  - !ruby/object:Gem::Version
@@ -27,6 +30,7 @@ dependencies:
27
30
  - !ruby/object:Gem::Dependency
28
31
  name: rspec-expectations
29
32
  requirement: !ruby/object:Gem::Requirement
33
+ none: false
30
34
  requirements:
31
35
  - - ! '>='
32
36
  - !ruby/object:Gem::Version
@@ -34,6 +38,7 @@ dependencies:
34
38
  type: :development
35
39
  prerelease: false
36
40
  version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
37
42
  requirements:
38
43
  - - ! '>='
39
44
  - !ruby/object:Gem::Version
@@ -41,6 +46,7 @@ dependencies:
41
46
  - !ruby/object:Gem::Dependency
42
47
  name: rspec-mocks
43
48
  requirement: !ruby/object:Gem::Requirement
49
+ none: false
44
50
  requirements:
45
51
  - - ! '>='
46
52
  - !ruby/object:Gem::Version
@@ -48,6 +54,7 @@ dependencies:
48
54
  type: :development
49
55
  prerelease: false
50
56
  version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
51
58
  requirements:
52
59
  - - ! '>='
53
60
  - !ruby/object:Gem::Version
@@ -55,20 +62,23 @@ dependencies:
55
62
  - !ruby/object:Gem::Dependency
56
63
  name: activerecord
57
64
  requirement: !ruby/object:Gem::Requirement
65
+ none: false
58
66
  requirements:
59
- - - ! '>='
67
+ - - ~>
60
68
  - !ruby/object:Gem::Version
61
- version: '0'
69
+ version: '3'
62
70
  type: :development
63
71
  prerelease: false
64
72
  version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
65
74
  requirements:
66
- - - ! '>='
75
+ - - ~>
67
76
  - !ruby/object:Gem::Version
68
- version: '0'
77
+ version: '3'
69
78
  - !ruby/object:Gem::Dependency
70
79
  name: active_record_inline_schema
71
80
  requirement: !ruby/object:Gem::Requirement
81
+ none: false
72
82
  requirements:
73
83
  - - ! '>='
74
84
  - !ruby/object:Gem::Version
@@ -76,6 +86,7 @@ dependencies:
76
86
  type: :development
77
87
  prerelease: false
78
88
  version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
79
90
  requirements:
80
91
  - - ! '>='
81
92
  - !ruby/object:Gem::Version
@@ -83,6 +94,7 @@ dependencies:
83
94
  - !ruby/object:Gem::Dependency
84
95
  name: faker
85
96
  requirement: !ruby/object:Gem::Requirement
97
+ none: false
86
98
  requirements:
87
99
  - - ! '>='
88
100
  - !ruby/object:Gem::Version
@@ -90,6 +102,7 @@ dependencies:
90
102
  type: :development
91
103
  prerelease: false
92
104
  version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
93
106
  requirements:
94
107
  - - ! '>='
95
108
  - !ruby/object:Gem::Version
@@ -97,6 +110,7 @@ dependencies:
97
110
  - !ruby/object:Gem::Dependency
98
111
  name: yard
99
112
  requirement: !ruby/object:Gem::Requirement
113
+ none: false
100
114
  requirements:
101
115
  - - ! '>='
102
116
  - !ruby/object:Gem::Version
@@ -104,6 +118,7 @@ dependencies:
104
118
  type: :development
105
119
  prerelease: false
106
120
  version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
107
122
  requirements:
108
123
  - - ! '>='
109
124
  - !ruby/object:Gem::Version
@@ -111,6 +126,7 @@ dependencies:
111
126
  - !ruby/object:Gem::Dependency
112
127
  name: activerecord-import
113
128
  requirement: !ruby/object:Gem::Requirement
129
+ none: false
114
130
  requirements:
115
131
  - - ! '>='
116
132
  - !ruby/object:Gem::Version
@@ -118,6 +134,7 @@ dependencies:
118
134
  type: :development
119
135
  prerelease: false
120
136
  version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
121
138
  requirements:
122
139
  - - ! '>='
123
140
  - !ruby/object:Gem::Version
@@ -125,6 +142,7 @@ dependencies:
125
142
  - !ruby/object:Gem::Dependency
126
143
  name: pry
127
144
  requirement: !ruby/object:Gem::Requirement
145
+ none: false
128
146
  requirements:
129
147
  - - ! '>='
130
148
  - !ruby/object:Gem::Version
@@ -132,6 +150,7 @@ dependencies:
132
150
  type: :development
133
151
  prerelease: false
134
152
  version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
135
154
  requirements:
136
155
  - - ! '>='
137
156
  - !ruby/object:Gem::Version
@@ -139,6 +158,7 @@ dependencies:
139
158
  - !ruby/object:Gem::Dependency
140
159
  name: pg-hstore
141
160
  requirement: !ruby/object:Gem::Requirement
161
+ none: false
142
162
  requirements:
143
163
  - - ! '>='
144
164
  - !ruby/object:Gem::Version
@@ -146,13 +166,31 @@ dependencies:
146
166
  type: :development
147
167
  prerelease: false
148
168
  version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
149
170
  requirements:
150
171
  - - ! '>='
151
172
  - !ruby/object:Gem::Version
152
173
  version: 1.1.3
174
+ - !ruby/object:Gem::Dependency
175
+ name: sequel
176
+ requirement: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ type: :development
183
+ prerelease: false
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
153
190
  - !ruby/object:Gem::Dependency
154
191
  name: sqlite3
155
192
  requirement: !ruby/object:Gem::Requirement
193
+ none: false
156
194
  requirements:
157
195
  - - ! '>='
158
196
  - !ruby/object:Gem::Version
@@ -160,6 +198,7 @@ dependencies:
160
198
  type: :development
161
199
  prerelease: false
162
200
  version_requirements: !ruby/object:Gem::Requirement
201
+ none: false
163
202
  requirements:
164
203
  - - ! '>='
165
204
  - !ruby/object:Gem::Version
@@ -167,6 +206,7 @@ dependencies:
167
206
  - !ruby/object:Gem::Dependency
168
207
  name: mysql2
169
208
  requirement: !ruby/object:Gem::Requirement
209
+ none: false
170
210
  requirements:
171
211
  - - ! '>='
172
212
  - !ruby/object:Gem::Version
@@ -174,6 +214,7 @@ dependencies:
174
214
  type: :development
175
215
  prerelease: false
176
216
  version_requirements: !ruby/object:Gem::Requirement
217
+ none: false
177
218
  requirements:
178
219
  - - ! '>='
179
220
  - !ruby/object:Gem::Version
@@ -181,6 +222,7 @@ dependencies:
181
222
  - !ruby/object:Gem::Dependency
182
223
  name: pg
183
224
  requirement: !ruby/object:Gem::Requirement
225
+ none: false
184
226
  requirements:
185
227
  - - ! '>='
186
228
  - !ruby/object:Gem::Version
@@ -188,6 +230,7 @@ dependencies:
188
230
  type: :development
189
231
  prerelease: false
190
232
  version_requirements: !ruby/object:Gem::Requirement
233
+ none: false
191
234
  requirements:
192
235
  - - ! '>='
193
236
  - !ruby/object:Gem::Version
@@ -195,6 +238,7 @@ dependencies:
195
238
  - !ruby/object:Gem::Dependency
196
239
  name: redcarpet
197
240
  requirement: !ruby/object:Gem::Requirement
241
+ none: false
198
242
  requirements:
199
243
  - - ! '>='
200
244
  - !ruby/object:Gem::Version
@@ -202,6 +246,7 @@ dependencies:
202
246
  type: :development
203
247
  prerelease: false
204
248
  version_requirements: !ruby/object:Gem::Requirement
249
+ none: false
205
250
  requirements:
206
251
  - - ! '>='
207
252
  - !ruby/object:Gem::Version
@@ -209,6 +254,7 @@ dependencies:
209
254
  - !ruby/object:Gem::Dependency
210
255
  name: rake
211
256
  requirement: !ruby/object:Gem::Requirement
257
+ none: false
212
258
  requirements:
213
259
  - - ! '>='
214
260
  - !ruby/object:Gem::Version
@@ -216,6 +262,7 @@ dependencies:
216
262
  type: :development
217
263
  prerelease: false
218
264
  version_requirements: !ruby/object:Gem::Requirement
265
+ none: false
219
266
  requirements:
220
267
  - - ! '>='
221
268
  - !ruby/object:Gem::Version
@@ -245,7 +292,7 @@ files:
245
292
  - lib/upsert/connection.rb
246
293
  - lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb
247
294
  - lib/upsert/connection/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb
248
- - lib/upsert/connection/Java_OrgSqliteConn.rb
295
+ - lib/upsert/connection/Java_OrgSqlite_Conn.rb
249
296
  - lib/upsert/connection/Mysql2_Client.rb
250
297
  - lib/upsert/connection/PG_Connection.rb
251
298
  - lib/upsert/connection/SQLite3_Database.rb
@@ -255,7 +302,7 @@ files:
255
302
  - lib/upsert/merge_function.rb
256
303
  - lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb
257
304
  - lib/upsert/merge_function/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb
258
- - lib/upsert/merge_function/Java_OrgSqliteConn.rb
305
+ - lib/upsert/merge_function/Java_OrgSqlite_Conn.rb
259
306
  - lib/upsert/merge_function/Mysql2_Client.rb
260
307
  - lib/upsert/merge_function/PG_Connection.rb
261
308
  - lib/upsert/merge_function/SQLite3_Database.rb
@@ -277,6 +324,7 @@ files:
277
324
  - spec/multibyte_spec.rb
278
325
  - spec/precision_spec.rb
279
326
  - spec/reserved_words_spec.rb
327
+ - spec/sequel_spec.rb
280
328
  - spec/spec_helper.rb
281
329
  - spec/speed_spec.rb
282
330
  - spec/threaded_spec.rb
@@ -285,26 +333,27 @@ files:
285
333
  - upsert.gemspec
286
334
  homepage: https://github.com/seamusabshere/upsert
287
335
  licenses: []
288
- metadata: {}
289
336
  post_install_message:
290
337
  rdoc_options: []
291
338
  require_paths:
292
339
  - lib
293
340
  required_ruby_version: !ruby/object:Gem::Requirement
341
+ none: false
294
342
  requirements:
295
343
  - - ! '>='
296
344
  - !ruby/object:Gem::Version
297
345
  version: '0'
298
346
  required_rubygems_version: !ruby/object:Gem::Requirement
347
+ none: false
299
348
  requirements:
300
349
  - - ! '>='
301
350
  - !ruby/object:Gem::Version
302
351
  version: '0'
303
352
  requirements: []
304
353
  rubyforge_project:
305
- rubygems_version: 2.0.3
354
+ rubygems_version: 1.8.25
306
355
  signing_key:
307
- specification_version: 4
356
+ specification_version: 3
308
357
  summary: Make it easy to upsert on MySQL, PostgreSQL, and SQLite3. Transparently creates
309
358
  merge functions for MySQL and PostgreSQL; on SQLite3, uses INSERT OR IGNORE.
310
359
  test_files:
@@ -321,6 +370,7 @@ test_files:
321
370
  - spec/multibyte_spec.rb
322
371
  - spec/precision_spec.rb
323
372
  - spec/reserved_words_spec.rb
373
+ - spec/sequel_spec.rb
324
374
  - spec/spec_helper.rb
325
375
  - spec/speed_spec.rb
326
376
  - spec/threaded_spec.rb
checksums.yaml DELETED
@@ -1,15 +0,0 @@
1
- ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- YTE3MDYwZTVkNjkyOWUwOTc4MmNjYTFmOWI4OGNhZjFkYjk5ZjE0YQ==
5
- data.tar.gz: !binary |-
6
- MzY5ZDAzZGViY2E5NzQ4NThjOTJiNTg3ZmQ1NTEyMjhmMjRjMjVlNw==
7
- !binary "U0hBNTEy":
8
- metadata.gz: !binary |-
9
- MzZjZjlmYmI4Y2QxZWMxMDAwYTcwMjUwMDc1YTZhMzU3MTA0YTY4MWQzNWE4
10
- OWMyZDBlOTVmZGNmNTQzZTQ5MzM4NjgxNTFkNzA0MTNhYzIyYWI0N2VmYTY2
11
- MDA1YjQ4NjY3Mzg5NWVjMDQ0YmViMzA2NmM1NDZjNmIxNWM2MGM=
12
- data.tar.gz: !binary |-
13
- MDM2ZTY0YTRlYTVhMmExZGZmMTAyYTEzMjc5YTFmYWVjNjY2MDIxZTg2ODMw
14
- YzQ2OWExNmNhNmRjZmQ2YTAxNjdjOTY3NWRhZDQwMjZjNzA2Mzg0NDY0NjBj
15
- ZDUzMzhhZjQzNTY5YjE5YmU3NzI0OTkzMTNlZjdmZDg0YjQ4NDE=