sequel-activerecord_connection 0.2.3 → 0.4.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af03da0244bb9eedd77b19377ee8ced698c347c1e91af8457956589a3960bc08
4
- data.tar.gz: ddb5b1d45042644b0caed6f0fce07aaec872cfb4e7ed6456ea4e536a4af6833f
3
+ metadata.gz: f8e137f0937e4f17511d40529df10648276b7dae834cef50f4bc6615206c486b
4
+ data.tar.gz: e25287b365a78b34c55afb084b0ebad4a24c8c00944323c1a2b455505fd4a251
5
5
  SHA512:
6
- metadata.gz: a08b930bdbd1262d4f4299e21ff5d92812ad2ef8c30de9b36963c22ae8a39666d308b9fe1a8fbf1749e412687c4f149e38b5b0dac12b74923036c4c674a5db53
7
- data.tar.gz: e5541c627333193ef80b5f74c2f2714137b15f8deff876e25503c89ddf4650f4db7ee12b9d30b8927fa3d03418934111277720541d43d3359458776f437957ff
6
+ metadata.gz: '033781eeae71623aa992f2a882c04a1fd718109435b3aa428b0710df4322f1f267664abb4670a9909e3aefa570029a37c8a6a6947d54d2b38e119b80c938245b'
7
+ data.tar.gz: c013ca47766a5cfae058353094df8a88e74a992de924f7b2107c6e50a5f76af72c74fab0f957032666f7b3442b80af886ca56f5bd3635d67c141fa67ff7be5d8
@@ -0,0 +1,51 @@
1
+ ## 0.4.0 (2020-09-28)
2
+
3
+ * Return correct result of `Database#in_transaction?` after ActiveRecord transaction exited (@janko)
4
+
5
+ * Make ActiveRecord create a savepoint inside a Sequel transaction with `auto_savepoint: true` (@janko)
6
+
7
+ * Make Sequel create a savepoint inside ActiveRecord transaction with `joinable: false` (@janko)
8
+
9
+ * Improve reliability of nested transactions when combining Sequel and ActiveRecord (@janko)
10
+
11
+ * Raise error when attempting to add an `after_commit`/`after_rollback` hook on ActiveRecord transaction (@janko)
12
+
13
+ * Fix infinite loop that could happen with transactional Rails tests (@janko)
14
+
15
+ ## 0.3.0 (2020-07-24)
16
+
17
+ * Fully support Sequel transaction API (all transaction options, transaction/savepoint hooks etc.) (@janko)
18
+
19
+ ## 0.2.6 (2020-07-19)
20
+
21
+ * Return block result in `Sequel::Database#transaction` (@zabolotnov87, @janko)
22
+
23
+ * Fix `Sequel::Model#save_changes` or `#save` with additional options not executing (@zabolotnov87, @janko)
24
+
25
+ ## 0.2.5 (2020-06-04)
26
+
27
+ * Use `#current_timestamp_utc` for the JDBC SQLite adapter as well (@HoneyryderChuck)
28
+
29
+ ## 0.2.4 (2020-06-03)
30
+
31
+ * Add JRuby support for ActiveRecord 6.0 and 5.2 (@HoneyryderChuck)
32
+
33
+ * Use `#current_timestamp_utc` setting for SQLite adapter on Sequel >= 5.33 (@HoneyryderChuck)
34
+
35
+ ## 0.2.3 (2020-05-25)
36
+
37
+ * Fix Ruby 2.7 kwargs warnings in `#transaction` (@HoneyryderChuck)
38
+
39
+ ## 0.2.2 (2020-05-02)
40
+
41
+ * Add support for ActiveRecord 4.2 (@janko)
42
+
43
+ ## 0.2.1 (2020-05-02)
44
+
45
+ * Add support for Active Record 5.0, 5.1 and 5.2 (@janko)
46
+
47
+ * Allow Sequel 4.x (@janko)
48
+
49
+ ## 0.2.0 (2020-04-29)
50
+
51
+ * Rename to `sequel-activerecord_connection` and make it a Sequel extension (@janko)
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
@@ -0,0 +1,38 @@
1
+ module Sequel
2
+ module ActiveRecordConnection
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
+
10
+ def statement(conn)
11
+ stmt = activerecord_raw_connection.connection.createStatement
12
+ yield stmt
13
+ rescue ActiveRecord::StatementInvalid => exception
14
+ raise_error(exception.cause, classes: database_error_classes)
15
+ rescue *database_error_classes => e
16
+ raise_error(e, classes: database_error_classes)
17
+ ensure
18
+ stmt.close if stmt
19
+ end
20
+
21
+ def execute(sql, opts=OPTS)
22
+ activerecord_connection.send(:log, sql) do
23
+ super
24
+ end
25
+ rescue ActiveRecord::StatementInvalid => exception
26
+ raise_error(exception.cause, classes: database_error_classes)
27
+ end
28
+
29
+ def execute_dui(sql, opts=OPTS)
30
+ activerecord_connection.send(:log, sql) do
31
+ super
32
+ end
33
+ rescue ActiveRecord::StatementInvalid => exception
34
+ raise_error(exception.cause, classes: database_error_classes)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -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,12 @@
1
1
  module Sequel
