sequel-activerecord_connection 0.2.4 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a46fc2de408ac66a48628919f4030c847118c12fb7db1fc4060436fb05ce8976
4
- data.tar.gz: 4aa867bb6b6a18de97fdf69f213987d0d94aed960539a70ae12fa50e348609fa
3
+ metadata.gz: bb35cdbc0ade81ae15042834733f9ec4683b6889eab2c35412710d631a02de53
4
+ data.tar.gz: 75f97d5bee8522c12a7a3c65e05e1511f40ca4eccab8bc4b8cb2e1c3ece1a44a
5
5
  SHA512:
6
- metadata.gz: 3403df36d5e6741f4a7c2236ee479645aa580b0841f799fe1f0ad8b6ce0421344330857ff8afa52aafd4232f9cc1ba3e497c16b9e85615c7119be4bb19ef219c
7
- data.tar.gz: e5c12006fce490bf8500e575d45c5046f368c6904386dcd5503ce7f9bd2c7b75a172d331aadc81d99259776fbae6e07d3074f6d554dcbe3171add28ebd27b952
6
+ metadata.gz: 4fbf8edd6c9a61a8761431d33ba89c63a2aa444417d5ebec7b42ec9d3ef8aa5c509b28d35d708af223289c8e085b62204c3af690dc18c13e0df69f61a837b41d
7
+ data.tar.gz: 5f4408be86e1d09a856215ca6e1fd4ef594955549e52987107ff33fc8efb24c68f4fdaf03c0132bb22fe4e2023b31d5526dc77f225d476718528d101c2f6bb8c
@@ -1,7 +1,41 @@
1
+ ## 0.4.1 (2020-09-28)
2
+
3
+ * Require Sequel version 5.16.0 or above (@janko)
4
+
5
+ ## 0.4.0 (2020-09-28)
6
+
7
+ * Return correct result of `Database#in_transaction?` after ActiveRecord transaction exited (@janko)
8
+
9
+ * Make ActiveRecord create a savepoint inside a Sequel transaction with `auto_savepoint: true` (@janko)
10
+
11
+ * Make Sequel create a savepoint inside ActiveRecord transaction with `joinable: false` (@janko)
12
+
13
+ * Improve reliability of nested transactions when combining Sequel and ActiveRecord (@janko)
14
+
15
+ * Raise error when attempting to add an `after_commit`/`after_rollback` hook on ActiveRecord transaction (@janko)
16
+
17
+ * Fix infinite loop that could happen with transactional Rails tests (@janko)
18
+
19
+ ## 0.3.0 (2020-07-24)
20
+
21
+ * Fully support Sequel transaction API (all transaction options, transaction/savepoint hooks etc.) (@janko)
22
+
23
+ ## 0.2.6 (2020-07-19)
24
+
25
+ * Return block result in `Sequel::Database#transaction` (@zabolotnov87, @janko)
26
+
27
+ * Fix `Sequel::Model#save_changes` or `#save` with additional options not executing (@zabolotnov87, @janko)
28
+
29
+ ## 0.2.5 (2020-06-04)
30
+
31
+ * Use `#current_timestamp_utc` for the JDBC SQLite adapter as well (@HoneyryderChuck)
32
+
1
33
  ## 0.2.4 (2020-06-03)
2
34
 
3
35
  * Add JRuby support for ActiveRecord 6.0 and 5.2 (@HoneyryderChuck)
4
36
 
37
+ * Use `#current_timestamp_utc` setting for SQLite adapter on Sequel >= 5.33 (@HoneyryderChuck)
38
+
5
39
  ## 0.2.3 (2020-05-25)
6
40
 
7
41
  * Fix Ruby 2.7 kwargs warnings in `#transaction` (@HoneyryderChuck)
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  This is an extension for [Sequel] that allows it to reuse an existing
4
4
  ActiveRecord connection for database interaction. It works on ActiveRecord 4.2
5
5
  or higher, and supports the built-in `postgresql`, `mysql2` and `sqlite3`
6
- adapters.
6
+ adapters, as well as JDBC adapter for JRuby.
7
7
 
8
8
  This can be useful if you're using a library that uses Sequel for database
9
9
  interaction (e.g. [Rodauth]), but you want to avoid creating a separate
@@ -11,15 +11,15 @@ database connection. Or if you're transitioning from ActiveRecord to Sequel,
11
11
  and want the database connection to be shared.
12
12
 
13
13
  Note that this is a best-effort implementation, so some discrepancies are still
14
- possible. That being said, this implementation passes [Rodauth]'s test suite
15
- (for all adapters), which has fairly advanced Sequel usage.
14
+ possible. That being said, this implementation passes Rodauth's test suite
15
+ (for all adapters), which has some fairly advanced Sequel usage.
16
16
 
