upsert 1.2.0 → 2.0.0

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
+ 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=