upsert 2.2.1 → 2.9.9

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 (41) hide show
  1. checksums.yaml +5 -5
  2. data/.ruby-version +1 -0
  3. data/.standard.yml +1 -0
  4. data/.travis.yml +54 -31
  5. data/CHANGELOG +9 -0
  6. data/Gemfile +12 -1
  7. data/LICENSE +3 -1
  8. data/README.md +35 -2
  9. data/Rakefile +7 -1
  10. data/lib/upsert.rb +49 -7
  11. data/lib/upsert/column_definition/mysql.rb +2 -2
  12. data/lib/upsert/column_definition/postgresql.rb +9 -8
  13. data/lib/upsert/column_definition/sqlite3.rb +3 -3
  14. data/lib/upsert/connection/Java_ComMysqlJdbc_JDBC4Connection.rb +5 -3
  15. data/lib/upsert/connection/Java_OrgPostgresqlJdbc_PgConnection.rb +33 -0
  16. data/lib/upsert/connection/PG_Connection.rb +5 -0
  17. data/lib/upsert/connection/jdbc.rb +7 -1
  18. data/lib/upsert/connection/postgresql.rb +2 -3
  19. data/lib/upsert/merge_function.rb +3 -2
  20. data/lib/upsert/merge_function/{Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb → Java_OrgPostgresqlJdbc_PgConnection.rb} +2 -2
  21. data/lib/upsert/merge_function/PG_Connection.rb +1 -1
  22. data/lib/upsert/merge_function/postgresql.rb +81 -19
  23. data/lib/upsert/merge_function/sqlite3.rb +10 -0
  24. data/lib/upsert/version.rb +1 -1
  25. data/spec/correctness_spec.rb +20 -5
  26. data/spec/database_functions_spec.rb +6 -2
  27. data/spec/hstore_spec.rb +53 -38
  28. data/spec/logger_spec.rb +1 -1
  29. data/spec/postgresql_spec.rb +81 -3
  30. data/spec/reserved_words_spec.rb +18 -14
  31. data/spec/sequel_spec.rb +16 -7
  32. data/spec/spec_helper.rb +238 -111
  33. data/spec/speed_spec.rb +3 -33
  34. data/spec/threaded_spec.rb +35 -12
  35. data/spec/type_safety_spec.rb +2 -1
  36. data/travis/run_docker_db.sh +20 -0
  37. data/upsert-java.gemspec +12 -0
  38. data/upsert.gemspec +9 -63
  39. data/upsert.gemspec.common +106 -0
  40. metadata +37 -44
  41. data/lib/upsert/connection/Java_OrgPostgresqlJdbc4_Jdbc4Connection.rb +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d0268cf71d5cdccb6336100af2ab5269affbec4c
4
- data.tar.gz: 889d7278a2b046ab5848c76cec79a94d1dfa4ec6
2
+ SHA256:
3
+ metadata.gz: 0d2ed0c63bb4539d42dcf15f20aaabdbca692c9fd5187c06aa76e1b1fc5d9655
4
+ data.tar.gz: 03bbc3ca8ebf200b3552bebe05a1fc13dbea187a023ad996691be02568ce02cb
5
5
  SHA512:
6
- metadata.gz: 60420f2b09bf34a80277104589cb7ef3cf376548c2f65fcef2a3bed6b11559b50e42bb4cbdf358d850f305b962dc421f4cb873da0fb5ab4b14c6edc372ef498c
7
- data.tar.gz: b4cbb08f7546fb13eb5eb784eb85c7ed3ecd93008f9cc0be31e05c17cc74ac160d5ea36bd0b7f39bd800982d35eff9bfeee00357b027d74142459fd99e7ed099
6
+ metadata.gz: 6c65b45f0d6305bea8c70a2046ccf045bebc29827d6a4d5bd859b3a034d2209e5230abf10f213aa9aebfbee76021c54f2e6bc138917ed280de7273662d4077a4
7
+ data.tar.gz: 486ea504857992b1711b8966d42f9996a14f27078fbe54809ebb4d7a53d18bed80d0f29f68a9992e3a5d6ab8fa3d61a1279375b1bb8efe6bca6453b49a3965a1
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.5
data/.standard.yml ADDED
@@ -0,0 +1 @@
1
+ ruby_version: 2.2.4
data/.travis.yml CHANGED
@@ -1,40 +1,63 @@
1
- sudo: required
2
- dist: trusty
1
+ dist: xenial
3
2
  language: ruby
