upsert 2.9.10-java

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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.ruby-version +1 -0
  4. data/.standard.yml +1 -0
  5. data/.travis.yml +63 -0
  6. data/.yardopts +2 -0
  7. data/CHANGELOG +265 -0
  8. data/Gemfile +20 -0
  9. data/LICENSE +24 -0
  10. data/README.md +411 -0
  11. data/Rakefile +54 -0
  12. data/lib/upsert.rb +284 -0
  13. data/lib/upsert/active_record_upsert.rb +12 -0
  14. data/lib/upsert/binary.rb +8 -0
  15. data/lib/upsert/column_definition.rb +79 -0
  16. data/lib/upsert/column_definition/mysql.rb +24 -0
  17. data/lib/upsert/column_definition/postgresql.rb +66 -0
  18. data/lib/upsert/column_definition/sqlite3.rb +34 -0
  19. data/lib/upsert/connection.rb +37 -0
  20. data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +31 -0
  21. data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
  22. data/lib/upsert/connection/Java_OrgSqlite_Conn.rb +17 -0
  23. data/lib/upsert/connection/Mysql2_Client.rb +76 -0
  24. data/lib/upsert/connection/PG_Connection.rb +35 -0
  25. data/lib/upsert/connection/SQLite3_Database.rb +28 -0
  26. data/lib/upsert/connection/jdbc.rb +105 -0
  27. data/lib/upsert/connection/postgresql.rb +24 -0
  28. data/lib/upsert/connection/sqlite3.rb +19 -0
  29. data/lib/upsert/merge_function.rb +73 -0
  30. data/lib/upsert/merge_function/Java_ComMysqlJdbc_JDBC4Connection.rb +42 -0
  31. data/lib/upsert/merge_function/Java_OrgPostgresqlJdbc_PgConnection.rb +27 -0
  32. data/lib/upsert/merge_function/Java_OrgSqlite_Conn.rb +10 -0
  33. data/lib/upsert/merge_function/Mysql2_Client.rb +36 -0
  34. data/lib/upsert/merge_function/PG_Connection.rb +26 -0
  35. data/lib/upsert/merge_function/SQLite3_Database.rb +10 -0
  36. data/lib/upsert/merge_function/mysql.rb +66 -0
  37. data/lib/upsert/merge_function/postgresql.rb +365 -0
  38. data/lib/upsert/merge_function/sqlite3.rb +43 -0
  39. data/lib/upsert/row.rb +59 -0
  40. data/lib/upsert/version.rb +3 -0
  41. data/spec/active_record_upsert_spec.rb +26 -0
  42. data/spec/binary_spec.rb +21 -0
  43. data/spec/correctness_spec.rb +190 -0
  44. data/spec/database_functions_spec.rb +106 -0
  45. data/spec/database_spec.rb +121 -0
  46. data/spec/hstore_spec.rb +249 -0
  47. data/spec/jruby_spec.rb +9 -0
  48. data/spec/logger_spec.rb +52 -0
  49. data/spec/misc/get_postgres_reserved_words.rb +12 -0
  50. data/spec/misc/mysql_reserved.txt +226 -0
  51. data/spec/misc/pg_reserved.txt +742 -0
  52. data/spec/multibyte_spec.rb +27 -0
  53. data/spec/postgresql_spec.rb +94 -0
  54. data/spec/precision_spec.rb +11 -0
  55. data/spec/reserved_words_spec.rb +50 -0
  56. data/spec/sequel_spec.rb +57 -0
  57. data/spec/spec_helper.rb +417 -0
  58. data/spec/speed_spec.rb +44 -0
  59. data/spec/threaded_spec.rb +57 -0
  60. data/spec/timezones_spec.rb +58 -0
  61. data/spec/type_safety_spec.rb +12 -0
  62. data/travis/install_postgres.sh +18 -0
  63. data/travis/run_docker_db.sh +20 -0
  64. data/travis/tune_mysql.sh +7 -0
  65. data/upsert-java.gemspec +14 -0
  66. data/upsert.gemspec +13 -0
  67. data/upsert.gemspec.common +106 -0
  68. metadata +373 -0
