sequel-activerecord_connection 0.3.0 → 1.1.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: d4078a41f54331cbb97fc266abdbe24cf8c297be14bb7f92b3bf605bf9b549bd
4
- data.tar.gz: af466cd9b062279a7781106136a05151f02339830469136cdf9b58d629397b98
3
+ metadata.gz: fba99c145ebf98ed9c3e26fdc96afc3bc5fa7820e01d466ab10320c72765c8ae
4
+ data.tar.gz: ae36bef3a63280cf54f1a92e814c1bf4928dc8050f572694aac4340389115f9b
5
5
  SHA512:
6
- metadata.gz: e28881de46188d6dc143952a473e00254b0742c497e7888e8d334ff33b627d3d90d9174675809cc3248b15cfcc800908bc83dda5a8da3fee93c4048c44282424
7
- data.tar.gz: 7c0a46d18a45e76ca4a7af11d467b957035dd4b62adbdcaf64d8e9cafe1bfb406fa40c3d4feadff10c823d2535cab978111203ade32ea519f991e828ed1df033
6
+ metadata.gz: 073babf3f4e9d60dcb439eb0d9a7b801b9cf23b9be53366382cdd9e18234de66c329d0a975e68fb57d78cbf80f26ddafcd3f1bc90e8282a9a24b90cd1f8c0a43
7
+ data.tar.gz: 420a6326c6c0e0b85874b52c89e747a59702280e763d9f6a090dd649b17718771cefaa1d9d72d4244a92bcfbee7ff2c186480ad0005d17685abcd0a595ccb97c
@@ -1,3 +1,63 @@
1
+ ## 1.1.0 (2020-11-08)
2
+
3
+ * Drop support for Ruby 2.2 (@janko)
4
+
5
+ * Support transaction/savepoint hooks even when Active Record holds the transaction/savepoint (@janko)
6
+
7
+ * Don't test the connection on `Sequel.connect` by default (@janko)
8
+
9
+ ## 1.0.1 (2020-10-28)
10
+
11
+ * Use Active Record connection lock in `Database#synchronize` (@janko)
12
+
13
+ ## 1.0.0 (2020-10-25)
14
+
15
+ * Clear AR statement cache on `ActiveRecord::PreparedStatementCacheExpired` when Sequel holds the transaction (@janko)
16
+
17
+ * Pick up `ActiveRecord::Base.default_timezone` being changed on runtime (@janko)
18
+
19
+ * Support prepared statements and bound variables in all adapters (@janko)
20
+
21
+ * Correctly identify identity columns as primary keys in Postgres adapter (@janko)
22
+
23
+ * Avoid using deprecated `sqlite3` API in SQLite adapter (@janko)
24
+
25
+ * Allow using any external Active Record adapters (@janko)
26
+
27
+ * Avoid potential bugs when converting Active Record exceptions into Sequel exceptions (@janko)
28
+
29
+ * Don't use Active Record locks when executing queries with Sequel (@janko)
30
+
31
+ * Support `Database#valid_connection?` in Postgres adapter (@janko)
32
+
33
+ * Fully utilize Sequel's logic for detecting disconnects in Postgres adapter (@janko)
34
+
35
+ * Support `Database#{copy_table,copy_into,listen}` in Postgres adapter (@janko)
36
+
37
+ * Log all queries executed by Sequel (@janko)
38
+
39
+ * Log executed queries to Sequel logger(s) as well (@janko)
40
+
41
+ * Specially label queries executed by Sequel in Active Record logs (@janko)
42
+
43
+ ## 0.4.1 (2020-09-28)
44
+
45
+ * Require Sequel version 5.16.0 or above (@janko)
46
+
47
+ ## 0.4.0 (2020-09-28)
48
+
49
+ * Return correct result of `Database#in_transaction?` after ActiveRecord transaction exited (@janko)
50
+
51
+ * Make ActiveRecord create a savepoint inside a Sequel transaction with `auto_savepoint: true` (@janko)
52
+
53
+ * Make Sequel create a savepoint inside ActiveRecord transaction with `joinable: false` (@janko)
54
+
55
+ * Improve reliability of nested transactions when combining Sequel and ActiveRecord (@janko)
56
+
57
+ * Raise error when attempting to add an `after_commit`/`after_rollback` hook on ActiveRecord transaction (@janko)
58
+
59
+ * Fix infinite loop that could happen with transactional Rails tests (@janko)
60
+
1
61
  ## 0.3.0 (2020-07-24)
