upsert 0.1.2 → 0.2.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.
@@ -23,8 +23,8 @@ shared_examples_for "supports multibyte" do
23
23
  end
24
24
  it "won't overflow" do
25
25
  upsert = Upsert.new connection, :pets
26
- if upsert.buffer.respond_to?(:max_sql_bytesize)
27
- max = upsert.buffer.send(:max_sql_bytesize)
26
+ if upsert.respond_to?(:max_sql_bytesize)
27
+ max = upsert.send(:max_sql_bytesize)
28
28
  ticks = max / 3 - 2
29
29
  lambda do
30
30
  loop do
@@ -0,0 +1,41 @@
1
+ shared_examples_for "doesn't blow up on reserved words" do
2
+ # collect and uniq reserved words
3
+ reserved_words = ['mysql_reserved.txt', 'pg_reserved.txt'].map do |basename|
4
+ File.expand_path("../../misc/#{basename}", __FILE__)
5
+ end.map do |path|
6
+ IO.readlines(path)
7
+ end.flatten.map(&:chomp).select(&:present?).uniq
8
+
9
+ # make lots of AR models, each of which has 10 columns named after these words
10
+ nasties = []
11
+ reserved_words.each_slice(10) do |words|
12
+ eval %{
13
+ class Nasty#{nasties.length} < ActiveRecord::Base
14
+ end
15
+ }
16
+ nasty = Object.const_get("Nasty#{nasties.length}")
17
+ nasty.class_eval do
18
+ self.primary_key = 'fake_primary_key'
19
+ col :fake_primary_key
20
+ words.each do |word|
21
+ col word
22
+ end
23
+ end
24
+ nasties << [ nasty, words ]
25
+ end
26
+ nasties.each do |nasty, _|
27
+ nasty.auto_upgrade!
28
+ end
29
+
30
+ nasties.each do |nasty, words|
31
+ it "doesn't die on reserved words #{words.join(',')}" do
32
+ upsert = Upsert.new connection, nasty.table_name
33
+ random = rand(1e3).to_s
34
+ selector = { :fake_primary_key => random, words.first => words.first }
35
+ document = words[1..-1].inject({}) { |memo, word| memo[word] = word; memo }
36
+ assert_creates nasty, [selector.merge(document)] do
37
+ upsert.row selector, document
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,23 @@
1
+ require 'helper'
2
+ require 'mysql2'
3
+
4
+ system %{ mysql -u root -ppassword -e "DROP DATABASE IF EXISTS test_upsert; CREATE DATABASE test_upsert CHARSET utf8" }
5
+ ActiveRecord::Base.establish_connection :adapter => 'mysql2', :username => 'root', :password => 'password', :database => 'test_upsert'
6
+
7
+ require 'upsert/active_record_upsert'
8
+
9
+ describe Upsert::ActiveRecordUpsert do
10
+ before do
11
+ ActiveRecord::Base.connection.drop_table(Pet.table_name) rescue nil
12
+ Pet.auto_upgrade!
13
+ end
14
+
15
+ describe :upsert do
16
+ it "is easy to use" do
17
+ assert_creates(Pet,[{:name => 'Jerry', :good => true}]) do
18
+ Pet.upsert({:name => 'Jerry'}, :good => false)
19
+ Pet.upsert({:name => 'Jerry'}, :good => true)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -36,4 +36,6 @@ describe "upserting on mysql2" do
36
36
  it_also 'is thread-safe'
37
37
 
38
38
  it_also "doesn't mess with timezones"
39
+
40
+ it_also "doesn't blow up on reserved words"
39
41
  end
@@ -37,4 +37,6 @@ describe "upserting on postgresql" do
37
37
  it_also 'is thread-safe'
38
38
 
39
39
  it_also "doesn't mess with timezones"
40
+
41
+ it_also "doesn't blow up on reserved words"
40
42
  end
@@ -11,19 +11,20 @@ describe "upserting on sqlite" do
11
11
  @opened_connections = []
12
12
  ActiveRecord::Base.connection.drop_table(Pet.table_name) rescue nil
13
13
  Pet.auto_upgrade!
14
- @connection = new_connection
15
- end
16
- after do
17
- @opened_connections.each { |c| c.close }
18
- end
19
-
20
- def new_connection
21
- c = SQLite3::Database.open(File.expand_path('../../tmp/test.sqlite3', __FILE__))
22
- @opened_connections << c
23
- c
14
+ # @connection = new_connection
24
15
  end
16
+ # after do
17
+ # @opened_connections.each { |c| c.close }
18
+ # end
19
+
20
+ # def new_connection
21
+ # c = SQLite3::Database.open(File.expand_path('../../tmp/test.sqlite3', __FILE__))
22
+ # @opened_connections << c
23
+ # c
24
+ # end
25
25
  def connection
