upsert 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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