upsert 1.0.2 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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