26
- @connection
26
+ # @connection
27
+ ActiveRecord::Base.connection
27
28
  end
28
29
 
29
30
  it_also 'is a database with an upsert trick'
@@ -39,4 +40,6 @@ describe "upserting on sqlite" do
39
40
  it_also "doesn't mess with timezones"
40
41
 
41
42
  it_also 'supports binary upserts'
43
+
44
+ # it_also "doesn't blow up on reserved words"
42
45
  end
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: 0.1.2
4
+ version: 0.2.0
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-06-19 00:00:00.000000000 Z
12
+ date: 2012-06-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sqlite3
@@ -187,24 +187,28 @@ files:
187
187
  - README.md
188
188
  - Rakefile
189
189
  - lib/upsert.rb
190
+ - lib/upsert/active_record_upsert.rb
190
191
  - lib/upsert/binary.rb
191
- - lib/upsert/buffer.rb
192
- - lib/upsert/buffer/mysql2_client.rb
193
- - lib/upsert/buffer/pg_connection.rb
194
- - lib/upsert/buffer/pg_connection/column_definition.rb
195
- - lib/upsert/buffer/sqlite3_database.rb
196
- - lib/upsert/quoter.rb
192
+ - lib/upsert/mysql2_client.rb
193
+ - lib/upsert/pg_connection.rb
194
+ - lib/upsert/pg_connection/column_definition.rb
197
195
  - lib/upsert/row.rb
196
+ - lib/upsert/sqlite3_database.rb
198
197
  - lib/upsert/version.rb
199
198
  - test/helper.rb
199
+ - test/misc/get_postgres_reserved_words.rb
200
+ - test/misc/mysql_reserved.txt
201
+ - test/misc/pg_reserved.txt
200
202
  - test/shared/binary.rb
201
203
  - test/shared/correctness.rb
202
204
  - test/shared/database.rb
203
205
  - test/shared/multibyte.rb
206
+ - test/shared/reserved_words.rb
204
207
  - test/shared/speed.rb
205
208
  - test/shared/threaded.rb
206
209
  - test/shared/timezones.rb
207
210
  - test/test_active_record_connection_adapter.rb
211
+ - test/test_active_record_upsert.rb
208
212
  - test/test_mysql2.rb
209
213
  - test/test_pg.rb
210
214
  - test/test_sqlite.rb
@@ -236,14 +240,19 @@ summary: Upsert for MySQL, PostgreSQL, and SQLite. Finally, all those SQL MERGE
236
240
  codified.
237
241
  test_files:
238
242
  - test/helper.rb
243
+ - test/misc/get_postgres_reserved_words.rb
244
+ - test/misc/mysql_reserved.txt
245
+ - test/misc/pg_reserved.txt
239
246
  - test/shared/binary.rb
240
247
  - test/shared/correctness.rb
241
248
  - test/shared/database.rb
242
249
  - test/shared/multibyte.rb
250
+ - test/shared/reserved_words.rb
243
251
  - test/shared/speed.rb
244
252
  - test/shared/threaded.rb
245
253
  - test/shared/timezones.rb
246
254
  - test/test_active_record_connection_adapter.rb
255
+ - test/test_active_record_upsert.rb
247
256
  - test/test_mysql2.rb
248
257
  - test/test_pg.rb
249
258
  - test/test_sqlite.rb