17
17
  ## Installation
18
18
 
19
19
  Add this line to your application's Gemfile:
20
20
 
21
21
  ```ruby
22
- gem "sequel-activerecord_connection"
22
+ gem "sequel-activerecord_connection", "~> 0.3"
23
23
  ```
24
24
 
25
25
  And then execute:
@@ -42,8 +42,7 @@ appropriate Sequel adapter and load the `activerecord_connection` extension:
42
42
  ```rb
43
43
  require "sequel"
44
44
 
45
- DB = Sequel.postgres(test: false) # avoid creating a connection
46
- DB.extension :activerecord_connection
45
+ DB = Sequel.postgres(extensions: :activerecord_connection)
47
46
  ```
48
47
 
49
48
  Now any Sequel operations that you make will internaly be done using the
@@ -75,70 +74,64 @@ ActiveRecord adapters, just make sure to initialize the corresponding Sequel
75
74
  adapter before loading the extension.
76
75
 
77
76
  ```rb
78
- DB = Sequel.postgres(test: false) # for "postgresql" adapter
77
+ DB = Sequel.postgres(extensions: :activerecord_connection) # for "postgresql" adapter
79
78
  # or
80
- DB = Sequel.mysql2(test: false) # for "mysql2" adapter
79
+ DB = Sequel.mysql2(extensions: :activerecord_connection) # for "mysql2" adapter
81
80
  # or
82
- DB = Sequel.sqlite(test: false) # for "sqlite3" adapter
81
+ DB = Sequel.sqlite(extensions: :activerecord_connection) # for "sqlite3" adapter
82
+ # or
83
+ DB = Sequel.jdbc(extensions: :activerecord_connection) # for JDBC adapter
83
84
  ```
84
85
 
85
86
  ### Transactions
86
87
 
87
- The database extension overrides Sequel transactions to use ActiveRecord
88
- transcations, which allows using ActiveRecord inside Sequel transactions (and
89
- vice-versa), and have things like ActiveRecord's transactional callbacks still
90
- work correctly.
88
+ This database extension keeps the transaction state of Sequel and ActiveRecord
89
+ in sync, allowing you to use Sequel and ActiveRecord transactions
90
+ interchangeably (including nesting them), and have things like ActiveRecord's
91
+ and Sequel's transactional callbacks still work correctly.
91
92
 
92
93
  ```rb
93
- DB.transaction do
94
- ActiveRecord::Base.transaction do
95
- # this all works
96
- end
94
+ ActiveRecord::Base.transaction do
95
+ DB.in_transaction? #=> true
97
96
  end
98
97
  ```
99
98
 
100
- The following Sequel transaction options are currently supported:
101
-
102
- * `:savepoint`
103
- * `:auto_savepoint`
104
- * `:rollback`
99
+ Sequel's transaction API is fully supported:
105
100
 
106
101
  ```rb
107
- ActiveRecord::Base.transaction do
108
- DB.transaction(savepoint: true) do # will create a savepoint
109
- DB.transaction do # will not create a savepoint
110
- # ...
111
- end
102
+ DB.transaction(isolation: :serializable) do
103
+ DB.after_commit { ... } # executed after transaction commits
104
+ DB.transaction(savepoint: true) do # creates a savepoint
105
+ DB.after_commit(savepoint: true) { ... } # executed if all enclosing savepoints have been released
112
106
  end
113
107
  end
114
108
  ```
115
109
 
116
- The `#in_transaction?` method is supported as well:
110
+ One caveat to keep in mind is that using Sequel's transaction/savepoint hooks
111
+ currently don't work if ActiveRecord holds the corresponding
112
+ transaction/savepoint. This is because it's difficult to be notified when
113
+ ActiveRecord commits or rolls back the transaction/savepoint.
117
114
 
118
115
  ```rb
119
- ActiveRecord::Base.transaction do
120
- DB.in_transaction? #=> true
116
+ DB.transaction do
117
+ DB.after_commit { ... } # will get executed
121
118
  end
122
- ```
123
-
124
- Other transaction-related Sequel methods (`#after_commit`, `#after_rollback`
125
- etc) are not supported, because ActiveRecord currently doesn't provide
126
- transactional callbacks on the connection level (only on the model level).
127
119
 
