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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.ruby-version +1 -0
- data/.standard.yml +1 -0
- data/.travis.yml +63 -0
- data/.yardopts +2 -0
- data/CHANGELOG +265 -0
- data/Gemfile +16 -0
- data/LICENSE +24 -0
- data/README.md +411 -0
- data/Rakefile +54 -0
- data/lib/upsert.rb +284 -0
- data/lib/upsert/active_record_upsert.rb +12 -0
- data/lib/upsert/binary.rb +8 -0
- data/lib/upsert/column_definition.rb +79 -0
- data/lib/upsert/column_definition/mysql.rb +24 -0
- data/lib/upsert/column_definition/postgresql.rb +66 -0
- data/lib/upsert/column_definition/sqlite3.rb +34 -0
- data/lib/upsert/connection.rb +37 -0
- data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +31 -0
- data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
- data/lib/upsert/connection/Java_OrgSqlite_Conn.rb +17 -0
- data/lib/upsert/connection/Mysql2_Client.rb +76 -0
- data/lib/upsert/connection/PG_Connection.rb +35 -0
- data/lib/upsert/connection/SQLite3_Database.rb +28 -0
- data/lib/upsert/connection/jdbc.rb +105 -0
- data/lib/upsert/connection/postgresql.rb +24 -0
- data/lib/upsert/connection/sqlite3.rb +19 -0
- data/lib/upsert/merge_function.rb +73 -0
- data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
- data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc_PgConnection.rb +27 -0
- data/lib/upsert/merge_function/Java_OrgSqlite_Conn.rb +10 -0
- data/lib/upsert/merge_function/Mysql2_Client.rb +36 -0
- data/lib/upsert/merge_function/PG_Connection.rb +26 -0
- data/lib/upsert/merge_function/SQLite3_Database.rb +10 -0
- data/lib/upsert/merge_function/mysql.rb +66 -0
- data/lib/upsert/merge_function/postgresql.rb +365 -0
- data/lib/upsert/merge_function/sqlite3.rb +43 -0
- data/lib/upsert/row.rb +59 -0
- data/lib/upsert/version.rb +3 -0
- data/spec/active_record_upsert_spec.rb +26 -0
- data/spec/binary_spec.rb +21 -0
- data/spec/correctness_spec.rb +190 -0
- data/spec/database_functions_spec.rb +106 -0
- data/spec/database_spec.rb +121 -0
- data/spec/hstore_spec.rb +249 -0
- data/spec/jruby_spec.rb +9 -0
- data/spec/logger_spec.rb +52 -0
- data/spec/misc/get_postgres_reserved_words.rb +12 -0
- data/spec/misc/mysql_reserved.txt +226 -0
- data/spec/misc/pg_reserved.txt +742 -0
- data/spec/multibyte_spec.rb +27 -0
- data/spec/postgresql_spec.rb +94 -0
- data/spec/precision_spec.rb +11 -0
- data/spec/reserved_words_spec.rb +50 -0
- data/spec/sequel_spec.rb +57 -0
- data/spec/spec_helper.rb +417 -0
- data/spec/speed_spec.rb +44 -0
- data/spec/threaded_spec.rb +57 -0
- data/spec/timezones_spec.rb +58 -0
- data/spec/type_safety_spec.rb +12 -0
- data/travis/install_postgres.sh +18 -0
- data/travis/run_docker_db.sh +20 -0
- data/travis/tune_mysql.sh +7 -0
- data/upsert-java.gemspec +13 -0
- data/upsert.gemspec +11 -0
- data/upsert.gemspec.common +107 -0
- 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
|
data/spec/sequel_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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'
|