upsert 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +7 -0
- data/Gemfile +4 -0
- data/README.md +115 -66
- data/Rakefile +16 -5
- data/lib/upsert.rb +86 -25
- data/lib/upsert/binary.rb +2 -0
- data/lib/upsert/column_definition.rb +27 -3
- data/lib/upsert/column_definition/mysql.rb +20 -0
- data/lib/upsert/column_definition/{PG_Connection.rb → postgresql.rb} +1 -1
- data/lib/upsert/connection.rb +20 -22
- data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +25 -0
- data/lib/upsert/connection/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +14 -0
- data/lib/upsert/connection/Java_OrgSqliteConn.rb +17 -0
- data/lib/upsert/connection/Mysql2_Client.rb +40 -18
- data/lib/upsert/connection/PG_Connection.rb +7 -3
- data/lib/upsert/connection/SQLite3_Database.rb +10 -2
- data/lib/upsert/connection/jdbc.rb +81 -0
- data/lib/upsert/connection/sqlite3.rb +23 -0
- data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
- data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +35 -0
- data/lib/upsert/merge_function/Java_OrgSqliteConn.rb +10 -0
- data/lib/upsert/merge_function/Mysql2_Client.rb +5 -58
- data/lib/upsert/merge_function/PG_Connection.rb +6 -78
- data/lib/upsert/merge_function/SQLite3_Database.rb +3 -22
- data/lib/upsert/merge_function/mysql.rb +67 -0
- data/lib/upsert/merge_function/postgresql.rb +94 -0
- data/lib/upsert/merge_function/sqlite3.rb +30 -0
- data/lib/upsert/row.rb +3 -6
- data/lib/upsert/version.rb +1 -1
- data/spec/binary_spec.rb +0 -2
- data/spec/correctness_spec.rb +26 -25
- data/spec/database_functions_spec.rb +6 -14
- data/spec/logger_spec.rb +22 -10
- data/spec/precision_spec.rb +1 -1
- data/spec/spec_helper.rb +115 -31
- data/spec/speed_spec.rb +1 -1
- data/spec/timezones_spec.rb +35 -14
- data/spec/type_safety_spec.rb +2 -2
- data/upsert.gemspec +18 -6
- metadata +25 -38
- data/lib/upsert/cell.rb +0 -5
- data/lib/upsert/cell/Mysql2_Client.rb +0 -16
- data/lib/upsert/cell/PG_Connection.rb +0 -28
- data/lib/upsert/cell/SQLite3_Database.rb +0 -36
- data/lib/upsert/column_definition/Mysql2_Client.rb +0 -24
- data/lib/upsert/column_definition/SQLite3_Database.rb +0 -7
- data/lib/upsert/row/Mysql2_Client.rb +0 -21
- data/lib/upsert/row/PG_Connection.rb +0 -7
- data/lib/upsert/row/SQLite3_Database.rb +0 -7
data/lib/upsert/binary.rb
CHANGED
@@ -8,6 +8,8 @@ class Upsert
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
+
TIME_DETECTOR = /date|time/i
|
12
|
+
|
11
13
|
attr_reader :name
|
12
14
|
attr_reader :sql_type
|
13
15
|
attr_reader :default
|
@@ -18,6 +20,7 @@ class Upsert
|
|
18
20
|
def initialize(connection, name, sql_type, default)
|
19
21
|
@name = name
|
20
22
|
@sql_type = sql_type
|
23
|
+
@temporal_query = !!(sql_type =~ TIME_DETECTOR)
|
21
24
|
@default = default
|
22
25
|
@quoted_name = connection.quote_ident name
|
23
26
|
@quoted_selector_name = connection.quote_ident "#{name}_sel"
|
@@ -25,19 +28,40 @@ class Upsert
|
|
25
28
|
end
|
26
29
|
|
27
30
|
def to_selector_arg
|
28
|
-
"#{quoted_selector_name} #{
|
31
|
+
"#{quoted_selector_name} #{arg_type}"
|
29
32
|
end
|
30
33
|
|
31
34
|
def to_setter_arg
|
32
|
-
"#{quoted_setter_name} #{
|
35
|
+
"#{quoted_setter_name} #{arg_type}"
|
33
36
|
end
|
34
37
|
|
35
38
|
def to_setter
|
36
|
-
"#{quoted_name} = #{
|
39
|
+
"#{quoted_name} = #{to_setter_value}"
|
37
40
|
end
|
38
41
|
|
39
42
|
def to_selector
|
40
43
|
"#{quoted_name} = #{quoted_selector_name}"
|
41
44
|
end
|
45
|
+
|
46
|
+
def temporal?
|
47
|
+
@temporal_query
|
48
|
+
end
|
49
|
+
|
50
|
+
def arg_type
|
51
|
+
if temporal?
|
52
|
+
'character varying(255)'
|
53
|
+
else
|
54
|
+
sql_type
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_setter_value
|
59
|
+
if temporal?
|
60
|
+
"CAST(#{quoted_setter_name} AS #{sql_type})"
|
61
|
+
else
|
62
|
+
quoted_setter_name
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
42
66
|
end
|
43
67
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Upsert
|
2
|
+
class ColumnDefinition
|
3
|
+
# @private
|
4
|
+
class Mysql < ColumnDefinition
|
5
|
+
class << self
|
6
|
+
def all(connection, table_name)
|
7
|
+
connection.execute("SHOW COLUMNS FROM #{connection.quote_ident(table_name)}").map do |row|
|
8
|
+
# {"Field"=>"name", "Type"=>"varchar(255)", "Null"=>"NO", "Key"=>"PRI", "Default"=>nil, "Extra"=>""}
|
9
|
+
name = row['Field'] || row['COLUMN_NAME']
|
10
|
+
type = row['Type'] || row['COLUMN_TYPE']
|
11
|
+
default = row['Default'] || row['COLUMN_DEFAULT']
|
12
|
+
new connection, name, type, default
|
13
|
+
end.sort_by do |cd|
|
14
|
+
cd.name
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
class Upsert
|
2
2
|
class ColumnDefinition
|
3
3
|
# @private
|
4
|
-
class
|
4
|
+
class Postgresql < ColumnDefinition
|
5
5
|
class << self
|
6
6
|
# activerecord-3.2.5/lib/active_record/connection_adapters/postgresql_adapter.rb#column_definitions
|
7
7
|
def all(connection, table_name)
|
data/lib/upsert/connection.rb
CHANGED
@@ -2,36 +2,34 @@ class Upsert
|
|
2
2
|
# @private
|
3
3
|
class Connection
|
4
4
|
attr_reader :controller
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :metal
|
6
6
|
|
7
|
-
def initialize(controller,
|
7
|
+
def initialize(controller, metal)
|
8
8
|
@controller = controller
|
9
|
-
@
|
9
|
+
@metal = metal
|
10
10
|
end
|
11
|
-
|
12
|
-
def
|
11
|
+
|
12
|
+
def convert_binary(bind_values)
|
13
|
+
bind_values.map do |v|
|
14
|
+
case v
|
15
|
+
when Upsert::Binary
|
16
|
+
binary v
|
17
|
+
else
|
18
|
+
v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def bind_value(v)
|
13
24
|
case v
|
14
|
-
when NilClass
|
15
|
-
NULL_WORD
|
16
|
-
when Upsert::Binary
|
17
|
-
quote_binary v.value # must be defined by base
|
18
|
-
when String
|
19
|
-
quote_string v # must be defined by base
|
20
|
-
when TrueClass, FalseClass
|
21
|
-
quote_boolean v
|
22
|
-
when BigDecimal
|
23
|
-
quote_big_decimal v
|
24
|
-
when Numeric
|
25
|
-
v
|
26
|
-
when Symbol
|
27
|
-
quote_string v.to_s
|
28
25
|
when Time, DateTime
|
29
|
-
|
26
|
+
Upsert.utc_iso8601 v
|
30
27
|
when Date
|
31
|
-
|
28
|
+
v.strftime ISO8601_DATE
|
32
29
|
else
|
33
|
-
|
30
|
+
v
|
34
31
|
end
|
35
32
|
end
|
33
|
+
|
36
34
|
end
|
37
35
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'upsert/connection/jdbc'
|
2
|
+
|
3
|
+
class Upsert
|
4
|
+
class Connection
|
5
|
+
# @private
|
6
|
+
class Java_ComMysqlJdbc_JDBC4Connection < Connection
|
7
|
+
include Jdbc
|
8
|
+
|
9
|
+
# ? backtick?
|
10
|
+
def quote_ident(k)
|
11
|
+
DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
|
12
|
+
end
|
13
|
+
|
14
|
+
def bind_value(v)
|
15
|
+
case v
|
16
|
+
when Time, DateTime
|
17
|
+
# mysql doesn't like it when you send timezone to a datetime
|
18
|
+
Upsert.utc_iso8601 v, false
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'upsert/connection/jdbc'
|
2
|
+
|
3
|
+
class Upsert
|
4
|
+
class Connection
|
5
|
+
# @private
|
6
|
+
class Java_OrgPostgresqlJdbc4_Jdbc4Connection < Connection
|
7
|
+
include Jdbc
|
8
|
+
|
9
|
+
def quote_ident(k)
|
10
|
+
DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'upsert/connection/jdbc'
|
2
|
+
require 'upsert/connection/sqlite3'
|
3
|
+
|
4
|
+
class Upsert
|
5
|
+
class Connection
|
6
|
+
# @private
|
7
|
+
class Java_OrgSqliteConn < Connection
|
8
|
+
include Jdbc
|
9
|
+
include Sqlite3
|
10
|
+
|
11
|
+
# ?
|
12
|
+
def quote_ident(k)
|
13
|
+
DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -4,7 +4,42 @@ class Upsert
|
|
4
4
|
class Mysql2_Client < Connection
|
5
5
|
def execute(sql)
|
6
6
|
Upsert.logger.debug { %{[upsert] #{sql}} }
|
7
|
-
|
7
|
+
if results = metal.query(sql)
|
8
|
+
rows = []
|
9
|
+
results.each { |row| rows << row }
|
10
|
+
if rows[0].is_a? Array
|
11
|
+
# you don't know if mysql2 is going to give you an array or a hash... and you shouldn't specify, because it's sticky
|
12
|
+
fields = results.fields
|
13
|
+
rows.map { |row| Hash[fields.zip(row)] }
|
14
|
+
else
|
15
|
+
rows
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def quote_value(v)
|
21
|
+
case v
|
22
|
+
when NilClass
|
23
|
+
NULL_WORD
|
24
|
+
when Upsert::Binary
|
25
|
+
quote_binary v.value
|
26
|
+
when String
|
27
|
+
quote_string v
|
28
|
+
when TrueClass, FalseClass
|
29
|
+
quote_boolean v
|
30
|
+
when BigDecimal
|
31
|
+
quote_big_decimal v
|
32
|
+
when Numeric
|
33
|
+
v
|
34
|
+
when Symbol
|
35
|
+
quote_string v.to_s
|
36
|
+
when DateTime, Time
|
37
|
+
quote_string Upsert.utc_iso8601(v) #round?
|
38
|
+
when Date
|
39
|
+
quote_date v
|
40
|
+
else
|
41
|
+
raise "not sure how to quote #{v.class}: #{v.inspect}"
|
42
|
+
end
|
8
43
|
end
|
9
44
|
|
10
45
|
def quote_boolean(v)
|
@@ -12,7 +47,7 @@ class Upsert
|
|
12
47
|
end
|
13
48
|
|
14
49
|
def quote_string(v)
|
15
|
-
SINGLE_QUOTE +
|
50
|
+
SINGLE_QUOTE + metal.escape(v) + SINGLE_QUOTE
|
16
51
|
end
|
17
52
|
|
18
53
|
# This doubles the size of the representation.
|
@@ -24,30 +59,17 @@ class Upsert
|
|
24
59
|
# might work if we could get the encoding issues fixed when joining together the values for the sql
|
25
60
|
# alias_method :quote_binary, :quote_string
|
26
61
|
|
27
|
-
def
|
28
|
-
quote_string v.strftime(
|
62
|
+
def quote_date(v)
|
63
|
+
quote_string v.strftime(ISO8601_DATE)
|
29
64
|
end
|
30
65
|
|
31
66
|
def quote_ident(k)
|
32
|
-
BACKTICK +
|
67
|
+
BACKTICK + metal.escape(k.to_s) + BACKTICK
|
33
68
|
end
|
34
69
|
|
35
70
|
def quote_big_decimal(v)
|
36
71
|
v.to_s('F')
|
37
72
|
end
|
38
|
-
|
39
|
-
def database_variable_get(k)
|
40
|
-
sql = "SHOW VARIABLES LIKE '#{k}'"
|
41
|
-
row = execute(sql).first
|
42
|
-
case row
|
43
|
-
when Array
|
44
|
-
row[1]
|
45
|
-
when Hash
|
46
|
-
row['Value']
|
47
|
-
else
|
48
|
-
raise "Don't know what to do if connection.query returns a #{row.class}"
|
49
|
-
end
|
50
|
-
end
|
51
73
|
end
|
52
74
|
end
|
53
75
|
end
|
@@ -5,15 +5,19 @@ class Upsert
|
|
5
5
|
def execute(sql, params = nil)
|
6
6
|
if params
|
7
7
|
Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
|
8
|
-
|
8
|
+
metal.exec sql, convert_binary(params)
|
9
9
|
else
|
10
10
|
Upsert.logger.debug { %{[upsert] #{sql}} }
|
11
|
-
|
11
|
+
metal.exec sql
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
15
|
def quote_ident(k)
|
16
|
-
|
16
|
+
metal.quote_ident k.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def binary(v)
|
20
|
+
{ :value => v.value, :format => 1 }
|
17
21
|
end
|
18
22
|
end
|
19
23
|
end
|
@@ -1,20 +1,28 @@
|
|
1
|
+
require 'upsert/connection/sqlite3'
|
2
|
+
|
1
3
|
class Upsert
|
2
4
|
class Connection
|
3
5
|
# @private
|
4
6
|
class SQLite3_Database < Connection
|
7
|
+
include Sqlite3
|
8
|
+
|
5
9
|
def execute(sql, params = nil)
|
6
10
|
if params
|
7
11
|
Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
|
8
|
-
|
12
|
+
metal.execute sql, convert_binary(params)
|
9
13
|
else
|
10
14
|
Upsert.logger.debug { %{[upsert] #{sql}} }
|
11
|
-
|
15
|
+
metal.execute sql
|
12
16
|
end
|
13
17
|
end
|
14
18
|
|
15
19
|
def quote_ident(k)
|
16
20
|
DOUBLE_QUOTE + SQLite3::Database.quote(k.to_s) + DOUBLE_QUOTE
|
17
21
|
end
|
22
|
+
|
23
|
+
def binary(v)
|
24
|
+
SQLite3::Blob.new v.value
|
25
|
+
end
|
18
26
|
end
|
19
27
|
end
|
20
28
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
class Upsert
|
2
|
+
class Connection
|
3
|
+
# @private
|
4
|
+
module Jdbc
|
5
|
+
# /Users/seamusabshere/.rvm/gems/jruby-head/gems/activerecord-jdbc-adapter-1.2.2.1/src/java/arjdbc/jdbc/RubyJdbcConnection.java
|
6
|
+
GETTER = {
|
7
|
+
java.sql.Types::VARCHAR => 'getString',
|
8
|
+
java.sql.Types::OTHER => 'getString', # ?! i guess unicode text?
|
9
|
+
java.sql.Types::BINARY => 'getBlob',
|
10
|
+
java.sql.Types::LONGVARCHAR => 'getString',
|
11
|
+
}
|
12
|
+
java.sql.Types.constants.each do |type_name|
|
13
|
+
i = java.sql.Types.const_get type_name
|
14
|
+
unless GETTER.has_key?(i)
|
15
|
+
GETTER[i] = 'get' + type_name[0].upcase + type_name[1..-1].downcase
|
16
|
+
end
|
17
|
+
end
|
18
|
+
SETTER = Hash.new do |hash, k|
|
19
|
+
hash[k] = 'set' + k
|
20
|
+
end.merge(
|
21
|
+
'TrueClass' => 'setBoolean',
|
22
|
+
'FalseClass' => 'setBoolean',
|
23
|
+
'Fixnum' => 'setInt',
|
24
|
+
)
|
25
|
+
|
26
|
+
def binary(v)
|
27
|
+
v.value.to_java_bytes.java_object
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute(sql, params = nil)
|
31
|
+
has_result = if params
|
32
|
+
Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
|
33
|
+
setters = self.class.const_get(:SETTER)
|
34
|
+
statement = metal.prepareStatement sql
|
35
|
+
params.each_with_index do |v, i|
|
36
|
+
case v
|
37
|
+
when Upsert::Binary
|
38
|
+
statement.setBytes i+1, binary(v)
|
39
|
+
when BigDecimal
|
40
|
+
statement.setBigDecimal i+1, java.math.BigDecimal.new(v.to_s)
|
41
|
+
when NilClass
|
42
|
+
# http://stackoverflow.com/questions/4243513/why-does-preparedstatement-setnull-requires-sqltype
|
43
|
+
statement.setObject i+1, nil
|
44
|
+
else
|
45
|
+
setter = setters[v.class.name]
|
46
|
+
statement.send setter, i+1, v
|
47
|
+
end
|
48
|
+
end
|
49
|
+
statement.execute
|
50
|
+
else
|
51
|
+
Upsert.logger.debug { %{[upsert] #{sql}} }
|
52
|
+
statement = metal.createStatement
|
53
|
+
statement.execute sql
|
54
|
+
end
|
55
|
+
if not has_result
|
56
|
+
statement.close
|
57
|
+
return
|
58
|
+
end
|
59
|
+
getters = self.class.const_get(:GETTER)
|
60
|
+
raw_result = statement.getResultSet
|
61
|
+
meta = raw_result.getMetaData
|
62
|
+
count = meta.getColumnCount
|
63
|
+
column_name_and_getter = (1..count).inject({}) do |memo, i|
|
64
|
+
memo[i] = [ meta.getColumnName(i), getters[meta.getColumnType(i)] ]
|
65
|
+
memo
|
66
|
+
end
|
67
|
+
result = []
|
68
|
+
while raw_result.next
|
69
|
+
row = {}
|
70
|
+
column_name_and_getter.each do |i, cg|
|
71
|
+
column_name, getter = cg
|
72
|
+
row[column_name] = raw_result.send(getter, i)
|
73
|
+
end
|
74
|
+
result << row
|
75
|
+
end
|
76
|
+
statement.close
|
77
|
+
result
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
class Upsert
|
2
|
+
class Connection
|
3
|
+
# @private
|
4
|
+
module Sqlite3
|
5
|
+
def bind_value(v)
|
6
|
+
case v
|
7
|
+
when BigDecimal
|
8
|
+
v.to_s('F')
|
9
|
+
when TrueClass
|
10
|
+
't'
|
11
|
+
when FalseClass
|
12
|
+
'f'
|
13
|
+
when Time, DateTime
|
14
|
+
Upsert.utc_iso8601 v
|
15
|
+
when Date
|
16
|
+
v.strftime ISO8601_DATE
|
17
|
+
else
|
18
|
+
v
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|