sequel-activerecord_connection 0.4.1 → 1.0.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: bb35cdbc0ade81ae15042834733f9ec4683b6889eab2c35412710d631a02de53
4
- data.tar.gz: 75f97d5bee8522c12a7a3c65e05e1511f40ca4eccab8bc4b8cb2e1c3ece1a44a
3
+ metadata.gz: 929f5047395464d1d3616aeab41742b6766ae14b96ca1f5b7ac47bb1991f3391
4
+ data.tar.gz: c931c23d71b73ce2bb35118e1d02ecc2df529f2c297b995c9c5ee46c5b0bfe85
5
5
  SHA512:
6
- metadata.gz: 4fbf8edd6c9a61a8761431d33ba89c63a2aa444417d5ebec7b42ec9d3ef8aa5c509b28d35d708af223289c8e085b62204c3af690dc18c13e0df69f61a837b41d
7
- data.tar.gz: 5f4408be86e1d09a856215ca6e1fd4ef594955549e52987107ff33fc8efb24c68f4fdaf03c0132bb22fe4e2023b31d5526dc77f225d476718528d101c2f6bb8c
6
+ metadata.gz: 062f919b4268a57e392bad5dd7d4a988adc90a8df2d4076f5abfa9afe7c125534f9e7ad1ca16733d0539e622f2b988f95d4653d6b076cd356809d95daf1c24d2
7
+ data.tar.gz: 1f1efabd6b3ae74c6d35b3d0a23ad87a9d1c939e209afc2f24267840c6c72b2ff60fb1ca86010335b65a251c3c57fa81b300aa4cf38b9ad49df53b00f931d89f
@@ -1,3 +1,33 @@
1
+ ## 1.0.0 (2020-10-25)
2
+
3
+ * Clear AR statement cache on `ActiveRecord::PreparedStatementCacheExpired` when Sequel holds the transaction (@janko)
4
+
5
+ * Pick up `ActiveRecord::Base.default_timezone` being changed on runtime (@janko)
6
+
7
+ * Support prepared statements and bound variables in all adapters (@janko)
8
+
9
+ * Correctly identify identity columns as primary keys in Postgres adapter (@janko)
10
+
11
+ * Avoid using deprecated `sqlite3` API in SQLite adapter (@janko)
12
+
13
+ * Allow using any external Active Record adapters (@janko)
14
+
15
+ * Avoid potential bugs when converting Active Record exceptions into Sequel exceptions (@janko)
16
+
17
+ * Don't use Active Record locks when executing queries with Sequel (@janko)
18
+
19
+ * Support `Database#valid_connection?` in Postgres adapter (@janko)
20
+
21
+ * Fully utilize Sequel's logic for detecting disconnects in Postgres adapter (@janko)
22
+
23
+ * Support `Database#{copy_table,copy_into,listen}` in Postgres adapter (@janko)
24
+
25
+ * Log all queries executed by Sequel (@janko)
26
+
27
+ * Log executed queries to Sequel logger(s) as well (@janko)
28
+
29
+ * Specially label queries executed by Sequel in Active Record logs (@janko)
30
+
1
31
  ## 0.4.1 (2020-09-28)
2
32
 
3
33
  * Require Sequel version 5.16.0 or above (@janko)
data/README.md CHANGED
@@ -74,13 +74,17 @@ ActiveRecord adapters, just make sure to initialize the corresponding Sequel
74
74
  adapter before loading the extension.
75
75
 
76
76
  ```rb
77
- DB = Sequel.postgres(extensions: :activerecord_connection) # for "postgresql" adapter
78
- # or
79
- DB = Sequel.mysql2(extensions: :activerecord_connection) # for "mysql2" adapter
80
- # or
81
- DB = Sequel.sqlite(extensions: :activerecord_connection) # for "sqlite3" adapter
82
- # or
83
- DB = Sequel.jdbc(extensions: :activerecord_connection) # for JDBC adapter
77
+ Sequel.postgres(extensions: :activerecord_connection) # for "postgresql" adapter
78
+ Sequel.mysql2(extensions: :activerecord_connection) # for "mysql2" adapter
79
+ Sequel.sqlite(extensions: :activerecord_connection) # for "sqlite3" adapter
80
+ ```
81
+
82
+ If you're on JRuby, you should be using the JDBC adapters:
83
+
84
+ ```rb
85
+ Sequel.connect("jdbc:postgresql://", extensions: :activerecord_connection) # for "jdbcpostgresql" adapter
86
+ Sequel.connect("jdbc:mysql://", extensions: :activerecord_connection) # for "jdbcmysql" adapter
87
+ Sequel.connect("jdbc:sqlite://", extensions: :activerecord_connection) # for "jdbcsqlite3" adapter
84
88
  ```
85
89
 
86
90
  ### Transactions
@@ -149,19 +153,6 @@ end
149
153
  DB.activerecord_model = MyModel