4
- global:
5
- - USERNAME=travis
6
- - PASSWORD=
7
- addons:
8
- apt:
9
- packages:
10
- # https://github.com/travis-ci/docs-travis-ci-com/pull/743
11
- - haveged
12
- - mysql-server-5.6
13
- - mysql-client-core-5.6
14
- - mysql-client-5.6
3
+ cache: bundler
4
+ services:
5
+ - docker
15
6
  rvm:
7
+ - 2.6
8
+ - 2.5
9
+ - 2.4
16
10
  - 2.3
17
11
  - 2.2
18
- - 2.1
19
- - 1.9.3
20
- - rbx
21
- - jruby-1.7
22
- - jruby-9
12
+ - jruby-9.1.14.0
13
+ - jruby-9.1.17.0
14
+ - jruby-9.2.7.0
15
+ env:
16
+ global:
17
+ - USERNAME=travis
18
+ - PASSWORD=
19
+ - DB_USER=upsert_test
20
+ - DB_PASSWORD=upsert_test
21
+ - DB_NAME=upsert_test
22
+ matrix:
23
+ - DB=postgresql DB_VERSION=postgres:9.4
24
+ - DB=postgresql DB_VERSION=postgres:9.5
25
+ - DB=postgresql DB_VERSION=postgres:9.6
26
+ - DB=postgresql DB_VERSION=postgres:10
27
+ - DB=postgresql DB_VERSION=postgres:11
28
+ - DB=postgresql DB_VERSION=postgres:12
29
+ - DB=postgresql DB_VERSION=postgres:9.4 UNIQUE_CONSTRAINT=true
30
+ - DB=postgresql DB_VERSION=postgres:9.5 UNIQUE_CONSTRAINT=true
31
+ - DB=postgresql DB_VERSION=postgres:9.6 UNIQUE_CONSTRAINT=true
32
+ - DB=postgresql DB_VERSION=postgres:10 UNIQUE_CONSTRAINT=true
33
+ - DB=postgresql DB_VERSION=postgres:11 UNIQUE_CONSTRAINT=true
34
+ - DB=postgresql DB_VERSION=postgres:12 UNIQUE_CONSTRAINT=true
35
+ - DB=mysql DB_VERSION=mysql:5.6
36
+ - DB=mysql DB_VERSION=mysql:5.7
37
+ - DB=mysql DB_VERSION=mysql:8
23
38
  matrix:
39
+ exclude:
40
+ - rvm: 2.6
41
+ env: DB=postgresql DB_VERSION=postgres:9.4
42
+ - rvm: 2.6
43
+ env: DB=postgresql DB_VERSION=postgres:9.5
44
+ - rvm: 2.6
45
+ env: DB=postgresql DB_VERSION=postgres:9.4 UNIQUE_CONSTRAINT=true
46
+ - rvm: 2.6
47
+ env: DB=postgresql DB_VERSION=postgres:9.5 UNIQUE_CONSTRAINT=true
48
+ - rvm: jruby-9.2.7
49
+ env: DB=postgresql DB_VERSION=postgres:9.4
50
+ - rvm: jruby-9.2.7
51
+ env: DB=postgresql DB_VERSION=postgres:9.5
52
+ - rvm: jruby-9.2.7
53
+ env: DB=postgresql DB_VERSION=postgres:9.4 UNIQUE_CONSTRAINT=true
54
+ - rvm: jruby-9.2.7
55
+ env: DB=postgresql DB_VERSION=postgres:9.5 UNIQUE_CONSTRAINT=true
24
56
  allow_failures:
25
- - rvm: rbx
26
- env:
27
- - DB=postgresql PGVERSION=9.4
28
- - DB=postgresql PGVERSION=9.5
29
- - DB=postgresql PGVERSION=9.4 UNIQUE_CONSTRAINT=true
30
- - DB=postgresql PGVERSION=9.5 UNIQUE_CONSTRAINT=true
31
- - DB=mysql DB_USER=root
57
+ - env: DB=postgresql DB_VERSION=postgres:12
58
+ - env: DB=postgresql DB_VERSION=postgres:12 UNIQUE_CONSTRAINT=true
32
59
  before_install:
33
- - if [ "$DB" = 'mysql' ]; then sudo ./travis/tune_mysql.sh; fi
34
- # Right now the build-script is properly installing Postgres version. We will need this to test PG 9.6 and up, though
35
- # - if [ "$DB" = 'postgresql' ]; then sudo ./travis/install_postgres.sh; fi
36
- - gem update --system
37
- - gem update bundler
60
+ - ./travis/run_docker_db.sh
38
61
  - bundle --version
39
62
  - gem --version
40
- script: bundle exec rake spec
63
+ script: ./travis/run_specs.sh
data/CHANGELOG CHANGED
@@ -1,3 +1,12 @@
1
+ -- no version -- / 2019-06-05
2
+
3
+ * Enhancements
4
+
5
+ * Bump development Ruby version to 2.5 since Ruby 2.2 is no longer supported.
6
+ This should not affect usage of the gem, only local development for people
7
+ working *on* the gem. Ruby 2.2 is also not dropped from Upsert compatibility
8
+ at this time but you should consider upgrading to newer Ruby versions anyway.
9
+
1
10
  2.2.1 / 2017-04-20
2
11
 
3
12
  * Bug fixes
data/Gemfile CHANGED
@@ -2,4 +2,15 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in upsert.gemspec
4
4
 
5
- gemspec
5
+ gemspec name: RUBY_PLATFORM == "java" ? "upsert-java" : "upsert"
6
+
7
+ case RUBY_PLATFORM
8
+ when "java"
9
+ gem "ffi", platforms: :jruby
10
+ else
11
+ gem "ffi"
12
+ end
13
+
14
+ group "test" do
15
+ gem "testmetrics_rspec"
16
+ end
data/LICENSE CHANGED
@@ -1,4 +1,6 @@
1
- Copyright (c) 2013 Seamus Abshere
1
+ Copyright (c) 2013-2019 Seamus Abshere
2
+ Copyright (c) 2017-2019 Philip Schalm
3
+ Portions Copyright (c) 2019 The JRuby Team
2
4
 
3
5
  MIT License
4
6
 
data/README.md CHANGED
@@ -123,7 +123,10 @@ See below for details about what SQL MERGE trick (emulation of upsert) is used,
123
123
 
124
124
  ### Rails / ActiveRecord
125
125
 
126
- (assuming that one of the other three supported drivers is being used under the covers)
126
+ (Assuming that one of the other three supported drivers is being used under the covers).
127
+
128
+ * add "upsert" to your Gemfile and
129
+ * run bundle install
127
130
 
128
131
  ```ruby
129
132
  Upsert.new Pet.connection, Pet.table_name
@@ -232,6 +235,15 @@ require 'pg_hstore'
232
235
  upsert.row({:name => 'Bill'}, :mydata => {:a => 1, :b => 2})
233
236
  ```
234
237
 
238
+ #### PostgreSQL notes
239
+
240
+ - Upsert doesn't do any type casting, so if you attempt to do something like the following:
241
+ `upsert.row({ :name => 'A Name' }, :tag_number => 'bob')`
242
+ you'll get an error which reads something like:
243
+ `invalid input syntax for integer: "bob"`
244
+
245
+
246
+
235
247
  #### Speed
236
248
 
237
249
  From the tests (updated 9/21/12):
@@ -369,10 +381,31 @@ If you're using MySQL, make sure server/connection timezone is UTC. If you're us
369
381
 
