sequel-activerecord_connection 0.2.3 → 0.4.0

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: 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