2
2
  module ActiveRecordConnection
3
3
  module Sqlite
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 execute_ddl(sql, opts=OPTS)
5
11
  execute(sql, opts)
6
12
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "sequel-activerecord_connection"
3
- spec.version = "0.2.3"
3
+ spec.version = "0.4.0"
4
4
  spec.authors = ["Janko Marohnić"]
5
5
  spec.email = ["janko.marohnic@gmail.com"]
6
6
 
@@ -14,9 +14,6 @@ Gem::Specification.new do |spec|
14
14
  spec.add_dependency "sequel", ">= 4.0", "< 6"
15
15
  spec.add_dependency "activerecord", ">= 4.2", "< 7"
16
16
 
17
- spec.add_development_dependency "pg", "~> 1.0"
18
- spec.add_development_dependency "mysql2", "~> 0.5"
19
- spec.add_development_dependency "sqlite3", "~> 1.3"
20
17
  spec.add_development_dependency "minitest"
21
18
 
22
19
  spec.files = Dir["README.md", "LICENSE.txt", "CHANGELOG.md", "lib/**/*.rb", "*.gemspec"]
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sequel-activerecord_connection
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.4.0
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-05-25 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
@@ -50,48 +50,6 @@ dependencies:
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
52
  version: '7'
53
- - !ruby/object:Gem::Dependency
54
- name: pg
55
- requirement: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - "~>"
58
- - !ruby/object:Gem::Version
59
- version: '1.0'
60
- type: :development
61
- prerelease: false
62
- version_requirements: !ruby/object:Gem::Requirement
63
- requirements:
64
- - - "~>"
65
- - !ruby/object:Gem::Version
66
- version: '1.0'
67
- - !ruby/object:Gem::Dependency
68
- name: mysql2
69
- requirement: !ruby/object:Gem::Requirement
70
- requirements:
71
- - - "~>"
72
- - !ruby/object:Gem::Version
73
- version: '0.5'
74
- type: :development
75
- prerelease: false
76
- version_requirements: !ruby/object:Gem::Requirement
77
- requirements:
78
- - - "~>"
79
- - !ruby/object:Gem::Version
80
- version: '0.5'
81
- - !ruby/object:Gem::Dependency
82
- name: sqlite3
83
- requirement: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - "~>"
86
- - !ruby/object:Gem::Version
87
- version: '1.3'
88
- type: :development
89
- prerelease: false
90
- version_requirements: !ruby/object:Gem::Requirement
91
- requirements:
92
- - - "~>"
93
- - !ruby/object:Gem::Version
94
- version: '1.3'
95
53
  - !ruby/object:Gem::Dependency
96
54
  name: minitest
97
55
  requirement: !ruby/object:Gem::Requirement
@@ -113,9 +71,11 @@ executables: []
113
71
  extensions: []
114
72
  extra_rdoc_files: []
115
73
  files:
74
+ - CHANGELOG.md
116
75
  - LICENSE.txt
117
76
  - README.md
118
77
  - lib/sequel/extensions/activerecord_connection.rb
78
+ - lib/sequel/extensions/activerecord_connection/jdbc.rb
119
79
  - lib/sequel/extensions/activerecord_connection/mysql2.rb
120
80
  - lib/sequel/extensions/activerecord_connection/postgres.rb
121
81
  - lib/sequel/extensions/activerecord_connection/sqlite.rb