128
- ### Exceptions
129
-
130
- To ensure Sequel compatibility, any `ActiveRecord::StatementInvalid` exceptions
131
- will be translated into Sequel exceptions:
132
-
133
- ```rb
134
- DB[:posts].multi_insert [{ id: 1 }, { id: 1 }]
135
- #~> Sequel::UniqueConstraintViolation
120
+ DB.transaction do
121
+ DB.transaction(savepoint: true) do
122
+ DB.after_commit(savepoint: true) { ... } # will get executed
123
+ end
124
+ end
136
125
 
137
- DB[:posts].insert(title: nil)
138
- #~> Sequel::NotNullConstraintViolation
126
+ ActiveRecord::Base.transaction do
127
+ DB.after_commit { ... } # not allowed (will raise Sequel::ActiveRecordConnection::Error)
128
+ end
139
129
 
140
- DB[:posts].insert(author_id: 123)
141
- #~> Sequel::ForeignKeyConstraintViolation
130
+ DB.transaction do
131
+ ActiveRecord::Base.transaction(requires_new: true) do
132
+ DB.after_commit(savepoint: true) { ... } # not allowed (will raise Sequel::ActiveRecordConnection::Error)
133
+ end
134
+ end
142
135
  ```
143
136
 
144
137
  ### Model
@@ -1,7 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sequel
2
4
  module ActiveRecordConnection
3
5
  Error = Class.new(Sequel::Error)
4
6
 
7
+ TRANSACTION_ISOLATION_MAP = {
8
+ uncommitted: :read_uncommitted,
9
+ committed: :read_committed,
10
+ repeatable: :repeatable_read,
11
+ serializable: :serializable,
12
+ }
13
+
5
14
  def self.extended(db)
6
15
  db.activerecord_model = ActiveRecord::Base
7
16
  db.timezone = ActiveRecord::Base.default_timezone
@@ -21,57 +30,82 @@ module Sequel
21
30
  raise Error, "creating a Sequel connection is not allowed"
22
31
  end
23
32
 
24
- def transaction(options = {})
25
- savepoint = options.delete(:savepoint)
26
- rollback = options.delete(:rollback)
27
- auto_savepoint = options.delete(:auto_savepoint)
28
- server = options.delete(:server)
33
+ # Avoid calling Sequel's connection pool, instead use ActiveRecord.
34
+ def synchronize(*)
35
+ if ActiveRecord.version >= Gem::Version.new("5.1.0")
36
+ activerecord_connection.lock.synchronize do
37
+ yield activerecord_raw_connection
38
+ end
39
+ else
40
+ yield activerecord_raw_connection
41
+ end
42
+ end
29
43
 
30
- fail Error, "#{options} transaction options are currently not supported" unless options.empty?
44
+ private
31
45
 
32
- if in_transaction?
33
- requires_new = savepoint || Thread.current[:sequel_activerecord_auto_savepoint]
34
- else
35
- requires_new = true
46
+ # Synchronizes transaction state with ActiveRecord. Sequel uses this
47
+ # information to know whether we're in a transaction, whether to create a
48
+ # savepoint, when to run transaction/savepoint hooks etc.
49
+ def _trans(conn)
50
+ hash = super || { savepoints: [], activerecord: true }
51
+
52
+ # add any ActiveRecord transactions/savepoints that have been opened
53
+ # directly via ActiveRecord::Base.transaction
54
+ while hash[:savepoints].length < activerecord_connection.open_transactions
55
+ hash[:savepoints] << { activerecord: true }
36
56
  end
37
57
 
38
- activerecord_model.transaction(requires_new: requires_new) do
39
- begin
40
- Thread.current[:sequel_activerecord_auto_savepoint] = true if auto_savepoint
41
- yield
42
- rescue Sequel::Rollback => exception
43
- raise if rollback == :reraise
44
- raise ActiveRecord::Rollback, exception.message, exception.backtrace
45
- ensure
46
- Thread.current[:sequel_activerecord_auto_savepoint] = nil if auto_savepoint
47
- end
58
+ # remove any ActiveRecord transactions/savepoints that have been closed
59
+ # directly via ActiveRecord::Base.transaction
60
+ while hash[:savepoints].length > activerecord_connection.open_transactions && hash[:savepoints].last[:activerecord]
61
+ hash[:savepoints].pop
62
+ end
63
+
64
+ # sync knowledge about joinability of current ActiveRecord transaction/savepoint
65
+ if activerecord_connection.transaction_open? && !activerecord_connection.current_transaction.joinable?
66
+ hash[:savepoints].last[:auto_savepoint] = true
67
+ end
48
68
 
49
- raise ActiveRecord::Rollback if rollback == :always
69
+ if hash[:savepoints].empty? && hash[:activerecord]
70
+ Sequel.synchronize { @transactions.delete(conn) }
71
+ else
72
+ Sequel.synchronize { @transactions[conn] = hash }
50
73
  end
74
+
75
+ super
51
76
  end
52
77
 
53
- def in_transaction?(*)
54
- activerecord_connection.transaction_open?
78
+ def begin_transaction(conn, opts = {})
79
+ isolation = TRANSACTION_ISOLATION_MAP.fetch(opts[:isolation]) if opts[:isolation]
80
+ joinable = !opts[:auto_savepoint]
81
+
82
+ activerecord_connection.begin_transaction(isolation: isolation, joinable: joinable)
55
83
  end