2
62
 
3
63
  * Fully support Sequel transaction API (all transaction options, transaction/savepoint hooks etc.) (@janko)
data/README.md CHANGED
@@ -1,25 +1,23 @@
1
- # Sequel::ActiveRecordConnection
1
+ # sequel-activerecord_connection
2
2
 
3
3
  This is an extension for [Sequel] that allows it to reuse an existing
4
- ActiveRecord connection for database interaction. It works on ActiveRecord 4.2
5
- or higher, and supports the built-in `postgresql`, `mysql2` and `sqlite3`
6
- adapters, as well as JDBC adapter for JRuby.
4
+ ActiveRecord connection for database interaction.
7
5
 
8
6
  This can be useful if you're using a library that uses Sequel for database
9
- interaction (e.g. [Rodauth]), but you want to avoid creating a separate
10
- database connection. Or if you're transitioning from ActiveRecord to Sequel,
11
- and want the database connection to be shared.
7
+ interaction (e.g. [Rodauth] or [rom-sql]), but you want to avoid creating a
8
+ separate database connection. Or if you're transitioning from ActiveRecord to
9
+ Sequel, and want the database connection to be shared.
12
10
 
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 some fairly advanced Sequel usage.
11
+ It works on ActiveRecord 4.2+ and fully supports PostgresSQL, MySQL and SQLite
12
+ adapters, both the native ones and JDBC (JRuby). Other adapters might work too,
13
+ but their integration hasn't been tested.
16
14
 
17
15
  ## Installation
18
16
 
19
17
  Add this line to your application's Gemfile:
20
18
 
21
- ```ruby
22
- gem "sequel-activerecord_connection", "~> 0.3"
19
+ ```rb
20
+ gem "sequel-activerecord_connection", "~> 1.0"
23
21
  ```
24
22
 
25
23
  And then execute:
@@ -42,8 +40,7 @@ appropriate Sequel adapter and load the `activerecord_connection` extension:
42
40
  ```rb
43
41
  require "sequel"
44
42
 
45
- DB = Sequel.postgres(test: false) # avoid creating a connection
46
- DB.extension :activerecord_connection
43
+ DB = Sequel.postgres(extensions: :activerecord_connection)
47
44
  ```
48
45
 
49
46
  Now any Sequel operations that you make will internaly be done using the
@@ -75,13 +72,17 @@ ActiveRecord adapters, just make sure to initialize the corresponding Sequel
75
72
  adapter before loading the extension.
76
73
 
77
74
  ```rb
78
- DB = Sequel.postgres(test: false) # for "postgresql" adapter
79
- # or
80
- DB = Sequel.mysql2(test: false) # for "mysql2" adapter
81
- # or
82
- DB = Sequel.sqlite(test: false) # for "sqlite3" adapter
83
- # or
84
- DB = Sequel.jdbc(test: false) # for JDBC adapter
75
+ Sequel.postgres(extensions: :activerecord_connection) # for "postgresql" adapter
76
+ Sequel.mysql2(extensions: :activerecord_connection) # for "mysql2" adapter
77
+ Sequel.sqlite(extensions: :activerecord_connection) # for "sqlite3" adapter
78
+ ```
79
+
80
+ If you're on JRuby, you should be using the JDBC adapters:
81
+
82
+ ```rb
83
+ Sequel.connect("jdbc:postgresql://", extensions: :activerecord_connection) # for "jdbcpostgresql" adapter
84
+ Sequel.connect("jdbc:mysql://", extensions: :activerecord_connection) # for "jdbcmysql" adapter
85
+ Sequel.connect("jdbc:sqlite://", extensions: :activerecord_connection) # for "jdbcsqlite3" adapter
85
86
  ```
86
87
 
87
88
  ### Transactions
@@ -101,41 +102,77 @@ Sequel's transaction API is fully supported:
101
102
 
102
103
  ```rb
