upsert 1.0.2 → 1.1.0
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.
- 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
|