sequel-activerecord_connection 0.4.1 → 1.0.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: 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.