103
104
  DB.transaction(isolation: :serializable) do
104
- DB.after_commit { ... } # call block after transaction commits
105
+ DB.after_commit { ... } # executed after transaction commits
105
106
  DB.transaction(savepoint: true) do # creates a savepoint
106
- # ...
107
+ DB.after_commit(savepoint: true) { ... } # executed if all enclosing savepoints have been released
107
108
  end
108
109
  end
109
110
  ```
110
111
 
111
- One caveat to keep in mind is that Sequel's transaction hooks
112
- (`after_commit`, `after_rollback`) will *not* run if ActiveRecord holds the
113
- outer transaction:
112
+ When combining Active Record and Sequel transactions, Sequel transaction hook
113
+ functionality will be utilized when possible.
114
114
 
115
115
  ```rb
116
+ # Sequel: An after_commit transaction hook will always get executed if the outer
117
+ # transaction commits, even if it's added inside a savepoint that's rolled back.
116
118
  DB.transaction do
117
- DB.after_commit { ... } # will get executed
118
- end
119
-
120
- ActiveRecord::Base.transaction do
121
- DB.after_commit { ... } # won't get executed
119
+ ActiveRecord::Base.transaction(requires_new: true) do
120
+ DB.after_commit { puts "after commit" }
121
+ raise ActiveRecord::Rollback
122
+ end
122
123
  end
123
-
124
- ActiveRecord::Base.transaction do
124
+ #>> BEGIN
125
+ #>> SAVEPOINT active_record_1
126
+ #>> ROLLBACK TO SAVEPOINT active_record_1
127
+ #>> COMMIT
128
+ #>> after commit
129
+
130
+ # Sequel: An after_commit savepoint hook will get executed only after the outer
131
+ # transaction commits, given that all enclosing savepoints have been released.
132
+ DB.transaction(auto_savepoint: true) do
125
133
  DB.transaction do
126
- DB.after_commit { ... } # won't get executed
134
+ DB.after_commit(savepoint: true) { puts "after commit" }
135
+ raise Sequel::Rollback
127
136
  end
128
137
  end
138
+ #>> BEGIN
139
+ #>> SAVEPOINT active_record_1
140
+ #>> RELEASE SAVEPOINT active_record_1
141
+ #>> COMMIT
142
+ #>> after commit
129
143
  ```
130
144
 
131
- Savepoint hooks should still work, though:
145
+ In case of (a) adding a transaction hook while Active Record holds the
146
+ transaction, or (b) adding a savepoint hook when Active Record holds any
147
+ enclosing savepoint, Active Record transaction callbacks will be used instead
148
+ of Sequel hooks, which have slightly different behaviour in some circumstances.
132
149
 
133
150
  ```rb
151
+ # ActiveRecord: An after_commit transaction callback is not executed if any
152
+ # if the enclosing savepoints have been rolled back
134
153
  ActiveRecord::Base.transaction do
135
154
  DB.transaction(savepoint: true) do
136
- DB.after_commit { ... } # will get executed after savepoint is released
155
+ DB.after_commit { puts "after commit" }
156
+ raise Sequel::Rollback
137
157
  end
138
158
  end
159
+ #>> BEGIN
160
+ #>> SAVEPOINT active_record_1
161
+ #>> ROLLBACK TO SAVEPOINT active_record_1
162
+ #>> COMMIT
163
+
164
+ # ActiveRecord: An after_commit transaction callback can be executed already
165
+ # after a savepoint is released, if the enclosing transaction is not joinable.
166
+ ActiveRecord::Base.transaction(joinable: false) do
167
+ DB.transaction do
168
+ DB.after_commit { puts "after commit" }
169
+ end
170
+ end
171
+ #>> BEGIN
172
+ #>> SAVEPOINT active_record_1
173
+ #>> RELEASE SAVEPOINT active_record_1
174
+ #>> after commit
175
+ #>> COMMIT
139
176
  ```
140
177
 
141
178
  ### Model
@@ -153,19 +190,6 @@ end
153
190
  DB.activerecord_model = MyModel
154
191
  ```
155
192
 