150
154
  ```
151
155
 
152
- ### Timezone
153
-
154
- Sequel's database timezone will be automatically set to ActiveRecord's default
155
- timezone (`:utc` by default) when the extension is loaded.
156
-
157
- If you happen to be changing ActiveRecord's default timezone after you've
158
- loaded the extension, make sure to reflect that in your Sequel database object,
159
- for example:
160
-
161
- ```rb
162
- DB.timezone = :local
163
- ```
164
-
165
156
  ## Tests
166
157
 
167
158
  You'll first want to run the rake tasks for setting up databases and users:
@@ -13,13 +13,12 @@ module Sequel
13
13
 
14
14
  def self.extended(db)
15
15
  db.activerecord_model = ActiveRecord::Base
16
- db.timezone = ActiveRecord::Base.default_timezone
17
16
 
18
17
  begin
19
18
  require "sequel/extensions/activerecord_connection/#{db.adapter_scheme}"
20
19
  db.extend Sequel::ActiveRecordConnection.const_get(db.adapter_scheme.capitalize)
21
20
  rescue LoadError
22
- fail Error, "unsupported adapter: #{db.adapter_scheme}"
21
+ # assume the Sequel adapter already works with Active Record
23
22
  end
24
23
  end
25
24
 
@@ -30,15 +29,21 @@ module Sequel
30
29
  raise Error, "creating a Sequel connection is not allowed"
31
30
  end
32
31
 
33
- # Avoid calling Sequel's connection pool, instead use ActiveRecord.
32
+ # Avoid calling Sequel's connection pool, instead use Active Record's.
34
33
  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
34
+ yield activerecord_connection.raw_connection
35
+ end
36
+
37
+ # Log executed queries into Active Record logger as well.
38
+ def log_connection_yield(sql, conn, args = nil)
39
+ sql += "; #{args.inspect}" if args
40
+
41
+ activerecord_log(sql) { super }
42
+ end
43
+
44
+ # Match database timezone with Active Record.
45
+ def timezone
46
+ @timezone || ActiveRecord::Base.default_timezone
42
47
  end
43
48
 
44
49
  private
@@ -75,20 +80,19 @@ module Sequel
75
80
  super
76
81
  end
77
82
 
78
- def begin_transaction(conn, opts = {})
83
+ def begin_transaction(conn, opts = OPTS)
79
84
  isolation = TRANSACTION_ISOLATION_MAP.fetch(opts[:isolation]) if opts[:isolation]
80
85
  joinable = !opts[:auto_savepoint]
81
86
 
82
87
  activerecord_connection.begin_transaction(isolation: isolation, joinable: joinable)
83
88
  end
84
89
 
85
- def commit_transaction(conn, opts = {})
90
+ def commit_transaction(conn, opts = OPTS)
86
91
  activerecord_connection.commit_transaction
87
92
  end
88
93
 
89
- def rollback_transaction(conn, opts = {})
94
+ def rollback_transaction(conn, opts = OPTS)
90
95
  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
96
  end
93
97
 
94
98
  def add_transaction_hook(conn, type, block)
@@ -107,19 +111,36 @@ module Sequel
107
111
  super
108
112
  end
109
113
 
110
- def activerecord_raw_connection
111
- activerecord_connection.raw_connection
112
- end
113
-
114
114
  def activerecord_connection
115
115
  activerecord_model.connection
116
116
  end
117
117
 
118
- def activesupport_interlock(&block)
119
- if ActiveSupport::Dependencies.respond_to?(:interlock)
120
- ActiveSupport::Dependencies.interlock.permit_concurrent_loads(&block)
121
- else
118
+ def activerecord_log(sql, &block)
119
+ ActiveSupport::Notifications.instrument(
120
+ "sql.active_record",
121
+ sql: sql,
122
+ name: "Sequel",
123
+ connection: activerecord_connection,
124
+ &block
125
+ )
126
+ end
127
+
128
+ module Utils
129
+ def self.set_value(object, name, new_value)
130
+ original_value = object.send(name)
131
+ object.send(:"#{name}=", new_value)
122
132
  yield
133
+ ensure
134
+ object.send(:"#{name}=", original_value)
135
+ end
136
+
137
+ def self.add_prepared_statements_cache(conn)
138
+ return if conn.respond_to?(:prepared_statements)
139
+
140
+ class << conn
141
+ attr_accessor :prepared_statements
142
+ end
143
+ conn.prepared_statements = {}
123
144
  end
124
145
  end
125
146
  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.4.1"
3
+ spec.version = "1.0.0"
4
4
  spec.authors = ["Janko Marohnić"]
5
5
  spec.email = ["janko.marohnic@gmail.com"]
6
6
 
@@ -15,6 +15,7 @@ Gem::Specification.new do |spec|
15
15
  spec.add_dependency "activerecord", ">= 4.2", "< 7"
16
16
 
17
17
  spec.add_development_dependency "minitest"
18
+ spec.add_development_dependency "warning" if RUBY_VERSION >= "2.4"
18
19
 
19
20
  spec.files = Dir["README.md", "LICENSE.txt", "CHANGELOG.md", "lib/**/*.rb", "*.gemspec"]
20
21
  spec.require_paths = ["lib"]
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.4.1
4
+ version: 1.0.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-09-28 00:00:00.000000000 Z
11
+ date: 2020-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel
@@ -58,6 +58,20 @@ dependencies:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
60
  version: '0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: warning
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
61
75
  description: Allows Sequel to use ActiveRecord connection for database interaction.
62
76
  email:
63
77
  - janko.marohnic@gmail.com
@@ -93,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
107
  - !ruby/object:Gem::Version
94
108
  version: '0'
95
109
  requirements: []
96
- rubygems_version: 3.1.1
110
+ rubygems_version: 3.1.4
97
111
  signing_key:
98
112
  specification_version: 4
99
113
  summary: Allows Sequel to use ActiveRecord connection for database interaction.