upsert 2.9.10-java
Sign up to get free protection for your applications and to get access to all the features.
- 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 +20 -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 +14 -0
- data/upsert.gemspec +13 -0
- data/upsert.gemspec.common +106 -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'
|