upsert 2.9.9-universal-java-11

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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.ruby-version +1 -0
  4. data/.standard.yml +1 -0
  5. data/.travis.yml +63 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG +265 -0
  8. data/Gemfile +16 -0
  9. data/LICENSE +24 -0
  10. data/README.md +411 -0
  11. data/Rakefile +54 -0
  12. data/lib/upsert.rb +284 -0
  13. data/lib/upsert/active_record_upsert.rb +12 -0
  14. data/lib/upsert/binary.rb +8 -0
  15. data/lib/upsert/column_definition.rb +79 -0
  16. data/lib/upsert/column_definition/mysql.rb +24 -0
  17. data/lib/upsert/column_definition/postgresql.rb +66 -0
  18. data/lib/upsert/column_definition/sqlite3.rb +34 -0
  19. data/lib/upsert/connection.rb +37 -0
  20. data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +31 -0
  21. data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
  22. data/lib/upsert/connection/Java_OrgSqlite_Conn.rb +17 -0
  23. data/lib/upsert/connection/Mysql2_Client.rb +76 -0
  24. data/lib/upsert/connection/PG_Connection.rb +35 -0
  25. data/lib/upsert/connection/SQLite3_Database.rb +28 -0
  26. data/lib/upsert/connection/jdbc.rb +105 -0
  27. data/lib/upsert/connection/postgresql.rb +24 -0
  28. data/lib/upsert/connection/sqlite3.rb +19 -0
  29. data/lib/upsert/merge_function.rb +73 -0
  30. data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
  31. data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc_PgConnection.rb +27 -0
  32. data/lib/upsert/merge_function/Java_OrgSqlite_Conn.rb +10 -0
  33. data/lib/upsert/merge_function/Mysql2_Client.rb +36 -0
  34. data/lib/upsert/merge_function/PG_Connection.rb +26 -0
  35. data/lib/upsert/merge_function/SQLite3_Database.rb +10 -0
  36. data/lib/upsert/merge_function/mysql.rb +66 -0
  37. data/lib/upsert/merge_function/postgresql.rb +365 -0
  38. data/lib/upsert/merge_function/sqlite3.rb +43 -0
  39. data/lib/upsert/row.rb +59 -0
  40. data/lib/upsert/version.rb +3 -0
  41. data/spec/active_record_upsert_spec.rb +26 -0
  42. data/spec/binary_spec.rb +21 -0
  43. data/spec/correctness_spec.rb +190 -0
  44. data/spec/database_functions_spec.rb +106 -0
  45. data/spec/database_spec.rb +121 -0
  46. data/spec/hstore_spec.rb +249 -0
  47. data/spec/jruby_spec.rb +9 -0
  48. data/spec/logger_spec.rb +52 -0
  49. data/spec/misc/get_postgres_reserved_words.rb +12 -0
  50. data/spec/misc/mysql_reserved.txt +226 -0
  51. data/spec/misc/pg_reserved.txt +742 -0
  52. data/spec/multibyte_spec.rb +27 -0
  53. data/spec/postgresql_spec.rb +94 -0
  54. data/spec/precision_spec.rb +11 -0
  55. data/spec/reserved_words_spec.rb +50 -0
  56. data/spec/sequel_spec.rb +57 -0
  57. data/spec/spec_helper.rb +417 -0
  58. data/spec/speed_spec.rb +44 -0
  59. data/spec/threaded_spec.rb +57 -0
  60. data/spec/timezones_spec.rb +58 -0
  61. data/spec/type_safety_spec.rb +12 -0
  62. data/travis/install_postgres.sh +18 -0
  63. data/travis/run_docker_db.sh +20 -0
  64. data/travis/tune_mysql.sh +7 -0
  65. data/upsert-java.gemspec +13 -0
  66. data/upsert.gemspec +11 -0
  67. data/upsert.gemspec.common +107 -0
  68. metadata +373 -0
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'spec_helper'
3
+ describe Upsert do
4
+ describe "supports multibyte" do
5
+ it "works one-by-one" do
6
+ assert_creates(Pet, [{:name => 'I♥NY', :gender => 'périferôl'}]) do
7
+ upsert = Upsert.new $conn, :pets
8
+ upsert.row({:name => 'I♥NY'}, {:gender => 'périferôl'})
9
+ end
10
+ end
11
+ it "works serially" do
12
+ assert_creates(Pet, [{:name => 'I♥NY', :gender => 'jÚrgen'}]) do
13
+ upsert = Upsert.new $conn, :pets
14
+ upsert.row({:name => 'I♥NY'}, {:gender => 'périferôl'})
15
+ upsert.row({:name => 'I♥NY'}, {:gender => 'jÚrgen'})
16
+ end
17
+ end
18
+ it "works batch" do
19
+ assert_creates(Pet, [{:name => 'I♥NY', :gender => 'jÚrgen'}]) do
20
+ Upsert.batch($conn, :pets) do |upsert|
21
+ upsert.row({:name => 'I♥NY'}, {:gender => 'périferôl'})
22
+ upsert.row({:name => 'I♥NY'}, {:gender => 'jÚrgen'})
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,94 @@
1
+ require 'spec_helper'
2
+ require 'upsert/merge_function/postgresql'
3
+
4
+ describe Upsert do
5
+ version = 'postgresql' == ENV['DB'] ? Upsert::MergeFunction::Postgresql.extract_version(
6
+ Pet.connection.select_value("SHOW server_version")
7
+ ) : 0
8
+
9
+ let(:upsert) do
10
+ Upsert.new($conn, :pets)
11
+ end
12
+
13
+ it "uses the native method if available (#{(UNIQUE_CONSTRAINT && version >= 90500).inspect})" do
14
+ p = Pet.create(:name => 'Jerry', :tag_number => 5)
15
+ upsert.row({ :name => 'Jerry'}, :tag_number => 6 )
16
+ expect(upsert.instance_variable_get(:@merge_function_cache).values.first.use_pg_native?).to(
17
+ UNIQUE_CONSTRAINT && version >= 90500 ? be_truthy : be_falsey
18
+ )
19
+ end
20
+
21
+ if version >= 90500 && UNIQUE_CONSTRAINT
22
+ it "works with a schema" do
23
+ table_name = ["#{RawConnectionFactory::DB_NAME}2", :pets2]
24
+ cls = clone_ar_class(Pet, table_name)
25
+ upsert = Upsert.new $conn, table_name
26
+ upsert.row({:name => 'Jerry'}, {:gender => 'male'})
27
+ expect(upsert.instance_variable_get(:@merge_function_cache).values.first.use_pg_native?).to be_truthy
28
+ end
29
+
30
+ it "checks the correct table for a unique constraint" do
31
+ Pet.connection.execute("CREATE SCHEMA IF NOT EXISTS unique_constraint_test")
32
+ Pet.connection.execute("CREATE TABLE unique_constraint_test.pets (LIKE public.pets INCLUDING ALL)")
33
+ Pet.connection.execute("SET search_path TO unique_constraint_test")
34
+
35
+ if RUBY_PLATFORM == "java"
36
+ $conn.nativeSQL("SET search_path TO unique_constraint_test")
37
+ $conn.setSchema("unique_constraint_test")
38
+ else
39
+ $conn.exec("SET search_path TO unique_constraint_test")
40
+ end
41
+
42
+ Pet.connection.execute("DROP INDEX unique_constraint_test.pets_name_idx")
43
+ Pet.connection.execute("ALTER TABLE unique_constraint_test.pets DROP CONSTRAINT IF EXISTS unique_name")
44
+ p = Pet.create(:name => 'Jerry', :tag_number => 5)
45
+ upsert.row({ :name => 'Jerry'}, :tag_number => 6 )
46
+ expect(upsert.instance_variable_get(:@merge_function_cache).values.first.use_pg_native?).to be_falsey
47
+ Pet.connection.execute("SET search_path TO public")
48
+
49
+ if RUBY_PLATFORM == "java"
50
+ $conn.nativeSQL("SET search_path TO public")
51
+ $conn.setSchema("public")
52
+ else
53
+ $conn.exec("SET search_path TO public")
54
+ end
55
+
56
+ Pet.connection.execute("DROP SCHEMA unique_constraint_test CASCADE")
57
+ end
58
+ end
59
+
60
+ describe "array escaping" do
61
+ let(:upsert) do
62
+ Upsert.new($conn, :posts)
63
+ end
64
+
65
+ before(:all) do
66
+ Sequel.migration do
67
+ change do
68
+ db = self
69
+ create_table?(:posts) do
70
+ primary_key :id
71
+ String :name
72
+ column :tags, "text[]"
73
+ end
74
+ end
75
+ end.apply(DB, :up)
76
+
77
+ Object.const_set("Post", Class.new(ActiveRecord::Base))
78
+ end
79
+
80
+ [
81
+ %w[1 2 3],
82
+ %w[can't stop won't stop],
83
+ %w["''" '""' '\\],
84
+ ["[]", "{}", "\\\\", "()"],
85
+ %w[*& *&^ $%IUBS (&^ ) ()*& // \\ \\\\ (*&^JN) (*HNCSD) ~!!!`` {} } { ( )],
86
+ %w[\\ \\\\ \\\\\\ \\\\\\\\ \\'\\'\'\\\'" \\'\\"\''\""],
87
+ ].each do |arr|
88
+ it "properly upserts array of: #{arr}" do
89
+ upsert.row({name: "same-name"}, tags: arr)
90
+ expect(Post.first.tags).to eq(arr)
91
+ end
92
+ end
93
+ end
94
+ end if ENV['DB'] == 'postgresql'
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+ describe Upsert do
3
+ describe "is precise" do
4
+ it "stores small numbers precisely" do
5
+ small = -0.00000000634943
6
+ upsert = Upsert.new $conn, :pets
7
+ upsert.row({:name => 'NotJerry'}, :lovability => small)
8
+ Pet.first.lovability.should be_within(1e-11).of(small) # ?
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+ describe Upsert do
3
+ describe "doesn't blow up on reserved words" do
4
+ # collect and uniq reserved words
5
+ reserved_words = ['mysql_reserved.txt', 'pg_reserved.txt'].map do |basename|
6
+ File.expand_path("../misc/#{basename}", __FILE__)
7
+ end.map do |path|
8
+ IO.readlines(path)
9
+ end.flatten.map(&:chomp).select(&:present?).uniq
10
+
11
+ # make lots of AR models, each of which has 10 columns named after these words
12
+ nasties = []
13
+ reserved_words.each_slice(10) do |words|
14
+ name = "Nasty#{nasties.length}"
15
+ Object.const_set(name, Class.new(ActiveRecord::Base))
16
+ nasty = Object.const_get(name)
17
+ nasty.class_eval do
18
+ self.table_name = name.downcase
19
+ self.primary_key = "fake_primary_key"
20
+ end
21
+
22
+ Sequel.migration do
23
+ change do
24
+ db = self
25
+ create_table?(name.downcase) do
26
+ primary_key :fake_primary_key
27
+ words.each do |word|
28
+ String word, limit: 191
29
+ end
30
+ end
31
+ end
32
+ end.apply(DB, :up)
33
+ nasties << [ nasty, words ]
34
+ end
35
+
36
+ describe "reserved words" do
37
+ nasties.each do |nasty, words|
38
+ it "doesn't die on reserved words #{words.join(',')}" do
39
+ upsert = Upsert.new $conn, nasty.table_name
40
+ random = rand(1e3)
41
+ selector = { :fake_primary_key => random, words.first => words.first }
42
+ setter = words[1..-1].inject({}) { |memo, word| memo[word] = word; memo }
43
+ assert_creates nasty, [selector.merge(setter)] do
44
+ upsert.row selector, setter
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,57 @@
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
+ config[:adapter] = case config[:adapter]
8
+ when 'postgresql' then 'postgres'
9
+ when 'sqlite3' then 'sqlite'
10
+ else config[:adapter]
11
+ end
12
+
13
+ let(:db) do
14
+ if RUBY_PLATFORM == "java"
15
+ Sequel.connect(
16
+ RawConnectionFactory::CONFIG,
17
+ :user => RawConnectionFactory::DB_USER,
18
+ :password => RawConnectionFactory::DB_PASSWORD
19
+ )
20
+ elsif config[:adapter] == "sqlite"
21
+ Sequel.sqlite("temp.db")
22
+ else
23
+ Sequel.connect(config.merge(
24
+ :user => config.values_at(:user, :username).compact.first,
25
+ :host => config.values_at(:host, :hostaddr).compact.first,
26
+ :database => config.values_at(:database, :dbname).compact.first
27
+ ))
28
+ end
29
+ end
30
+
31
+ it "Doesn't explode on connection" do
32
+ expect { db }.to_not raise_error
33
+ end
34
+
35
+ it "Doesn't explode when using DB.pool.hold" do
36
+ db.pool.hold do |conn|
37
+ expect {
38
+ upsert = Upsert.new(conn, :pets)
39
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
40
+ upsert.row({:name => 'Jerry'}, {:gender => 'male'})
41
+ end
42
+ }.to_not raise_error
43
+ end
44
+ end
45
+
46
+ it "Doesn't explode when using DB.synchronize" do
47
+ db.synchronize do |conn|
48
+ expect {
49
+ upsert = Upsert.new(conn, :pets)
50
+ assert_creates(Pet, [{:name => 'Jerry', :gender => 'male'}]) do
51
+ upsert.row({:name => 'Jerry'}, {:gender => 'male'})
52
+ end
53
+ }.to_not raise_error
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,417 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'bundler/setup'
3
+ Bundler.require(:default, :development)
4
+
5
+ require 'shellwords'
6
+ require "sequel"
7
+ Sequel.default_timezone = :utc
8
+ Sequel.extension :migration
9
+
10
+ require "active_record"
11
+ require "activerecord-import"
12
+ ActiveRecord::Base.default_timezone = :utc
13
+
14
+ raise "A DB value is required" unless ENV["DB"]
15
+ ENV['DB'] = 'postgresql' if ENV['DB'].to_s =~ /postgresql/i
16
+ UNIQUE_CONSTRAINT = ENV['UNIQUE_CONSTRAINT'] == 'true'
17
+
18
+ raise "please use DB=postgresql NOT postgres" if ENV["DB"] == "postgres"
19
+
20
+ class RawConnectionFactory
21
+ DB_NAME = ENV['DB_NAME'] || 'upsert_test'
22
+ # You *need* to specific DB_USER on certain combinations of JRuby/JDK as spawning a shell
23
+ # has some oddities
24
+ DB_USER = (ENV['DB_USER'] || `whoami`.chomp).to_s
25
+ raise "A DB_USER value is required" if DB_USER.empty?
26
+ DB_PASSWORD = ENV['DB_PASSWORD']
27
+ DB_HOST = ENV['DB_HOST'] || '127.0.0.1'
28
+
29
+ def self.db_env
30
+ @db_env ||= base_params(nil, false).map { |k, v| [":#{k}", v.to_s.empty? ? nil : Shellwords.escape(v)] }.to_h
31
+ end
32
+
33
+ def self.adapter_name(adapter = nil)
34
+ RUBY_PLATFORM != "java" && adapter == "mysql" ? "mysql2" : adapter
35
+ end
36
+
37
+ def self.base_params(adapter = nil, show_additional_params = true)
38
+ return { :adapter => "sqlite3", :database => "temp.db", cache: "shared" } if adapter == "sqlite3"
39
+ {
40
+ host: DB_HOST,
41
+ database: DB_NAME,
42
+ dbname: DB_NAME,
43
+ username: DB_USER,
44
+ user: DB_USER,
45
+ password: DB_PASSWORD,
46
+ adapter: adapter,
47
+ }.merge(
48
+ show_additional_params ? additional_params(adapter) : {}
49
+ )
50
+ end
51
+
52
+ def self.additional_params(adapter = nil)
53
+ {
54
+ "mysql" => { encoding: "utf8mb4" },
55
+ "mysql2" => { encoding: "utf8mb4" },
56
+ }.fetch(adapter, {})
57
+ end
58
+
59
+ def self.postgresql_call(string)
60
+ Kernel.system "PGHOST=#{db_env[":host"]} PGUSER=#{db_env[":user"]} PGPASSWORD=#{db_env[":password"]} #{string.gsub(/:[a-z]+/, db_env)}"
61
+ end
62
+
63
+ def self.mysql_call(string)
64
+ Kernel.system "mysql -h #{db_env[":host"]} -u #{db_env[":user"]} --password=#{db_env[":password"]} #{string.gsub(/:[a-z]+/, db_env)}"
65
+ end
66
+
67
+ SYSTEM_CALLS = {
68
+ "postgresql" => [
69
+ %{ dropdb :dbname },
70
+ %{ createdb :dbname },
71
+ %{ psql -d :dbname -c 'DROP SCHEMA IF EXISTS :dbname2 CASCASE' },
72
+ %{ psql -d :dbname -c 'CREATE SCHEMA :dbname2' },
73
+ ],
74
+ "mysql" => [
75
+ %{ -e "DROP DATABASE IF EXISTS :dbname" },
76
+ %{ -e "DROP DATABASE IF EXISTS :dbname2" },
77
+ %{ -e "CREATE DATABASE :dbname CHARSET utf8mb4 COLLATE utf8mb4_general_ci" },
78
+ %{ -e "CREATE DATABASE :dbname2 CHARSET utf8mb4 COLLATE utf8mb4_general_ci" },
79
+ ]
80
+ }.freeze
81
+
82
+ REQUIRES = {
83
+ "mysql" => "mysql2",
84
+ "postgresql" => "pg",
85
+ "sqlite3" => "sqlite3",
86
+ "java-postgresql" => "jdbc/postgres",
87
+ "java-mysql" => "jdbc/mysql",
88
+ "java-sqlite3" => "jdbc/sqlite3",
89
+ }.freeze
90
+
91
+ NEW_CONNECTION = {
92
+ "postgresql" => ->(base_params) { PG::Connection.new(base_params.except(:database, :username, :adapter)) },
93
+ "mysql" => ->(base_params) { Mysql2::Client.new(base_params) },
94
+ "sqlite3" => ->(base_params) { ActiveRecord::Base.connection.raw_connection },
95
+ }
96
+
97
+ POST_CONNECTION = {
98
+ "mysql" => -> { ActiveRecord::Base.connection.execute "SET NAMES utf8mb4 COLLATE utf8mb4_general_ci" },
99
+ "sqlite3" => -> { [ActiveRecord::Base.connection, ::DB].each { |c| c.execute "ATTACH DATABASE 'temp2.db' AS #{DB_NAME}2" } },
100
+ }
101
+
102
+ SYSTEM_CALLS.fetch(ENV["DB"], []).each do |str|
103
+ send("#{ENV["DB"]}_call", str)
104
+ end
105
+
106
+ if RUBY_PLATFORM == 'java'
107
+ CONFIG = "jdbc:#{ENV["DB"]}://#{DB_HOST}/#{DB_NAME}"
108
+ require REQUIRES["java-#{ENV["DB"]}"]
109
+
110
+ case ENV["DB"]
111
+ when "postgresql" then Jdbc::Postgres.load_driver
112
+ when "mysql" then Jdbc::MySQL.load_driver
113
+ when "sqlite3"
114
+ Jdbc::SQLite3.load_driver
115
+ CONFIG = "jdbc:sqlite::memory:?cache=shared"
116
+ end
117
+
118
+ def new_connection
119
+ java.util.TimeZone.setDefault(java.util.TimeZone.getTimeZone("+00:00"))
120
+ java.sql.DriverManager.get_connection CONFIG, DB_USER, DB_PASSWORD
121
+ end
122
+ else
123
+ case ENV['DB']
124
+ when "postgresql", "mysql"
125
+ require REQUIRES[ENV["DB"]]
126
+ def new_connection
127
+ NEW_CONNECTION[ENV["DB"]].call(self.class.base_params(ENV["DB"]))
128
+ end
129
+ when "sqlite3"
130
+ require REQUIRES[ENV["DB"]]
131
+ def new_connection
132
+ NEW_CONNECTION[ENV["DB"]].call(self.class.base_params(ENV["DB"]))
133
+ end
134
+ CONFIG = { :adapter => "sqlite3", :database => "temp.db", cache: "shared" }
135
+
136
+ end
137
+ end
138
+
139
+ ActiveRecord::Base.establish_connection(
140
+ base_params(adapter_name(ENV["DB"]))
141
+ )
142
+ ari_adapter_name = adapter_name(ENV["DB"]) == "mysql" ? "mysql2" : adapter_name(ENV["DB"])
143
+ require "activerecord-import/active_record/adapters/#{ari_adapter_name}_adapter"
144
+ end
145
+
146
+ raise "not supported" unless RawConnectionFactory.instance_methods.include?(:new_connection)
147
+
148
+ config = ActiveRecord::Base.connection.instance_variable_get(:@config)
149
+ config[:adapter] = case config[:adapter]
150
+ when "postgresql" then "postgres"
151
+ when "sqlite3" then "sqlite"
152
+ else config[:adapter]
153
+ end
154
+ params = if RUBY_PLATFORM == "java"
155
+ RawConnectionFactory::CONFIG
156
+ else
157
+ config.merge(
158
+ :user => config.values_at(:user, :username).compact.first,
159
+ :host => config.values_at(:host, :hostaddr).compact.first,
160
+ :database => config.values_at(:database, :dbname).compact.first
161
+ )
162
+ end
163
+ DB = if RUBY_PLATFORM == "java"
164
+ Sequel.connect(
165
+ params,
166
+ :user => RawConnectionFactory::DB_USER,
167
+ :password => RawConnectionFactory::DB_PASSWORD
168
+ )
169
+ elsif ENV["DB"] == "sqlite3"
170
+ Kernel.at_exit { FileUtils.rm(Dir.glob("temp*.db")) }
171
+ Sequel.sqlite("temp.db")
172
+ else
173
+ Sequel.connect(params)
174
+ end
175
+
176
+ $conn_factory = RawConnectionFactory.new
177
+ $conn = $conn_factory.new_connection
178
+ RawConnectionFactory::POST_CONNECTION.fetch(ENV["DB"], -> {}).call
179
+
180
+
181
+ require 'logger'
182
+ require 'fileutils'
183
+ FileUtils.rm_f 'test.log'
184
+ ActiveRecord::Base.logger = Logger.new('test.log')
185
+
186
+ if ENV['VERBOSE'] == 'true'
187
+ ActiveRecord::Base.logger.level = Logger::DEBUG
188
+ else
189
+ ActiveRecord::Base.logger.level = Logger::WARN
190
+ end
191
+
192
+ if ENV['DB'] == 'postgresql'
193
+ begin
194
+ DB << "ALTER TABLE pets DROP CONSTRAINT IF EXISTS unique_name"
195
+ rescue => e
196
+ puts e.inspect
197
+ end
198
+ end
199
+
200
+ class InternalMigration
201
+ DEFINITIONS = {
202
+ pets: ->(db) {
203
+ primary_key :id
204
+ String :name, { size: 191 }.merge(ENV["DB"] == "mysql" || UNIQUE_CONSTRAINT ? { index: { unique: true } } : {})
205
+ String :gender
206
+ String :spiel
207
+ TrueClass :good
208
+ BigDecimal :lovability, size: [30, 15] # 15 integer digits and 15 fractional digits
209
+ DateTime :morning_walk_time
210
+ File :zipped_biography
211
+ Integer :tag_number
212
+ Bignum :big_tag_number
213
+ Date :birthday
214
+ String :home_address, text: true
215
+
216
+ if db.database_type == :postgres
217
+ column :tsntz, "timestamp without time zone"
218
+ end
219
+ },
220
+ tasks: ->(db) {
221
+ primary_key :id
222
+ String :name
223
+ DateTime :created_at
224
+ DateTime :created_on
225
+ },
226
+ people: ->(db) {
227
+ primary_key :id
228
+ String :"First Name"
229
+ String :"Last Name"
230
+ },
231
+ alphabets: ->(db) {
232
+ ("a".."z").each do |col|
233
+ Integer "the_letter_#{col}".to_sym
234
+ end
235
+ }
236
+ }
237
+ end
238
+
239
+ Sequel.migration do
240
+ change do
241
+ db = self
242
+ InternalMigration::DEFINITIONS.each do |table, blk|
243
+ create_table?(table) do
244
+ instance_exec(db, &blk)
245
+ end
246
+ end
247
+ end
248
+ end.apply(DB, :up)
249
+
250
+ if ENV['DB'] == 'postgresql' && UNIQUE_CONSTRAINT
251
+ DB << "ALTER TABLE pets ADD CONSTRAINT unique_name UNIQUE (name)"
252
+ end
253
+
254
+ %i[Pet Task Person Alphabet].each do |name|
255
+ Object.const_set(name, Class.new(ActiveRecord::Base))
256
+ end
257
+
258
+ require 'zlib'
259
+ require 'benchmark'
260
+ require 'faker'
261
+
262
+ module SpecHelper
263
+ def random_time_or_datetime
264
+ time = Time.at(rand * Time.now.to_i)
265
+ if ENV['DB'] == 'mysql'
266
+ time = time.change(:usec => 0)
267
+ end
268
+ if rand > 0.5
269
+ time = time.change(:usec => 0).to_datetime
270
+ end
271
+ time
272
+ end
273
+
274
+ def lotsa_records
275
+ @records ||= begin
276
+ memo = []
277
+ names = []
278
+ 333.times do
279
+ names << Faker::Name.name
280
+ end
281
+ 2000.times do
282
+ selector = ActiveSupport::OrderedHash.new
283
+ selector[:name] = if RUBY_VERSION >= '1.9'
284
+ names.sample
285
+ else
286
+ names.choice
287
+ end
288
+ setter = {
289
+ :lovability => BigDecimal(rand(1e11).to_s, 2),
290
+ :tag_number => rand(1e8),
291
+ :spiel => Faker::Lorem.sentences.join,
292
+ :good => true,
293
+ :birthday => Time.at(rand * Time.now.to_i).to_date,
294
+ :morning_walk_time => random_time_or_datetime,
295
+ :home_address => Faker::Lorem.sentences.join,
296
+ # hard to know how to have AR insert this properly unless Upsert::Binary subclasses String
297
+ # :zipped_biography => Upsert.binary(Zlib::Deflate.deflate(Faker::Lorem.paragraphs.join, Zlib::BEST_SPEED))
298
+ }
299
+ memo << [selector, setter]
300
+ end
301
+ memo
302
+ end
303
+ end
304
+
305
+ def assert_same_result(records, &blk)
306
+ blk.call(records)
307
+ ref1 = Pet.order(:name).all.map { |pet| pet.attributes.except('id') }
308
+
309
+ Pet.delete_all
310
+
311
+ Upsert.batch($conn, :pets) do |upsert|
312
+ records.each do |selector, setter|
313
+ upsert.row(selector, setter)
314
+ end
315
+ end
316
+ ref2 = Pet.order(:name).all.map { |pet| pet.attributes.except('id') }
317
+ compare_attribute_sets ref1, ref2
318
+ end
319
+
320
+ def assert_creates(model, expected_records)
321
+ expected_records.each do |selector, setter|
322
+ # should i use setter in where?
323
+ model.where(selector).count.should == 0
324
+ end
325
+ yield
326
+ expected_records.each do |selector, setter|
327
+ setter ||= {}
328
+ found = model.where(selector).map { |record| record.attributes.except('id') }
329
+ expect(found).to_not be_empty, { :selector => selector, :setter => setter }.inspect
330
+ expected = [ selector.stringify_keys.merge(setter.stringify_keys) ]
331
+ compare_attribute_sets expected, found
332
+ end
333
+ end
334
+
335
+ def compare_attribute_sets(expected, found)
336
+ e = expected.map { |attrs| simplify_attributes attrs }
337
+ f = found.map { |attrs| simplify_attributes attrs }
338
+ f.each_with_index do |fa, i|
339
+ fa.should == e[i]
340
+ end
341
+ end
342
+
343
+ def simplify_attributes(attrs)
344
+ attrs.select do |k, v|
345
+ v.present?
346
+ end.inject({}) do |memo, (k, v)|
347
+ memo[k] = case v
348
+ when Time, DateTime
349
+ v.to_i
350
+ else
351
+ v
352
+ end
353
+ memo
354
+ end
355
+ end
356
+
357
+ def assert_faster_than(competition, records, &blk)
358
+ # dry run
359
+ blk.call records
360
+ Pet.delete_all
361
+ sleep 1
362
+ # --
363
+
364
+ ar_time = Benchmark.realtime { blk.call(records) }
365
+
366
+ Pet.delete_all
367
+ sleep 1
368
+
369
+ upsert_time = Benchmark.realtime do
370
+ Upsert.batch($conn, :pets) do |upsert|
371
+ records.each do |selector, setter|
372
+ upsert.row(selector, setter)
373
+ end
374
+ end
375
+ end
376
+ upsert_time.should be < ar_time
377
+ $stderr.puts " Upsert was #{((ar_time - upsert_time) / ar_time * 100).round}% faster than #{competition}"
378
+ end
379
+
380
+ def clone_ar_class(klass, table_name)
381
+ u = Upsert.new $conn, klass.table_name
382
+ new_table_name = [*table_name].compact
383
+ # AR's support for quoting of schema and table names is horrendous
384
+ # schema.table and schema.`table` are considiered different names on MySQL, but
385
+ # schema.table and schema."table" are correctly considered the same on Postgres
386
+ sequel_table_name = new_table_name.map(&:to_sym)
387
+ new_table_name[-1] = u.connection.quote_ident(new_table_name[-1]) if new_table_name[-1].to_s.index('.')
388
+ new_table_name = new_table_name.join('.')
389
+
390
+ Sequel.migration do
391
+ change do
392
+ db = self
393
+ create_table?(sequel_table_name.length > 1 ? Sequel.qualify(*sequel_table_name) : sequel_table_name.first) do
394
+ instance_exec(db, &InternalMigration::DEFINITIONS[klass.table_name.to_sym])
395
+ end
396
+ end
397
+ end.apply(DB, :up)
398
+
399
+ cls = Class.new(klass)
400
+ cls.class_eval do
401
+ self.table_name = new_table_name
402
+ def self.quoted_table_name
403
+ new_table_name
404
+ end
405
+ end
406
+ cls
407
+ end
408
+ end
409
+
410
+ RSpec.configure do |c|
411
+ c.include SpecHelper
412
+ c.before do
413
+ Pet.delete_all
414
+ end
415
+ end
416
+
417
+ require 'upsert'