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.
Files changed (49) hide show
  1. data/CHANGELOG +7 -0
  2. data/Gemfile +4 -0
  3. data/README.md +115 -66
  4. data/Rakefile +16 -5
  5. data/lib/upsert.rb +86 -25
  6. data/lib/upsert/binary.rb +2 -0
  7. data/lib/upsert/column_definition.rb +27 -3
  8. data/lib/upsert/column_definition/mysql.rb +20 -0
  9. data/lib/upsert/column_definition/{PG_Connection.rb → postgresql.rb} +1 -1
  10. data/lib/upsert/connection.rb +20 -22
  11. data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +25 -0
  12. data/lib/upsert/connection/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +14 -0
  13. data/lib/upsert/connection/Java_OrgSqliteConn.rb +17 -0
  14. data/lib/upsert/connection/Mysql2_Client.rb +40 -18
  15. data/lib/upsert/connection/PG_Connection.rb +7 -3
  16. data/lib/upsert/connection/SQLite3_Database.rb +10 -2
  17. data/lib/upsert/connection/jdbc.rb +81 -0
  18. data/lib/upsert/connection/sqlite3.rb +23 -0
  19. data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
  20. data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +35 -0
  21. data/lib/upsert/merge_function/Java_OrgSqliteConn.rb +10 -0
  22. data/lib/upsert/merge_function/Mysql2_Client.rb +5 -58
  23. data/lib/upsert/merge_function/PG_Connection.rb +6 -78
  24. data/lib/upsert/merge_function/SQLite3_Database.rb +3 -22
  25. data/lib/upsert/merge_function/mysql.rb +67 -0
  26. data/lib/upsert/merge_function/postgresql.rb +94 -0
  27. data/lib/upsert/merge_function/sqlite3.rb +30 -0
  28. data/lib/upsert/row.rb +3 -6
  29. data/lib/upsert/version.rb +1 -1
  30. data/spec/binary_spec.rb +0 -2
  31. data/spec/correctness_spec.rb +26 -25
  32. data/spec/database_functions_spec.rb +6 -14
  33. data/spec/logger_spec.rb +22 -10
  34. data/spec/precision_spec.rb +1 -1
  35. data/spec/spec_helper.rb +115 -31
  36. data/spec/speed_spec.rb +1 -1
  37. data/spec/timezones_spec.rb +35 -14
  38. data/spec/type_safety_spec.rb +2 -2
  39. data/upsert.gemspec +18 -6
  40. metadata +25 -38
  41. data/lib/upsert/cell.rb +0 -5
  42. data/lib/upsert/cell/Mysql2_Client.rb +0 -16
  43. data/lib/upsert/cell/PG_Connection.rb +0 -28
  44. data/lib/upsert/cell/SQLite3_Database.rb +0 -36
  45. data/lib/upsert/column_definition/Mysql2_Client.rb +0 -24
  46. data/lib/upsert/column_definition/SQLite3_Database.rb +0 -7
  47. data/lib/upsert/row/Mysql2_Client.rb +0 -21
  48. data/lib/upsert/row/PG_Connection.rb +0 -7
  49. data/lib/upsert/row/SQLite3_Database.rb +0 -7
data/lib/upsert/binary.rb CHANGED
@@ -2,5 +2,7 @@ class Upsert
2
2
  # A wrapper class for binary strings so that Upsert knows to escape them as such.
3
3
  #
4
4
  # Create them with +Upsert.binary(x)+
5
+ #
6
+ # @private
5
7
  Binary = Struct.new(:value)
6
8
  end
@@ -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} #{sql_type}"
31
+ "#{quoted_selector_name} #{arg_type}"
29
32
  end
30
33
 
31
34
  def to_setter_arg
32
- "#{quoted_setter_name} #{sql_type}"
35
+ "#{quoted_setter_name} #{arg_type}"
33
36
  end
34
37
 
35
38
  def to_setter
36
- "#{quoted_name} = #{quoted_setter_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 PG_Connection < ColumnDefinition
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)
@@ -2,36 +2,34 @@ class Upsert
2
2
  # @private
3
3
  class Connection
4
4
  attr_reader :controller
5
- attr_reader :raw_connection
5
+ attr_reader :metal
6
6
 
7
- def initialize(controller, raw_connection)
7
+ def initialize(controller, metal)
8
8
  @controller = controller
9
- @raw_connection = raw_connection
9
+ @metal = metal
10
10
  end
11
-
12
- def quote_value(v)
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
- quote_time v # must be defined by base
26
+ Upsert.utc_iso8601 v
30
27
  when Date
31
- quote_string v.strftime(ISO8601_DATE)
28
+ v.strftime ISO8601_DATE
32
29
  else
33
- raise "not sure how to quote #{v.class}: #{v.inspect}"
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
- raw_connection.query sql
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 + raw_connection.escape(v) + 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 quote_time(v)
28
- quote_string v.strftime(ISO8601_DATETIME)
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 + raw_connection.escape(k.to_s) + 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
- raw_connection.exec sql, params
8
+ metal.exec sql, convert_binary(params)
9
9
  else
10
10
  Upsert.logger.debug { %{[upsert] #{sql}} }
11
- raw_connection.exec sql
11
+ metal.exec sql
12
12
  end
13
13
  end
14
14
 
15
15
  def quote_ident(k)
16
- raw_connection.quote_ident k.to_s
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
- raw_connection.execute sql, params
12
+ metal.execute sql, convert_binary(params)
9
13
  else
10
14
  Upsert.logger.debug { %{[upsert] #{sql}} }
11
- raw_connection.execute sql
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