156
- ### Timezone
157
-
158
- Sequel's database timezone will be automatically set to ActiveRecord's default
159
- timezone (`:utc` by default) when the extension is loaded.
160
-
161
- If you happen to be changing ActiveRecord's default timezone after you've
162
- loaded the extension, make sure to reflect that in your Sequel database object,
163
- for example:
164
-
165
- ```rb
166
- DB.timezone = :local
167
- ```
168
-
169
193
  ## Tests
170
194
 
171
195
  You'll first want to run the rake tasks for setting up databases and users:
@@ -198,3 +222,4 @@ Everyone interacting in this project's codebases, issue trackers, chat rooms and
198
222
 
199
223
  [Sequel]: https://github.com/jeremyevans/sequel
200
224
  [Rodauth]: https://github.com/jeremyevans/rodauth
225
+ [rom-sql]: https://github.com/rom-rb/rom-sql
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "after_commit_everywhere"
4
+
1
5
  module Sequel
2
6
  module ActiveRecordConnection
3
7
  Error = Class.new(Sequel::Error)
@@ -9,15 +13,17 @@ module Sequel
9
13
  serializable: :serializable,
10
14
  }
11
15
 
16
+ ACTIVERECORD_CALLBACKS = Object.new.extend(AfterCommitEverywhere)
17
+
12
18
  def self.extended(db)
13
19
  db.activerecord_model = ActiveRecord::Base
14
- db.timezone = ActiveRecord::Base.default_timezone
20
+ db.opts[:test] = false unless db.opts.key?(:test)
15
21
 
16
22
  begin
17
23
  require "sequel/extensions/activerecord_connection/#{db.adapter_scheme}"
18
24
  db.extend Sequel::ActiveRecordConnection.const_get(db.adapter_scheme.capitalize)
19
25
  rescue LoadError
20
- fail Error, "unsupported adapter: #{db.adapter_scheme}"
26
+ # assume the Sequel adapter already works with Active Record
21
27
  end
22
28
  end
23
29
 
@@ -28,79 +34,141 @@ module Sequel
28
34
  raise Error, "creating a Sequel connection is not allowed"
29
35
  end
30
36
 
31
- # Avoid calling Sequel's connection pool, instead use ActiveRecord.
37
+ # Avoid calling Sequel's connection pool, instead use Active Record's.
32
38
  def synchronize(*)
33
- if ActiveRecord.version >= Gem::Version.new("5.1.0")
34
- activerecord_connection.lock.synchronize do
35
- yield activerecord_raw_connection
36
- end
37
- else
38
- yield activerecord_raw_connection
39
+ activerecord_lock do
40
+ yield activerecord_connection.raw_connection
39
41
  end
40
42
  end
41
43
 
44
+ # Log executed queries into Active Record logger as well.
45
+ def log_connection_yield(sql, conn, args = nil)
46
+ sql += "; #{args.inspect}" if args
47
+
48
+ activerecord_log(sql) { super }
49
+ end
50
+
51
+ # Match database timezone with Active Record.
52
+ def timezone
53
+ @timezone || ActiveRecord::Base.default_timezone
54
+ end
55
+
42
56
  private
43
57
 
44
- # Backfills any ActiveRecord transactions/savepoints that have been opened
45
- # directly via ActiveRecord::Base.transaction. Sequel uses this information
46
- # to know whether we're in a transaction, whether to create a savepoint,
47
- # when to run transaction/savepoint hooks etc.
58
+ # Synchronizes transaction state with ActiveRecord. Sequel uses this
59
+ # information to know whether we're in a transaction, whether to create a
60
+ # savepoint, when to run transaction/savepoint hooks etc.
48
61
  def _trans(conn)
49
- Sequel.synchronize do
50
- result = @transactions[conn]
51
-
52
- if activerecord_connection.transaction_open?
53
- result ||= { savepoints: [] }
54
- while result[:savepoints].length < activerecord_connection.open_transactions
55
- result[:savepoints].unshift({ activerecord: true })
56
- end
57
- end
62
+ hash = super || { savepoints: [], activerecord: true }
58
63
 