56
84
 
57
- %i[after_commit after_rollback rollback_on_exit rollback_checker].each do |meth|
58
- define_method(meth) do |*|
59
- fail Error, "Database##{meth} is currently not supported"
60
- end
85
+ def commit_transaction(conn, opts = {})
86
+ activerecord_connection.commit_transaction
61
87
  end
62
88
 
63
- # Avoid calling Sequel's connection pool, instead use ActiveRecord.
64
- def synchronize(*)
65
- if ActiveRecord.version >= Gem::Version.new("5.1.0")
66
- activerecord_connection.lock.synchronize do
67
- yield activerecord_raw_connection
68
- end
69
- else
70
- yield activerecord_raw_connection
89
+ def rollback_transaction(conn, opts = {})
90
+ activerecord_connection.rollback_transaction
91
+ activerecord_connection.transaction_manager.send(:after_failure_actions, activerecord_connection.current_transaction, $!) if activerecord_connection.transaction_manager.respond_to?(:after_failure_actions)
92
+ end
93
+
94
+ def add_transaction_hook(conn, type, block)
95
+ if _trans(conn)[:activerecord]
96
+ fail Error, "cannot add transaction hook when ActiveRecord holds the outer transaction"
71
97
  end
98
+
99
+ super
72
100
  end
73
101
 
74
- private
102
+ def add_savepoint_hook(conn, type, block)
103
+ if _trans(conn)[:savepoints].last[:activerecord]
104
+ fail Error, "cannot add savepoint hook when ActiveRecord holds the current savepoint"
105
+ end
106
+
107
+ super
108
+ end
75
109
 
76
110
  def activerecord_raw_connection
77
111
  activerecord_connection.raw_connection
@@ -1,6 +1,12 @@
1
1
  module Sequel
2
2
  module ActiveRecordConnection
3
3
  module Jdbc
4
+ def self.extended(db)
5
+ if db.timezone == :utc && db.respond_to?(:current_timestamp_utc)
6
+ db.current_timestamp_utc = true
7
+ end
8
+ end
9
+
4
10
  def statement(conn)
5
11
  stmt = activerecord_raw_connection.connection.createStatement
6
12
  yield stmt
@@ -9,11 +9,21 @@ module Sequel
9
9
  else
10
10
  result.cmd_tuples
11
11
  end
12
+ rescue ActiveRecord::PreparedStatementCacheExpired
13
+ raise # ActiveRecord's transaction manager needs to handle this exception
12
14
  rescue ActiveRecord::StatementInvalid => exception
13
15
  raise_error(exception.cause, classes: database_error_classes)
14
16
  ensure
15
17
  result.clear if result
16
18
  end
19
+
20
+ def transaction(options = {})
21
+ %i[deferrable read_only synchronous].each do |key|
22
+ fail Error, "#{key.inspect} transaction option is currently not supported" if options.key?(key)
23
+ end
24
+
25
+ super
26
+ end
17
27
  end
18
28
  end
19
29
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "sequel-activerecord_connection"
3
- spec.version = "0.2.4"
3
+ spec.version = "0.4.1"
4
4
  spec.authors = ["Janko Marohnić"]
5
5
  spec.email = ["janko.marohnic@gmail.com"]
6
6
 
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
 
12
12
  spec.required_ruby_version = Gem::Requirement.new(">= 2.2.0")
13
13
 
14
- spec.add_dependency "sequel", ">= 4.0", "< 6"
14
+ spec.add_dependency "sequel", "~> 5.16"
15
15
  spec.add_dependency "activerecord", ">= 4.2", "< 7"
16
16
 
17
17
  spec.add_development_dependency "minitest"
metadata CHANGED
@@ -1,35 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel-activerecord_connection
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.4
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Janko Marohnić
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-03 00:00:00.000000000 Z
11
+ date: 2020-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
18
- - !ruby/object:Gem::Version
19
- version: '4.0'
20
- - - "<"
17
+ - - "~>"
21
18
  - !ruby/object:Gem::Version
22
- version: '6'
19
+ version: '5.16'
23
20
  type: :runtime
24
21
  prerelease: false
25
22
  version_requirements: !ruby/object:Gem::Requirement
26
23
  requirements:
27
- - - ">="
28
- - !ruby/object:Gem::Version
29
- version: '4.0'
30
- - - "<"
24
+ - - "~>"
31
25
  - !ruby/object:Gem::Version
32
- version: '6'
26
+ version: '5.16'
33
27
  - !ruby/object:Gem::Dependency
34
28
  name: activerecord
35
29
  requirement: !ruby/object:Gem::Requirement