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,54 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_helper"
3
+ case RUBY_PLATFORM
4
+ when "java"
5
+ Bundler::GemHelper.install_tasks name: "upsert-java"
6
+ else
7
+ Bundler::GemHelper.install_tasks name: "upsert"
8
+ end
9
+ require "rspec/core/rake_task"
10
+
11
+ RSpec::Core::RakeTask.new(:spec) do |t|
12
+ t.rspec_opts = "--format documentation"
13
+ end
14
+
15
+ task :default => :spec
16
+
17
+ task :rspec_all_databases do
18
+ results = {}
19
+
20
+ dbs = %w{ postgresql mysql sqlite3 }
21
+ if ENV['DB']
22
+ dbs = ENV['DB'].split(',')
23
+ end
24
+
25
+ dbs.each do |db|
26
+ puts
27
+ puts '#'*50
28
+ puts "# Running specs against #{db}"
29
+ puts '#'*50
30
+ puts
31
+
32
+ if RUBY_VERSION >= '1.9'
33
+ pid = spawn({'DB' => db}, 'rspec', '--format', 'documentation', File.expand_path('../spec', __FILE__))
34
+ Process.waitpid pid
35
+ results[db] = $?.success?
36
+ else
37
+ exec({'DB' => db}, 'rspec', '--format', 'documentation', File.expand_path('../spec', __FILE__))
38
+ end
39
+
40
+ end
41
+ puts results.inspect
42
+ end
43
+
44
+ task :n, :from, :to do |t, args|
45
+ Dir[File.expand_path("../lib/upsert/**/#{args.from}.*", __FILE__)].each do |path|
46
+ dir = File.dirname(path)
47
+ File.open("#{dir}/#{args.to}.rb", 'w') do |f|
48
+ f.write File.read(path).gsub(args.from, args.to)
49
+ end
50
+ end
51
+ end
52
+
53
+ require 'yard'
54
+ YARD::Rake::YardocTask.new
@@ -0,0 +1,284 @@
1
+ require 'bigdecimal'
2
+ require 'thread'
3
+ require 'logger'
4
+
5
+ require 'upsert/version'
6
+ require 'upsert/binary'
7
+ require 'upsert/connection'
8
+ require 'upsert/merge_function'
9
+ require 'upsert/column_definition'
10
+ require 'upsert/row'
11
+
12
+ class Upsert
13
+ class << self
14
+ # What logger to use.
15
+ # @return [#info,#warn,#debug]
16
+ attr_writer :logger
17
+ MUTEX_FOR_PERFORM = Mutex.new
18
+
19
+ # The current logger
20
+ # @return [#info,#warn,#debug]
21
+ def logger
22
+ @logger || MUTEX_FOR_PERFORM.synchronize do
23
+ @logger ||= if defined?(::Rails) and (rails_logger = ::Rails.logger)
24
+ rails_logger
25
+ elsif defined?(::ActiveRecord) and ::ActiveRecord.const_defined?(:Base) and (ar_logger = ::ActiveRecord::Base.logger)
26
+ ar_logger
27
+ else
28
+ my_logger = Logger.new $stderr
29
+ case ENV['UPSERT_DEBUG']
30
+ when 'true'
31
+ my_logger.level = Logger::DEBUG
32
+ when 'false'
33
+ my_logger.level = Logger::INFO
34
+ end
35
+ my_logger
36
+ end
37
+ end
38
+ end
39
+
40
+ def mutex_for_row(upsert, row)
41
+ retrieve_mutex(upsert.table_name, row.selector.keys)
42
+ end
43
+
44
+ def mutex_for_function(upsert, row)
45
+ retrieve_mutex(upsert.table_name, row.selector.keys, row.setter.keys)
46
+ end
47
+
48
+ # TODO: Rewrite this to use the thread_safe gem, perhaps?
49
+ def retrieve_mutex(*args)
50
+ # ||= isn't an atomic operation
51
+ MUTEX_FOR_PERFORM.synchronize do
52
+ @mutex_cache ||= {}
53
+ end
54
+
55
+ @mutex_cache.fetch(args.flatten.join('::')) do |k|
56
+ MUTEX_FOR_PERFORM.synchronize do
57
+ # We still need the ||= because this block could have
58
+ # theoretically been entered simultaneously by two threads
59
+ # but the actual assignment is protected by the mutex
60
+ @mutex_cache[k] ||= Mutex.new
61
+ end
62
+ end
63
+ end
64
+
65
+ # @param [Mysql2::Client,Sqlite3::Database,PG::Connection,#metal] connection A supported database connection.
66
+ #
67
+ # Clear any database functions that may have been created.
68
+ #
69
+ # Currently only applies to PostgreSQL.
70
+ def clear_database_functions(connection)
71
+ dummy = new(connection, :dummy)
72
+ dummy.clear_database_functions
73
+ end
74
+
75
+ # @param [String] v A string containing binary data that should be inserted/escaped as such.
76
+ #
77
+ # @return [Upsert::Binary]
78
+ def binary(v)
79
+ Binary.new v
80
+ end
81
+
82
+ # More efficient way of upserting multiple rows at once.
83
+ #
84
+ # @param [Mysql2::Client,Sqlite3::Database,PG::Connection,#metal] connection A supported database connection.
85
+ # @param [String,Symbol] table_name The name of the table into which you will be upserting.
86
+ #
87
+ # @yield [Upsert] An +Upsert+ object in batch mode. You can call #row on it multiple times and it will try to optimize on speed.
88
+ #
89
+ # @return [nil]
90
+ #
91
+ # @example Many at once
92
+ # Upsert.batch(Pet.connection, Pet.table_name) do |upsert|
93
+ # upsert.row({:name => 'Jerry'}, :breed => 'beagle')
94
+ # upsert.row({:name => 'Pierre'}, :breed => 'tabby')
95
+ # end
96
+ def batch(connection, table_name, options = {})
97
+ upsert = new connection, table_name, options
98
+ yield upsert
99
+ end
100
+
101
+ # @deprecated Use .batch instead.
102
+ alias :stream :batch
103
+
104
+ # @private
105
+ def class_name(metal)
106
+ if RUBY_PLATFORM == 'java'
107
+ metal.class.name || metal.get_class.name
108
+ else
109
+ metal.class.name
110
+ end
111
+ end
112
+
113
+ # @private
114
+ def flavor(metal)
115
+ case class_name(metal)
116
+ when /sqlite/i
117
+ 'Sqlite3'
118
+ when /mysql/i
119
+ 'Mysql'
120
+ when /pg/i, /postgres/i
121
+ 'Postgresql'
122
+ else
123
+ raise "[upsert] #{metal} not supported"
124
+ end
125
+ end
126
+
127
+ # @private
128
+ def adapter(metal)
129
+ metal_class_name = class_name metal
130
+ METAL_CLASS_ALIAS.fetch(metal_class_name, metal_class_name).gsub /\W+/, '_'
131
+ end
132
+
133
+ # @private
134
+ def metal(connection)
135
+ metal = connection.respond_to?(:raw_connection) ? connection.raw_connection : connection
136
+ if metal.class.name.to_s.start_with?('ActiveRecord::ConnectionAdapters')
137
+ metal = metal.connection
138
+ end
139
+ metal
140
+ end
141
+
142
+ # @private
143
+ def utc(time)
144
+ if time.is_a? DateTime
145
+ usec = time.sec_fraction * SEC_FRACTION
146
+ if time.offset != 0
147
+ time = time.new_offset(0)
148
+ end
149
+ Time.utc time.year, time.month, time.day, time.hour, time.min, time.sec, usec
150
+ elsif time.utc?
151
+ time
152
+ else
153
+ time.utc
154
+ end
155
+ end
156
+
157
+ # @private
158
+ def utc_iso8601(time, tz = true)
159
+ t = utc time
160
+ s = t.strftime(ISO8601_DATETIME) + '.' + (USEC_SPRINTF % t.usec)
161
+ tz ? (s + UTC_TZ) : s
162
+ end
163
+ end
164
+
165
+ SINGLE_QUOTE = %{'}
166
+ DOUBLE_QUOTE = %{"}
167
+ BACKTICK = %{`}
168
+ X_AND_SINGLE_QUOTE = %{x'}
169
+ USEC_SPRINTF = '%06d'
170
+ if RUBY_VERSION >= '1.9.0'
171
+ SEC_FRACTION = 1e6
172
+ NANO_FRACTION = 1e9
173
+ else
174
+ SEC_FRACTION = 8.64e10
175
+ NANO_FRACTION = 8.64e13
176
+ end
177
+ ISO8601_DATETIME = '%Y-%m-%d %H:%M:%S'
178
+ ISO8601_DATE = '%F'
179
+ UTC_TZ = '+00:00'
180
+ NULL_WORD = 'NULL'
181
+ METAL_CLASS_ALIAS = {
182
+ 'PGConn' => 'PG::Connection',
183
+ 'org.sqlite.Conn' => 'Java::OrgSqlite::Conn', # for some reason, org.sqlite.Conn doesn't have a ruby class name
184
+ 'Sequel::Postgres::Adapter' => 'PG::Connection', # Only the Postgres adapter needs an alias
185
+ }
186
+ CREATED_COL_REGEX = /\Acreated_(at|on)\z/
187
+
188
+ # @return [Upsert::Connection]
189
+ attr_reader :connection
190
+
191
+ # @return [String]
192
+ attr_reader :table_name
193
+
194
+ # @private
195
+ attr_reader :merge_function_class
196
+
197
+ # @private
198
+ attr_reader :flavor
199
+
200
+ # @private
201
+ attr_reader :adapter
202
+
203
+ # @private
204
+ def assume_function_exists?
205
+ @assume_function_exists
206
+ end
207
+
208
+ # @param [Mysql2::Client,Sqlite3::Database,PG::Connection,#metal] connection A supported database connection.
209
+ # @param [String,Symbol] table_name The name of the table into which you will be upserting.
210
+ # @param [Hash] options
211
+ # @option options [TrueClass,FalseClass] :assume_function_exists (true) Assume the function has already been defined correctly by another process.
212
+ def initialize(connection, table_name, options = {})
213
+ @table_name = self.class.normalize_table_name(table_name)
214
+ metal = Upsert.metal connection
215
+ @flavor = Upsert.flavor metal
216
+ @adapter = Upsert.adapter metal
217
+ # todo memoize
218
+ Dir[File.expand_path("../upsert/**/{#{flavor.downcase},#{adapter}}.rb", __FILE__)].each do |path|
219
+ require path
220
+ end
221
+ @connection = Connection.const_get(adapter).new self, metal
222
+ @merge_function_class = MergeFunction.const_get adapter
223
+ @merge_function_cache = {}
224
+ @assume_function_exists = options.fetch :assume_function_exists, @flavor != "Postgresql"
225
+
226
+ @merge_function_mutex = Mutex.new
227
+ @row_mutex = Mutex.new
228
+ end
229
+
230
+ # Upsert a row given a selector and a setter.
231
+ #
232
+ # The selector values are used as setters if it's a new row. So if your selector is `name=Jerry` and your setter is `age=4`, and there is no Jerry yet, then a new row will be created with name Jerry and age 4.
233
+ #
234
+ # @see http://api.mongodb.org/ruby/1.6.4/Mongo/Collection.html#update-instance_method Loosely based on the upsert functionality of the mongo-ruby-driver #update method
235
+ #
236
+ # @param [Hash] selector Key-value pairs that will be used to find or create a row.
237
+ # @param [Hash] setter Key-value pairs that will be set on the row, whether it previously existed or not.
238
+ #
239
+ # @return [nil]
240
+ #
241
+ # @example One at a time
242
+ # upsert = Upsert.new Pet.connection, Pet.table_name
243
+ # upsert.row({:name => 'Jerry'}, :breed => 'beagle')
244
+ # upsert.row({:name => 'Pierre'}, :breed => 'tabby')
245
+ def row(selector, setter = {}, options = nil)
246
+ row_object = Row.new(selector, setter, options)
247
+ self.class.mutex_for_row(self, row_object).synchronize do
248
+ merge_function(row_object).execute(row_object)
249
+ nil
250
+ end
251
+ end
252
+
253
+ # @private
254
+ def clear_database_functions
255
+ merge_function_class.clear connection
256
+ end
257
+
258
+ def merge_function(row)
259
+ cache_key = [row.selector.keys, row.setter.keys]
260
+ self.class.mutex_for_function(self, row).synchronize do
261
+ @merge_function_cache[cache_key] ||=
262
+ merge_function_class.new(self, row.selector.keys, row.setter.keys, assume_function_exists?)
263
+ end
264
+ end
265
+
266
+ # @private
267
+ def quoted_table_name
268
+ @quoted_table_name ||= table_name.map { |t| connection.quote_ident(t) }.join(".")
269
+ end
270
+
271
+ # @private
272
+ def column_definitions
273
+ @column_definitions ||= ColumnDefinition.const_get(flavor).all connection, quoted_table_name
274
+ end
275
+
276
+ # @private
277
+ def self.normalize_table_name(table_name)
278
+ if defined?(Sequel) && table_name.is_a?(::Sequel::SQL::QualifiedIdentifier)
279
+ [table_name.table, table_name.column]
280
+ else
281
+ [*table_name].map(&:to_s)
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,12 @@
1
+ class Upsert
2
+ module ActiveRecordUpsert
3
+ def upsert(selector, setter = {})
4
+ ActiveRecord::Base.connection_pool.with_connection do |c|
5
+ upsert = Upsert.new c, table_name
6
+ upsert.row selector, setter
7
+ end
8
+ end
9
+ end
10
+ end
11
+
12
+ ActiveRecord::Base.extend Upsert::ActiveRecordUpsert
@@ -0,0 +1,8 @@
1
+ class Upsert
2
+ # A wrapper class for binary strings so that Upsert knows to escape them as such.
3
+ #
4
+ # Create them with +Upsert.binary(x)+
5
+ #
6
+ # @private
7
+ Binary = Struct.new(:value)
8
+ end
@@ -0,0 +1,79 @@
1
+ class Upsert
2
+ # @private
3
+ class ColumnDefinition
4
+ class << self
5
+ # activerecord-3.2.X/lib/active_record/connection_adapters/XXXXXXXXX_adapter.rb#column_definitions
6
+ def all(connection, table_name)
7
+ raise "not impl"
8
+ end
9
+ end
10
+
11
+ TIME_DETECTOR = /date|time/i
12
+
13
+ attr_reader :name
14
+ attr_reader :sql_type
15
+ attr_reader :default
16
+ attr_reader :quoted_name
17
+ attr_reader :quoted_selector_name
18
+ attr_reader :quoted_setter_name
19
+
20
+ def initialize(connection, name, sql_type, default)
21
+ @name = name
22
+ @sql_type = sql_type
23
+ @temporal_query = !!(sql_type =~ TIME_DETECTOR)
24
+ @default = default
25
+ @quoted_name = connection.quote_ident name
26
+ @quoted_selector_name = connection.quote_ident "#{name}_sel"
27
+ @quoted_setter_name = connection.quote_ident "#{name}_set"
28
+ end
29
+
30
+ def to_selector_arg
31
+ "#{quoted_selector_name} #{arg_type}"
32
+ end
33
+
34
+ def to_setter_arg
35
+ "#{quoted_setter_name} #{arg_type}"
36
+ end
37
+
38
+ def to_setter
39
+ "#{quoted_name} = #{to_setter_value}"
40
+ end
41
+
42
+ def to_selector
43
+ equality(quoted_name, to_selector_value)
44
+ end
45
+
46
+ def temporal?
47
+ @temporal_query
48
+ end
49
+
50
+ def equality(left, right)
51
+ "(#{left} = #{right} OR (#{left} IS NULL AND #{right} IS NULL))"
52
+ end
53
+
54
+ def arg_type
55
+ if temporal?
56
+ 'character varying(255)'
57
+ else
58
+ sql_type
59
+ end
60
+ end
61
+
62
+ def to_setter_value
63
+ if temporal?
64
+ "CAST(#{quoted_setter_name} AS #{sql_type})"
65
+ else
66
+ quoted_setter_name
67
+ end
68
+ end
69
+
70
+ def to_selector_value
71
+ if temporal?
72
+ "CAST(#{quoted_selector_name} AS #{sql_type})"
73
+ else
74
+ quoted_selector_name
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,24 @@
1
+ class Upsert
2
+ class ColumnDefinition
3
+ # @private
4
+ class Mysql < ColumnDefinition
5
+ class << self
6
+ def all(connection, quoted_table_name)
7
+ connection.execute("SHOW COLUMNS FROM #{quoted_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'] || row[:Field] || row[:COLUMN_NAME]
10
+ type = row['Type'] || row['COLUMN_TYPE'] || row[:Type] || row[:COLUMN_TYPE]
11
+ default = row['Default'] || row['COLUMN_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
+
19
+ def equality(left, right)
20
+ "#{left} <=> #{right}"
21
+ end
22
+ end
23
+ end
24
+ end