59
- @transactions[conn] = result if result
60
- result
64
+ # add any ActiveRecord transactions/savepoints that have been opened
65
+ # directly via ActiveRecord::Base.transaction
66
+ while hash[:savepoints].length < activerecord_connection.open_transactions
67
+ hash[:savepoints] << { activerecord: true }
68
+ end
69
+
70
+ # remove any ActiveRecord transactions/savepoints that have been closed
71
+ # directly via ActiveRecord::Base.transaction
72
+ while hash[:savepoints].length > activerecord_connection.open_transactions && hash[:savepoints].last[:activerecord]
73
+ hash[:savepoints].pop
74
+ end
75
+
76
+ # sync knowledge about joinability of current ActiveRecord transaction/savepoint
77
+ if activerecord_connection.transaction_open? && !activerecord_connection.current_transaction.joinable?
78
+ hash[:savepoints].last[:auto_savepoint] = true
79
+ end
80
+
81
+ if hash[:savepoints].empty? && hash[:activerecord]
82
+ Sequel.synchronize { @transactions.delete(conn) }
83
+ else
84
+ Sequel.synchronize { @transactions[conn] = hash }
61
85
  end
62
- end
63
86
 
64
- # First delete any transactions/savepoints opened directly via
65
- # ActiveRecord::Base.transaction, so that Sequel can detect when the last
66
- # Sequel transaction has been closed and clear transaction information.
67
- def transaction_finished?(conn)
68
- _trans(conn)[:savepoints].shift while _trans(conn)[:savepoints].first[:activerecord]
69
87
  super
70
88
  end
71
89
 
72
- def begin_transaction(conn, opts = {})
90
+ def begin_transaction(conn, opts = OPTS)
73
91
  isolation = TRANSACTION_ISOLATION_MAP.fetch(opts[:isolation]) if opts[:isolation]
92
+ joinable = !opts[:auto_savepoint]
74
93
 
75
- activerecord_connection.begin_transaction(isolation: isolation)
94
+ activerecord_connection.begin_transaction(isolation: isolation, joinable: joinable)
76
95
  end
77
96
 
78
- def commit_transaction(conn, opts = {})
97
+ def commit_transaction(conn, opts = OPTS)
79
98
  activerecord_connection.commit_transaction
80
99
  end
81
100
 
82
- def rollback_transaction(conn, opts = {})
101
+ def rollback_transaction(conn, opts = OPTS)
83
102
  activerecord_connection.rollback_transaction
84
- activerecord_connection.transaction_manager.send(:after_failure_actions, activerecord_connection.current_transaction, $!) if activerecord_connection.transaction_manager.respond_to?(:after_failure_actions)
85
103
  end
86
104
 
87
- def savepoint_level(conn)
88
- activerecord_connection.open_transactions
105
+ # When Active Record holds the transaction, we cannot use Sequel hooks,
106
+ # because Sequel doesn't have knowledge of when the transaction is
107
+ # committed. So in this case we register an Active Record hook using the
108
+ # after_commit_everywhere gem.
109
+ def add_transaction_hook(conn, type, block)
110
+ if _trans(conn)[:activerecord]
111
+ ACTIVERECORD_CALLBACKS.public_send(type, &block)
112
+ else
113
+ super
114
+ end
89
115
  end
90
116
 
91
- def activerecord_raw_connection
92
- activerecord_connection.raw_connection
117
+ # When Active Record holds the savepoint, we cannot use Sequel hooks,
118
+ # because Sequel doesn't have knowledge of when the savepoint is
119
+ # released. So in this case we register an Active Record hook using the
120
+ # after_commit_everywhere gem.
121
+ def add_savepoint_hook(conn, type, block)
122
+ if _trans(conn)[:savepoints].last[:activerecord]
123
+ ACTIVERECORD_CALLBACKS.public_send(type, &block)
124
+ else
125
+ super
126
+ end
127
+ end
128
+
129
+ # Active Record doesn't guarantee that a single connection can only be used
130
+ # by one thread at a time, so we need to use locking, which is what Active
131
+ # Record does internally as well.
132
+ def activerecord_lock
133
+ return yield if ActiveRecord.version < Gem::Version.new("5.1.0")
134
+
135
+ activerecord_connection.lock.synchronize do
136
+ ActiveSupport::Dependencies.interlock.permit_concurrent_loads do
137
+ yield
138
+ end
139
+ end
93
140
  end