@@ -0,0 +1,66 @@
1
+ class Upsert
2
+ class ColumnDefinition
3
+ # @private
4
+ class Postgresql < ColumnDefinition
5
+ class << self
6
+ # activerecord-3.2.5/lib/active_record/connection_adapters/postgresql_adapter.rb#column_definitions
7
+ def all(connection, quoted_table_name)
8
+ res = connection.execute <<-EOS
9
+ SELECT a.attname AS name, format_type(a.atttypid, a.atttypmod) AS sql_type, d.adsrc AS default
10
+ FROM pg_attribute a LEFT JOIN pg_attrdef d
11
+ ON a.attrelid = d.adrelid AND a.attnum = d.adnum
12
+ WHERE a.attrelid = '#{quoted_table_name}'::regclass
13
+ AND a.attnum > 0 AND NOT a.attisdropped
14
+ EOS
15
+ res.map do |row|
16
+ new connection, row['name'], row['sql_type'], row['default']
17
+ end.sort_by do |cd|
18
+ cd.name
19
+ end
20
+ end
21
+ end
22
+
23
+ # NOTE not using this because it can't be indexed
24
+ # def equality(left, right)
25
+ # "#{left} IS NOT DISTINCT FROM #{right}"
26
+ # end
27
+
28
+ HSTORE_DETECTOR = /hstore/i
29
+
30
+ def initialize(*)
31
+ super
32
+ @hstore_query = !!(sql_type =~ HSTORE_DETECTOR)
33
+ end
34
+
35
+ def hstore?
36
+ @hstore_query
37
+ end
38
+
39
+ def arg_type
40
+ if hstore?
41
+ 'text'
42
+ else
43
+ # JDBC uses prepared statements and properly sends date objects (which are otherwise escaped)
44
+ RUBY_PLATFORM == "java" ? sql_type : super
45
+ end
46
+ end
47
+
48
+ def to_setter_value
49
+ if hstore?
50
+ "#{quoted_setter_name}::hstore"
51
+ else
52
+ super
53
+ end
54
+ end
55
+
56
+ def to_setter
57
+ if hstore?
58
+ # http://stackoverflow.com/questions/9317971/adding-a-key-to-an-empty-hstore-column
59
+ "#{quoted_name} = COALESCE(#{quoted_name}, hstore(array[]::varchar[])) || #{to_setter_value}"
60
+ else
61
+ super
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,34 @@
1
+ class Upsert
2
+ class ColumnDefinition
3
+ # @private
4
+ class Sqlite3 < ColumnDefinition
5
+ class << self
6
+ def all(connection, quoted_table_name)
7
+ # activerecord-3.2.13/lib/active_record/connection_adapters/sqlite_adapter.rb
8
+ connection.execute("PRAGMA table_info(#{quoted_table_name})").map do |row|#, 'SCHEMA').to_hash
9
+ if connection.metal.respond_to?(:results_as_hash) and not connection.metal.results_as_hash
10
+ row = {'name' => row[1], 'type' => row[2], 'dflt_value' => row[4]}
11
+ end
12
+ default = case row["dflt_value"]
13
+ when /^null$/i
14
+ nil
15
+ when /^'(.*)'$/
16
+ $1.gsub(/''/, "'")
17
+ when /^"(.*)"$/
18
+ $1.gsub(/""/, '"')
19
+ else
20
+ row["dflt_value"]
21
+ end
22
+ new connection, row['name'], row['type'], default
23
+ end.sort_by do |cd|
24
+ cd.name
25
+ end
26
+ end
27
+ end
28
+
29
+ def equality(left, right)
30
+ "(#{left} IS #{right} OR (#{left} IS NULL AND #{right} IS NULL))"
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,37 @@
1
+ class Upsert
2
+ # @private
3
+ class Connection
4
+ attr_reader :controller
5
+ attr_reader :metal
6
+
7
+ def initialize(controller, metal)
8
+ @controller = controller
9
+ @metal = metal
10
+ end
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)
24
+ case v
25
+ when Time, DateTime
26
+ Upsert.utc_iso8601 v
27
+ when Date
28
+ v.strftime ISO8601_DATE
29
+ when Symbol
30
+ v.to_s
31
+ else
32
+ v
33
+ end
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,31 @@
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
+ def quote_ident(k)
10
+ if metal.useAnsiQuotedIdentifiers
11
+ DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
12
+ else
13
+ # Escape backticks by doubling them. Ref http://dev.mysql.com/doc/refman/5.7/en/identifiers.html
14
+ BACKTICK + k.to_s.gsub(BACKTICK, BACKTICK + BACKTICK) + BACKTICK
15
+ end
16
+ end
17
+
18
+ def bind_value(v)
19
+ case v
20
+ when DateTime, Time
21
+ date = v.utc
22
+ java.time.LocalDateTime.of(date.year, date.month, date.day, date.hour, date.min, date.sec, date.nsec)
23
+ when Date
24
+ java.time.LocalDate.of(v.year, v.month, v.day)
25
+ else
26
+ super
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,33 @@
1
+ require_relative "jdbc"
2
+ require_relative "postgresql"
3
+
4
+ class Upsert
5
+ class Connection
6
+ # @private
7
+ class Java_OrgPostgresqlJdbc_PgConnection < Connection
8
+ include Jdbc
9
+ include Postgresql
10
+
11
+ def quote_ident(k)
12
+ DOUBLE_QUOTE + k.to_s.gsub(DOUBLE_QUOTE, '""') + DOUBLE_QUOTE
13
+ end
14
+
15
+ def in_transaction?
16
+ # https://github.com/kares/activerecord-jdbc-adapter/commit/4d6e0e0c52d12b0166810dffc9f898141a23bee6
17
+ ![0, 4].include?(metal.get_transaction_state)
18
+ end
19
+
20
+ def bind_value(v)
21
+ case v
22
+ when DateTime, Time
23
+ date = v.utc
24
+ java.time.LocalDateTime.of(date.year, date.month, date.day, date.hour, date.min, date.sec, date.nsec)
25
+ when Date
26
+ java.time.LocalDate.of(v.year, v.month, v.day)
27
+ else
28
+ super
29
+ end
30
+ end
31
+ end
32
+ end
33
+ 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_OrgSqlite_Conn < 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
@@ -0,0 +1,76 @@
1
+ class Upsert
2
+ class Connection
3
+ # @private
4
+ class Mysql2_Client < Connection
5
+ def execute(sql)
6
+ Upsert.logger.debug { %{[upsert] #{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
+ # mysql doesn't like it when you send timezone to a datetime
38
+ quote_string Upsert.utc_iso8601(v, false)
39
+ when Date
40
+ quote_date v
41
+ else
42
+ raise "not sure how to quote #{v.class}: #{v.inspect}"
43
+ end
44
+ end
45
+
46
+ def quote_boolean(v)
47
+ v ? 'TRUE' : 'FALSE'
48
+ end
49
+
50
+ def quote_string(v)
51
+ SINGLE_QUOTE + metal.escape(v) + SINGLE_QUOTE
52
+ end
53
+
54
+ # This doubles the size of the representation.
55
+ def quote_binary(v)
56
+ X_AND_SINGLE_QUOTE + v.unpack("H*")[0] + SINGLE_QUOTE
57
+ end
58
+
59
+ # put raw binary straight into sql
60
+ # might work if we could get the encoding issues fixed when joining together the values for the sql
61
+ # alias_method :quote_binary, :quote_string
62
+
63
+ def quote_date(v)
64
+ quote_string v.strftime(ISO8601_DATE)
65
+ end
66
+
67
+ def quote_ident(k)
68
+ BACKTICK + metal.escape(k.to_s) + BACKTICK
69
+ end
70
+
71
+ def quote_big_decimal(v)
72
+ v.to_s('F')
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,35 @@
1
+ require_relative "postgresql"
2
+
3
+ class Upsert
4
+ class Connection
5
+ # @private
6
+ class PG_Connection < Connection
7
+ include Postgresql
8
+
9
+ def execute(sql, params = nil)
10
+ if params
11
+ # Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
12
+ # The following will blow up if you pass a value that cannot be automatically type-casted,
13
+ # such as passing a string to an integer field. You'll get an error something along the
14
+ # lines of: "invalid input syntax for <type>: <value>"
15
+ metal.exec sql, convert_binary(params)
16
+ else
17
+ Upsert.logger.debug { %{[upsert] #{sql}} }
18
+ metal.exec sql
19
+ end
20
+ end
21
+
22
+ def quote_ident(k)
23
+ metal.quote_ident k.to_s
24
+ end
25
+
26
+ def binary(v)
27
+ { :value => v.value, :format => 1 }
28
+ end
29
+
30
+ def in_transaction?
31
+ ![PG::PQTRANS_IDLE, PG::PQTRANS_UNKNOWN].include?(metal.transaction_status)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ require 'upsert/connection/sqlite3'
2
+
3
+ class Upsert
4
+ class Connection
5
+ # @private
6
+ class SQLite3_Database < Connection
7
+ include Sqlite3
8
+
9
+ def execute(sql, params = nil)
10
+ if params
11
+ Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
12
+ metal.execute sql, convert_binary(params)
13
+ else
14
+ Upsert.logger.debug { %{[upsert] #{sql}} }
15
+ metal.execute sql
16
+ end
17
+ end
18
+
19
+ def quote_ident(k)
20
+ DOUBLE_QUOTE + SQLite3::Database.quote(k.to_s) + DOUBLE_QUOTE
21
+ end
22
+
23
+ def binary(v)
24
+ SQLite3::Blob.new v.value
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,105 @@
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::CHAR => 'getString',
8
+ java.sql.Types::VARCHAR => 'getString',
9
+ java.sql.Types::OTHER => 'getString', # ?! i guess unicode text?
10
+ java.sql.Types::BINARY => 'getBlob',
11
+ java.sql.Types::LONGVARCHAR => 'getString',
12
+ java.sql.Types::BIGINT => 'getLong',
13
+ java.sql.Types::INTEGER => 'getInt',
14
+ java.sql.Types::REAL => "getLong",
15
+ java.sql.Types::ARRAY => ->(r, i){ r.getArray(i).array.to_ary }
16
+ }
17
+ java.sql.Types.constants.each do |type_name|
18
+ i = java.sql.Types.const_get type_name
19
+ unless GETTER.has_key?(i)
20
+ GETTER[i] = 'get' + type_name[0].upcase + type_name[1..-1].downcase
21
+ end
22
+ end
23
+ SETTER = Hash.new do |hash, k|
24
+ hash[k] = 'set' + k
25
+ end.merge(
26
+ 'TrueClass' => 'setBoolean',
27
+ 'FalseClass' => 'setBoolean',
28
+ 'Fixnum' => 'setInt',
29
+ 'Integer' => 'setInt'
30
+ )
31
+
32
+ def binary(v)
33
+ v.value.to_java_bytes.java_object
34
+ end
35
+
36
+ def execute(sql, params = nil)
37
+ has_result = if params
38
+ Upsert.logger.debug { %{[upsert] #{sql} with #{params.inspect}} }
39
+ setters = self.class.const_get(:SETTER)
40
+ statement = metal.prepareStatement sql
41
+ params.each_with_index do |v, i|
42
+ if v.is_a?(Fixnum) && v > 2_147_483_647
43
+ statement.setLong i+1, v
44
+ next
45
+ end
46
+
47
+ case v
48
+ when Upsert::Binary
49
+ statement.setBytes i+1, binary(v)
50
+ when Float, BigDecimal
51
+ statement.setBigDecimal i+1, java.math.BigDecimal.new(v.to_s)
52
+ when NilClass
53
+ # http://stackoverflow.com/questions/4243513/why-does-preparedstatement-setnull-requires-sqltype
54
+ statement.setObject i+1, nil
55
+ when java.time.LocalDateTime, java.time.Instant, java.time.LocalDate
56
+ statement.setObject i+1, v
57
+ else
58
+ setter = setters[v.class.name]
59
+ Upsert.logger.debug { "Setting [#{v.class}, #{v}] via #{setter}" }
60
+ statement.send setter, i+1, v
61
+ end
62
+ end
63
+ statement.execute
64
+ else
65
+ Upsert.logger.debug { %{[upsert] #{sql}} }
66
+ statement = metal.createStatement
67
+ statement.execute sql
68
+ end
69
+ if not has_result
70
+ statement.close
71
+ return
72
+ end
73
+ getters = self.class.const_get(:GETTER)
74
+ raw_result = statement.getResultSet
75
+ meta = raw_result.getMetaData
76
+ count = meta.getColumnCount
77
+ column_name_and_getter = (1..count).inject({}) do |memo, i|
78
+ memo[i] = [ meta.getColumnName(i), getters[meta.getColumnType(i)] ]
79
+ memo
80
+ end
81
+ result = []
82
+ while raw_result.next
83
+ row = {}
84
+ column_name_and_getter.each do |i, cg|
85
+ column_name, getter = cg
86
+ if getter == 'getNull'
87
+ row[column_name] = nil
88
+ elsif getter.respond_to?(:call)
89
+ row[column_name] = getter.call(raw_result, i)
90
+ else
91
+ row[column_name] = raw_result.send(getter, i)
92
+ end
93
+ end
94
+ result << row
95
+ end
96
+ statement.close
97
+ result
98
+ end
99
+
100
+ def in_transaction?
101
+ raise "Not implemented"
102
+ end
103
+ end
104
+ end
105
+ end