@@ -1,58 +0,0 @@
1
- class Upsert
2
- # @private
3
- class Buffer
4
- class << self
5
- def for(connection, table_name)
6
- if connection.respond_to?(:raw_connection)
7
- # deal with ActiveRecord::Base.connection or ActiveRecord::Base.connection_pool.checkout
8
- connection = connection.raw_connection
9
- end
10
- const_get(connection.class.name.gsub(/\W+/, '_')).new connection, table_name
11
- end
12
- end
13
-
14
- SINGLE_QUOTE = %{'}
15
- DOUBLE_QUOTE = %{"}
16
- BACKTICK = %{`}
17
- E_AND_SINGLE_QUOTE = %{E'}
18
- X_AND_SINGLE_QUOTE = %{x'}
19
- USEC_SPRINTF = '%06d'
20
- ISO8601_DATETIME = '%Y-%m-%d %H:%M:%S'
21
-
22
- attr_reader :connection
23
- attr_reader :table_name
24
- attr_reader :rows
25
-
26
- def initialize(connection, table_name)
27
- @connection = connection
28
- @table_name = table_name
29
- @rows = []
30
- end
31
-
32
- def async?
33
- !!@async
34
- end
35
-
36
- def async!
37
- @async = true
38
- end
39
-
40
- def sync!
41
- @async = false
42
- clear
43
- end
44
-
45
- def add(selector, document)
46
- rows << Row.new(self, selector, document)
47
- if sql = chunk
48
- execute sql
49
- end
50
- end
51
-
52
- def clear
53
- while sql = chunk
54
- execute sql
55
- end
56
- end
57
- end
58
- end
@@ -1,164 +0,0 @@
1
- class Upsert
2
- class Buffer
3
- # @private
4
- class Mysql2_Client < Buffer
5
- include Quoter
6
-
7
- def chunk
8
- return if rows.empty?
9
- all = rows.length
10
- take = all
11
- while take > 1 and probably_oversize?(take)
12
- take -= 1
13
- end
14
- if async? and take == all
15
- return
16
- end
17
- while take > 1 and oversize?(take)
18
- $stderr.puts " Length prediction via sampling failed, shrinking" if ENV['UPSERT_DEBUG'] == 'true'
19
- take -= 1
20
- end
21
- chunk = sql take
22
- while take > 1 and chunk.bytesize > max_sql_bytesize
23
- $stderr.puts " Supposedly exact bytesize guess failed, shrinking" if ENV['UPSERT_DEBUG'] == 'true'
24
- take -= 1
25
- chunk = sql take
26
- end
27
- if chunk.bytesize > max_sql_bytesize
28
- raise TooBig
29
- end
30
- $stderr.puts " Chunk (#{take}/#{chunk.bytesize}) was #{(chunk.bytesize / max_sql_bytesize.to_f * 100).round}% of the max" if ENV['UPSERT_DEBUG'] == 'true'
31
- @rows = rows.drop(take)
32
- chunk
33
- end
34
-
35
- def execute(sql)
36
- connection.query sql
37
- end
38
-
39
- def probably_oversize?(take)
40
- estimate_sql_bytesize(take) > max_sql_bytesize
41
- end
42
-
43
- def oversize?(take)
44
- sql_bytesize(take) > max_sql_bytesize
45
- end
46
-
47
- def columns
48
- @columns ||= rows.first.columns
49
- end
50
-
51
- def insert_part
52
- @insert_part ||= %{INSERT INTO "#{table_name}" (#{quote_idents(columns)}) VALUES }
53
- end
54
-
55
- def update_part
56
- @update_part ||= begin
57
- updaters = columns.map do |k|
58
- qk = quote_ident k
59
- [ qk, "VALUES(#{qk})" ].join('=')
60
- end.join(',')
61
- %{ ON DUPLICATE KEY UPDATE #{updaters}}
62
- end
63
- end
64
-
65
- # where 2 is the parens
66
- def static_sql_bytesize
67
- @static_sql_bytesize ||= insert_part.bytesize + update_part.bytesize + 2
68
- end
69
-
70
- # where 3 is parens and comma
71
- def variable_sql_bytesize(take)
72
- rows.first(take).inject(0) { |sum, row| sum + row.values_sql_bytesize + 3 }
73
- end
74
-
75
- def estimate_variable_sql_bytesize(take)
76
- p = (take / 10.0).ceil
77
- 10.0 * rows.sample(p).inject(0) { |sum, row| sum + row.values_sql_bytesize + 3 }
78
- end
79
-
80
- def sql_bytesize(take)
81
- static_sql_bytesize + variable_sql_bytesize(take)
82
- end
83
-
84
- def estimate_sql_bytesize(take)
85
- static_sql_bytesize + estimate_variable_sql_bytesize(take)
86
- end
87
-
88
- def sql(take)
89
- all_value_sql = rows.first(take).map { |row| row.values_sql }
90
- [ insert_part, '(', all_value_sql.join('),('), ')', update_part ].join
91
- end
92
-
93
- # since setting an option like :as => :hash actually persists that option to the client, don't pass any options
94
- def max_sql_bytesize
95
- @max_sql_bytesize ||= begin
96
- case (row = connection.query("SHOW VARIABLES LIKE 'max_allowed_packet'").first)
97
- when Array
98
- row[1]
99
- when Hash
100
- row['Value']
101
- else
102
- raise "Don't know what to do if connection.query returns a #{row.class}"
103
- end.to_i
104
- end
105
- end
106
-
107
- def quoted_value_bytesize(v)
108
- case v
109
- when NilClass
110
- 4
111
- when TrueClass
112
- 4
113
- when FalseClass
114
- 5
115
- when BigDecimal
116
- v.to_s('F').bytesize
117
- when Upsert::Binary
118
- v.bytesize * 2 + 3
119
- when Numeric
120
- v.to_s.bytesize
121
- when String
122
- v.bytesize + 2
123
- when Symbol
124
- v.to_s.bytesize + 2
125
- when Time, DateTime
126
- 24 + 2
127
- when Date
128
- 10 + 2
129
- else
130
- raise "not sure how to get quoted length of #{v.class}: #{v.inspect}"
131
- end
132
- end
133
-
134
- def quote_boolean(v)
135
- v ? 'TRUE' : 'FALSE'
136
- end
137
-
138
- def quote_string(v)
139
- SINGLE_QUOTE + connection.escape(v) + SINGLE_QUOTE
140
- end
141
-
142
- # This doubles the size of the representation.
143
- def quote_binary(v)
144
- X_AND_SINGLE_QUOTE + v.unpack("H*")[0] + SINGLE_QUOTE
145
- end
146
-
147
- # put raw binary straight into sql
148
- # might work if we could get the encoding issues fixed when joining together the values for the sql
149
- # alias_method :quote_binary, :quote_string
150
-
151
- def quote_time(v)
152
- quote_string v.strftime(ISO8601_DATETIME)
153
- end
154
-
155
- def quote_ident(k)
156
- BACKTICK + connection.escape(k.to_s) + BACKTICK
157
- end
158
-
159
- def quote_big_decimal(v)
160
- v.to_s('F')
161
- end
162
- end
163
- end
164
- end
@@ -1,87 +0,0 @@
1
- require 'upsert/buffer/pg_connection/column_definition'
2
-
3
- class Upsert
4
- class Buffer
5
- # @private
6
- class PG_Connection < Buffer
7
- include Quoter
8
-
9
- attr_reader :merge_function
10
-
11
- def chunk
12
- return if rows.empty?
13
- row = rows.shift
14
- unless merge_function
15
- create_merge_function row
16
- end
17
- hsh = row.to_hash
18
- ordered_args = column_definitions.map do |c|
19
- hsh[c.name]
20
- end
21
- %{SELECT #{merge_function}(#{quote_values(ordered_args)})}
22
- end
23
-
24
- def execute(sql)
25
- connection.exec sql
26
- end
27
-
28
- def quote_string(v)
29
- SINGLE_QUOTE + connection.escape_string(v) + SINGLE_QUOTE
30
- end
31
-
32
- def quote_binary(v)
33
- E_AND_SINGLE_QUOTE + connection.escape_bytea(v) + SINGLE_QUOTE
34
- end
35
-
36
- def quote_time(v)
37
- quote_string [v.strftime(ISO8601_DATETIME), sprintf(USEC_SPRINTF, v.usec)].join('.')
38
- end
39
-
40
- def quote_big_decimal(v)
41
- v.to_s('F')
42
- end
43
-
44
- def quote_boolean(v)
45
- v ? 'TRUE' : 'FALSE'
46
- end
47
-
48
- def quote_ident(k)
49
- DOUBLE_QUOTE + connection.quote_ident(k.to_s) + DOUBLE_QUOTE
50
- end
51
-
52
- def column_definitions
53
- @column_definitions ||= ColumnDefinition.all(connection, table_name)
54
- end
55
-
56
- private
57
-
58
- def create_merge_function(example_row)
59
- @merge_function = "pg_temp.merge_#{table_name}_#{Kernel.rand(1e11)}"
60
- execute <<-EOS
61
- CREATE FUNCTION #{merge_function}(#{column_definitions.map { |c| "#{c.name}_input #{c.sql_type} DEFAULT #{c.default || 'NULL'}" }.join(',') }) RETURNS VOID AS
62
- $$
63
- BEGIN
64
- LOOP
65
- -- first try to update the key
66
- UPDATE #{table_name} SET #{column_definitions.map { |c| "#{c.name} = #{c.name}_input" }.join(',')} WHERE #{example_row.selector.keys.map { |k| "#{k} = #{k}_input" }.join(' AND ') };
67
- IF found THEN
68
- RETURN;
69
- END IF;
70
- -- not there, so try to insert the key
71
- -- if someone else inserts the same key concurrently,
72
- -- we could get a unique-key failure
73
- BEGIN
74
- INSERT INTO #{table_name}(#{column_definitions.map { |c| c.name }.join(',')}) VALUES (#{column_definitions.map { |c| "#{c.name}_input" }.join(',')});
75
- RETURN;
76
- EXCEPTION WHEN unique_violation THEN
77
- -- Do nothing, and loop to try the UPDATE again.
78
- END;
79
- END LOOP;
80
- END;
81
- $$
82
- LANGUAGE plpgsql;
83
- EOS
84
- end
85
- end
86
- end
87
- end