94
141
 
95
142
  def activerecord_connection
96
143
  activerecord_model.connection
97
144
  end
98
145
 
99
- def activesupport_interlock(&block)
100
- if ActiveSupport::Dependencies.respond_to?(:interlock)
101
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads(&block)
102
- else
146
+ def activerecord_log(sql, &block)
147
+ ActiveSupport::Notifications.instrument(
148
+ "sql.active_record",
149
+ sql: sql,
150
+ name: "Sequel",
151
+ connection: activerecord_connection,
152
+ &block
153
+ )
154
+ end
155
+
156
+ module Utils
157
+ def self.set_value(object, name, new_value)
158
+ original_value = object.send(name)
159
+ object.send(:"#{name}=", new_value)
103
160
  yield
161
+ ensure
162
+ object.send(:"#{name}=", original_value)
163
+ end
164
+
165
+ def self.add_prepared_statements_cache(conn)
166
+ return if conn.respond_to?(:prepared_statements)
167
+
168
+ class << conn
169
+ attr_accessor :prepared_statements
170
+ end
171
+ conn.prepared_statements = {}
104
172
  end
105
173
  end
106
174
  end
@@ -7,32 +7,11 @@ module Sequel
7
7
  end
8
8
  end
9
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
10
+ def synchronize(*)
11
+ super do |conn|
12
+ yield conn.connection
32
13
  end
33
- rescue ActiveRecord::StatementInvalid => exception
34
- raise_error(exception.cause, classes: database_error_classes)
35
14
  end
36
15
  end
37
16
  end
38
- end
17
+ end
@@ -1,34 +1,20 @@
1
1
  module Sequel
2
2
  module ActiveRecordConnection
3
3
  module Mysql2
4
- def execute(sql, opts=OPTS)
5
- original_query_options = activerecord_raw_connection.query_options.dup
4
+ def synchronize(*)
5
+ super do |conn|
6
+ # required for prepared statements
7
+ conn.instance_variable_set(:@sequel_default_query_options, conn.query_options.dup)
8
+ Utils.add_prepared_statements_cache(conn)
6
9
 
7
- activerecord_raw_connection.query_options.merge!(
8
- as: :hash,
9
- symbolize_keys: true,
10
- cache_rows: false,
11
- )
10
+ conn.query_options.merge!(as: :hash, symbolize_keys: true, cache_rows: false)
12
11
 
13
- result = activerecord_connection.execute(sql)
14
-
15
- if opts[:type] == :select
16
- if block_given?
17
- yield result
18
- else
19
- result
12
+ begin
13
+ yield conn
14
+ ensure
15
+ conn.query_options.replace(conn.instance_variable_get(:@sequel_default_query_options))
20
16
  end
21
- elsif block_given?
22
- yield activerecord_raw_connection
23
- end
24
- rescue ActiveRecord::StatementInvalid => exception
25
- if exception.cause.is_a?(::Mysql2::Error)
26
- raise_error(exception.cause)
27
- else
28
- raise
29
17
  end
30
- ensure
31
- activerecord_raw_connection.query_options.replace(original_query_options)
32
18
  end
33
19
  end
34
20
  end
@@ -1,28 +1,98 @@
1
1
  module Sequel
2
2
  module ActiveRecordConnection
3
3
  module Postgres
4
- def execute(sql, opts=OPTS)
5
- result = activerecord_connection.execute(sql)
4
+ def synchronize(*)
5
+ super do |conn|
6
+ conn.extend(ConnectionMethods)
7
+ conn.instance_variable_set(:@db, self)
6
8
 
7
- if block_given?
8
- yield result
9
- else
10
- result.cmd_tuples
9
+ Utils.add_prepared_statements_cache(conn)
10
+
11
+ Utils.set_value(conn, :type_map_for_results, PG::TypeMapAllStrings.new) do
12
+ yield conn
13
+ end
11
14
  end
12
- rescue ActiveRecord::PreparedStatementCacheExpired
13
- raise # ActiveRecord's transaction manager needs to handle this exception
14
- rescue ActiveRecord::StatementInvalid => exception
15
- raise_error(exception.cause, classes: database_error_classes)
16
- ensure
17
- result.clear if result
18
15
  end