370
382
  In general, run some upserts and make sure datetimes get persisted like you expect.
371
383
 
384
+ ### Clearning all library-generated functions
385
+
386
+ Place the following in to a rake task (so you don't globally redefine the `NAME_PREFIX` constant)
387
+
388
+ ```ruby
389
+ Upsert::MergeFunction::NAME_PREFIX = "upsert"
390
+
391
+ # ActiveRecord
392
+ Upsert.clear_database_functions(ActiveRecord::Base.connection)
393
+
394
+ # Sequel
395
+ DB.synchronize do |conn|
396
+ Upsert.clear_database_functions(conn)
397
+ end
398
+ ```
399
+
372
400
  ### Doesn't work with transactional fixtures
373
401
 
374
402
  Per https://github.com/seamusabshere/upsert/issues/23 you might have issues if you try to use transactional fixtures and this library.
375
403
 
404
+ ##
405
+ Testmetrics - https://www.testmetrics.app/seamusabshere/upsert
406
+
376
407
  ## Copyright
377
408
 
378
- Copyright 2014 Seamus Abshere
409
+ Copyright 2013-2019 Seamus Abshere
410
+ Copyright 2017-2019 Philip Schalm
411
+ Portions Copyright (c) 2019 The JRuby Team
data/Rakefile CHANGED
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env rake
2
- require "bundler/gem_tasks"
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
3
9
  require "rspec/core/rake_task"
4
10
 
5
11
  RSpec::Core::RakeTask.new(:spec) do |t|
data/lib/upsert.rb CHANGED
@@ -37,6 +37,31 @@ class Upsert
37
37
  end
38
38
  end
39
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
+
40
65
  # @param [Mysql2::Client,Sqlite3::Database,PG::Connection,#metal] connection A supported database connection.
41
66
  #
42
67
  # Clear any database functions that may have been created.
@@ -185,7 +210,7 @@ class Upsert
185
210
  # @param [Hash] options
186
211
  # @option options [TrueClass,FalseClass] :assume_function_exists (true) Assume the function has already been defined correctly by another process.
187
212
  def initialize(connection, table_name, options = {})
188
- @table_name = table_name.to_s
213
+ @table_name = self.class.normalize_table_name(table_name)
189
214
  metal = Upsert.metal connection
190
215
  @flavor = Upsert.flavor metal
191
216
  @adapter = Upsert.adapter metal
@@ -196,7 +221,10 @@ class Upsert
196
221
  @connection = Connection.const_get(adapter).new self, metal
197
222
  @merge_function_class = MergeFunction.const_get adapter
198
223
  @merge_function_cache = {}
199
- @assume_function_exists = options.fetch :assume_function_exists, true
224
+ @assume_function_exists = options.fetch :assume_function_exists, @flavor != "Postgresql"
225
+
226
+ @merge_function_mutex = Mutex.new
227
+ @row_mutex = Mutex.new
200
228
  end
201
229
 
202
230
  # Upsert a row given a selector and a setter.
@@ -216,8 +244,10 @@ class Upsert
216
244
  # upsert.row({:name => 'Pierre'}, :breed => 'tabby')
217
245
  def row(selector, setter = {}, options = nil)
218
246
  row_object = Row.new(selector, setter, options)
219
- merge_function(row_object).execute(row_object)
220
- nil
247
+ self.class.mutex_for_row(self, row_object).synchronize do
248
+ merge_function(row_object).execute(row_object)
249
+ nil
250
+ end
221
251
  end
222
252
 
223
253
  # @private
@@ -227,16 +257,28 @@ class Upsert
227
257
 
228
258
  def merge_function(row)
229
259
  cache_key = [row.selector.keys, row.setter.keys]
230
- @merge_function_cache[cache_key] ||= merge_function_class.new(self, row.selector.keys, row.setter.keys, assume_function_exists?)
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
231
264
  end
232
265
 
233
266
  # @private
234
267
  def quoted_table_name
235
- @quoted_table_name ||= connection.quote_ident table_name
268
+ @quoted_table_name ||= table_name.map { |t| connection.quote_ident(t) }.join(".")
236
269
  end
237
270
 
238
271
  # @private
239
272
  def column_definitions
240
- @column_definitions ||= ColumnDefinition.const_get(flavor).all connection, table_name
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
241
283
  end
242
284
  end
@@ -3,8 +3,8 @@ class Upsert
3
3
  # @private
4
4
  class Mysql < ColumnDefinition
5
5
  class << self
6
- def all(connection, table_name)
7
- connection.execute("SHOW COLUMNS FROM #{connection.quote_ident(table_name)}").map do |row|
6
+ def all(connection, quoted_table_name)
7
+ connection.execute("SHOW COLUMNS FROM #{quoted_table_name}").map do |row|
8
8
  # {"Field"=>"name", "Type"=>"varchar(255)", "Null"=>"NO", "Key"=>"PRI", "Default"=>nil, "Extra"=>""}
9
9
  name = row['Field'] || row['COLUMN_NAME'] || row[:Field] || row[:COLUMN_NAME]
10
10
  type = row['Type'] || row['COLUMN_TYPE'] || row[:Type] || row[:COLUMN_TYPE]
@@ -4,14 +4,14 @@ class Upsert
4
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
- def all(connection, table_name)
7
+ def all(connection, quoted_table_name)
8
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
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
11
  ON a.attrelid = d.adrelid AND a.attnum = d.adnum
12
- WHERE a.attrelid = '#{connection.quote_ident(table_name)}'::regclass
13
- AND a.attnum > 0 AND NOT a.attisdropped
14
- EOS
12
+ WHERE a.attrelid = '#{quoted_table_name}'::regclass
13
+ AND a.attnum > 0 AND NOT a.attisdropped
14
+ EOS
15
15
  res.map do |row|
16
16
  new connection, row['name'], row['sql_type'], row['default']
17
17
  end.sort_by do |cd|
@@ -19,7 +19,7 @@ EOS
19
19
  end
20
20
  end
21
21
  end
22
-
22
+
23
23
  # NOTE not using this because it can't be indexed
24
24
  # def equality(left, right)
25
25
  # "#{left} IS NOT DISTINCT FROM #{right}"
@@ -40,7 +40,8 @@ EOS
40
40
  if hstore?
41
41
  'text'
42
42
  else
43
- super
43
+ # JDBC uses prepared statements and properly sends date objects (which are otherwise escaped)
44
+ RUBY_PLATFORM == "java" ? sql_type : super
44
45
  end
45
46
  end
46
47
 
@@ -3,9 +3,9 @@ class Upsert
3
3
  # @private
4
4
  class Sqlite3 < ColumnDefinition
5
5
  class << self
6
- def all(connection, table_name)
6
+ def all(connection, quoted_table_name)
7
7
  # activerecord-3.2.13/lib/active_record/connection_adapters/sqlite_adapter.rb
8
- connection.execute("PRAGMA table_info(#{connection.quote_ident(table_name)})").map do |row|#, 'SCHEMA').to_hash
8
+ connection.execute("PRAGMA table_info(#{quoted_table_name})").map do |row|#, 'SCHEMA').to_hash
9
9
  if connection.metal.respond_to?(:results_as_hash) and not connection.metal.results_as_hash
10
10
  row = {'name' => row[1], 'type' => row[2], 'dflt_value' => row[4]}
11
11
  end
@@ -25,7 +25,7 @@ class Upsert
25
25
  end
26
26
  end
27
27
  end
28
-
28
+
29
29
  def equality(left, right)
30
30
  "(#{left} IS #{right} OR (#{left} IS NULL AND #{right} IS NULL))"
31
31
  end
@@ -17,9 +17,11 @@ class Upsert
17
17
 
18
18
  def bind_value(v)
19
19
  case v
20
- when Time, DateTime
21
- # mysql doesn't like it when you send timezone to a datetime
22
- Upsert.utc_iso8601 v, false
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)
23
25
  else
24
26
  super
25
27
  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