upsert 2.2.1 → 2.9.9

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