19
16
 
20
- def transaction(options = {})
17
+ # Reject unsupported Postgres-specific transaction options.
18
+ def transaction(opts = OPTS)
21
19
  %i[deferrable read_only synchronous].each do |key|
22
- fail Error, "#{key.inspect} transaction option is currently not supported" if options.key?(key)
20
+ fail Error, "#{key.inspect} transaction option is currently not supported" if opts.key?(key)
23
21
  end
24
22
 
25
23
  super
24
+ rescue => e
25
+ activerecord_connection.clear_cache! if e.class.name == "ActiveRecord::PreparedStatementCacheExpired" && !in_transaction?
26
+ raise
27
+ end
28
+
29
+ # Copy-pasted from Sequel::Postgres::Adapter.
30
+ module ConnectionMethods
31
+ # The underlying exception classes to reraise as disconnect errors
32
+ # instead of regular database errors.
33
+ DISCONNECT_ERROR_CLASSES = [IOError, Errno::EPIPE, Errno::ECONNRESET, ::PG::ConnectionBad].freeze
34
+
35
+ # Since exception class based disconnect checking may not work,
36
+ # also trying parsing the exception message to look for disconnect
37
+ # errors.
38
+ DISCONNECT_ERROR_REGEX = /\A#{Regexp.union([
39
+ "ERROR: cached plan must not change result type",
40
+ "could not receive data from server",
41
+ "no connection to the server",
42
+ "connection not open",
43
+ "connection is closed",
44
+ "terminating connection due to administrator command",
45
+ "PQconsumeInput() "
46
+ ])}/
47
+
48
+ def async_exec_params(sql, args)
49
+ defined?(super) ? super : async_exec(sql, args)
50
+ end
51
+
52
+ # Raise a Sequel::DatabaseDisconnectError if a one of the disconnect
53
+ # error classes is raised, or a PG::Error is raised and the connection
54
+ # status cannot be determined or it is not OK.
55
+ def check_disconnect_errors
56
+ begin
57
+ yield
58
+ rescue *DISCONNECT_ERROR_CLASSES => e
59
+ disconnect = true
60
+ raise(Sequel.convert_exception_class(e, Sequel::DatabaseDisconnectError))
61
+ rescue PG::Error => e
62
+ disconnect = false
63
+ begin
64
+ s = status
65
+ rescue PG::Error
66
+ disconnect = true
67
+ end
68
+ status_ok = (s == PG::CONNECTION_OK)
69
+ disconnect ||= !status_ok
70
+ disconnect ||= e.message =~ DISCONNECT_ERROR_REGEX
71
+ disconnect ? raise(Sequel.convert_exception_class(e, Sequel::DatabaseDisconnectError)) : raise
72
+ ensure
73
+ block if status_ok && !disconnect
74
+ end
75
+ end
76
+
77
+ # Execute the given SQL with this connection. If a block is given,
78
+ # yield the results, otherwise, return the number of changed rows.
79
+ def execute(sql, args = nil)
80
+ args = args.map { |v| @db.bound_variable_arg(v, self) } if args
81
+ result = check_disconnect_errors { execute_query(sql, args) }
82
+
83
+ block_given? ? yield(result) : result.cmd_tuples
84
+ ensure
85
+ result.clear if result
86
+ end
87
+
88
+ private
89
+
90
+ # Return the PG::Result containing the query results.
91
+ def execute_query(sql, args)
92
+ @db.log_connection_yield(sql, self, args) do
93
+ args ? async_exec_params(sql, args) : async_exec(sql)
94
+ end
95
+ end
26
96
  end
27
97
  end
28
98
  end
@@ -7,43 +7,16 @@ module Sequel
7
7
  end
8
8
  end
9
9
 
10
- def execute_ddl(sql, opts=OPTS)
11
- execute(sql, opts)
12
- end
13
-
14
- private
15
-
16
- # ActiveRecord doesn't send SQLite methods Sequel expects, so we need to
17
- # try to replicate what ActiveRecord does around connection excecution.
18
- def _execute(type, sql, opts, &block)
19
- if activerecord_raw_connection.respond_to?(:extended_result_codes=)
20
- activerecord_raw_connection.extended_result_codes = true
21
- end
10
+ def synchronize(*)
11
+ super do |conn|
12
+ conn.extended_result_codes = true if conn.respond_to?(:extended_result_codes=)
22
13
 
23
- if ActiveRecord::VERSION::MAJOR >= 6
24
- activerecord_connection.materialize_transactions
25
- end
14
+ Utils.add_prepared_statements_cache(conn)
26
15
 
27
- activerecord_connection.send(:log, sql) do
28
- activesupport_interlock do
29
- case type
30
- when :select
31
- activerecord_raw_connection.query(sql, &block)
32
- when :insert
33
- activerecord_raw_connection.execute(sql)
34
- activerecord_raw_connection.last_insert_row_id
35
- when :update
36
- activerecord_raw_connection.execute_batch(sql)
37
- activerecord_raw_connection.changes
38
- end
16
+ Utils.set_value(conn, :results_as_hash, nil) do
17
+ yield conn
39
18
  end
40
19
  end
41
- rescue ActiveRecord::StatementInvalid => exception
42
- if exception.cause.is_a?(SQLite3::Exception)
43
- raise_error(exception.cause)
44
- else
45
- raise exception
46
- end
47
20
  end
48
21
  end
49
22
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "sequel-activerecord_connection"
3
- spec.version = "0.3.0"
3
+ spec.version = "1.1.0"
4
4
  spec.authors = ["Janko Marohnić"]
5
5
  spec.email = ["janko.marohnic@gmail.com"]
6
6
 
@@ -9,12 +9,15 @@ Gem::Specification.new do |spec|
9
9
  spec.homepage = "https://github.com/janko/sequel-activerecord_connection"
10
10
  spec.license = "MIT"
11
11
 
12
- spec.required_ruby_version = Gem::Requirement.new(">= 2.2.0")
12
+ spec.required_ruby_version = ">= 2.3"
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
+ spec.add_dependency "after_commit_everywhere", "~> 0.1.5"
16
17
 
18
+ spec.add_development_dependency "sequel", "~> 5.38"
17
19
  spec.add_development_dependency "minitest"
20
+ spec.add_development_dependency "warning" if RUBY_VERSION >= "2.4"
18
21
 
19
22
  spec.files = Dir["README.md", "LICENSE.txt", "CHANGELOG.md", "lib/**/*.rb", "*.gemspec"]
20
23
  spec.require_paths = ["lib"]
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.3.0
4
+ version: 1.1.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-07-24 00:00:00.000000000 Z
11
+ date: 2020-11-08 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
@@ -50,6 +44,34 @@ dependencies:
50
44
  - - "<"
51
45
  - !ruby/object:Gem::Version
52
46
  version: '7'
47
+ - !ruby/object:Gem::Dependency
48
+ name: after_commit_everywhere
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.1.5
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 0.1.5
61
+ - !ruby/object:Gem::Dependency
62
+ name: sequel
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '5.38'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '5.38'
53
75
  - !ruby/object:Gem::Dependency
54
76
  name: minitest
55
77
  requirement: !ruby/object:Gem::Requirement
@@ -64,6 +86,20 @@ dependencies:
64
86
  - - ">="
65
87
  - !ruby/object:Gem::Version
66
88
  version: '0'
89
+ - !ruby/object:Gem::Dependency
90
+ name: warning
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
67
103
  description: Allows Sequel to use ActiveRecord connection for database interaction.
68
104
  email:
69
105
  - janko.marohnic@gmail.com
@@ -92,14 +128,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
92
128
  requirements:
93
129
  - - ">="
94
130
  - !ruby/object:Gem::Version
95
- version: 2.2.0
131
+ version: '2.3'
96
132
  required_rubygems_version: !ruby/object:Gem::Requirement
97
133
  requirements:
98
134
  - - ">="
99
135
  - !ruby/object:Gem::Version
100
136
  version: '0'
101
137
  requirements: []
102
- rubygems_version: 3.1.1
138
+ rubygems_version: 3.1.4
103
139
  signing_key:
104
140
  specification_version: 4
105
141
  summary: Allows Sequel to use ActiveRecord connection for database interaction.