activerecord-trilogy-adapter 3.0.0 → 3.1.1

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: 778687330e4ff0e83f8cf02ea27808b9e7197c625312611c0dd004d17627a29e
4
- data.tar.gz: f3ddf11ab50accd31b5bd3a4b41899a781f4ac04555739d1e65eff3e884b8b38
3
+ metadata.gz: 0a3a2190bc8818bad95cf334d7e4b0c9108a9520f37a899ff356f36b2c067ef1
4
+ data.tar.gz: bf58ccfed24d5a7ecd17f6670d4a6fb623e27f862cbcbb87b408470b46fa44c0
5
5
  SHA512:
6
- metadata.gz: 7b2e8b370f301aeda40bef4e1a66c16dc0351b245a5c967341a22e1ab05830e4e11d5cd1de2de2e38a246cbd5fb492e27d8e906770d4484707512a34e9d8430c
7
- data.tar.gz: 3471e795e4942858134083a06f632da5afebb69c60a87001631486c79ecb81dc3f90e5c5cbdf5eb408c48ca125f47055ca8c08c0af945324e1043aa7996b104e
6
+ metadata.gz: 0b0991df8962bdfbe7d917aac03f224cbe919a3cef9113c39f1eab150d207c04b4a7db457404d492d3bb0e5b0e459d353f7511a1e82d7ff92c804d02353a8c34
7
+ data.tar.gz: 44a43a09ad1230250f29da21e941bc59a8f2d606f0716c7fc045ffba0b457e029fe01b34fade384b33277f06f2667cb8498c9b895fc7fce3461f7f07afacf332
data/README.md CHANGED
@@ -1,27 +1,34 @@
1
1
  # Trilogy Adapter
2
2
 
3
- Active Record database adapter for [Trilogy](https://github.com/trilogy-libraries/trilogy)
3
+ Ruby on Rails Active Record database adapter for [Trilogy](https://github.com/trilogy-libraries/trilogy), a client library for MySQL-compatible database servers, designed for performance, flexibility, and ease of embedding.
4
4
 
5
- This gem offers Trilogy support for versions of ActiveRecord prior to 7.1. Currently supports:
5
+ This gem offers Trilogy support for versions of Active Record prior to v7.1. Currently supports:
6
6
 
7
- - Rails v7.0.x
7
+ - ⚠️ Rails v7.1+ includes Trilogy support by default making this gem unnecessary
8
+ - ✅ Rails v7.0.x
9
+ - ✅ Rails v6.1.x
10
+ - ✅ Rails v6.0.x
8
11
 
9
12
  ## Requirements
10
13
 
11
- - [Ruby](https://www.ruby-lang.org) 2.7 or higher
12
- - [Active Record](https://github.com/rails/rails) 7.0.x
13
- - [Trilogy](https://github.com/trilogy-libraries/trilogy) 2.4.0 or higher
14
+ - [Ruby](https://www.ruby-lang.org) v2.7 or higher
15
+ - [Active Record](https://github.com/rails/rails) v6.0.x or higher
16
+ - [Trilogy](https://github.com/trilogy-libraries/trilogy) v2.4.0 or higher, which is included as a dependency of this gem.
14
17
 
15
18
  ## Setup
16
19
 
17
- * Add the following to your Gemfile:
20
+ 1. Add the following to your `Gemfile` and run `bundle install`:
18
21
 
19
- ```rb
20
- gem "activerecord-trilogy-adapter"
21
- ```
22
+ ```rb
23
+ # Gemfile
24
+ gem "activerecord-trilogy-adapter"
25
+ ```
26
+ 2. Update your application's database configuration to use `trilogy` as the adapter:
22
27
 
23
- * Update your database configuration (e.g. `config/database.yml`) to use
24
- `trilogy` as the adapter.
28
+ ```yaml
29
+ # config/database.yml
30
+ adapter: trilogy
31
+ ```
25
32
 
26
33
  ## Versioning
27
34
 
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module Trilogy
6
+ module DatabaseStatements
7
+ READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
8
+ :desc, :describe, :set, :show, :use
9
+ ) # :nodoc:
10
+ private_constant :READ_QUERY
11
+
12
+ HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP(6)").freeze # :nodoc:
13
+ private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
14
+
15
+ def execute(sql, name = nil, **kwargs)
16
+ sql = transform_query(sql)
17
+ check_if_write_query(sql)
18
+ mark_transaction_written_if_write(sql)
19
+
20
+ result = raw_execute(sql, name, **kwargs)
21
+ ActiveRecord::Result.new(result.fields, result.to_a)
22
+ end
23
+
24
+ def write_query?(sql) # :nodoc:
25
+ !READ_QUERY.match?(sql)
26
+ rescue ArgumentError # Invalid encoding
27
+ !READ_QUERY.match?(sql.b)
28
+ end
29
+
30
+ def explain(arel, binds = [])
31
+ sql = "EXPLAIN #{to_sql(arel, binds)}"
32
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
33
+ result = internal_exec_query(sql, "EXPLAIN", binds)
34
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
35
+
36
+ MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
37
+ end
38
+
39
+ def select_all(*, **) # :nodoc:
40
+ result = super
41
+ with_trilogy_connection do |conn|
42
+ conn.next_result while conn.more_results_exist?
43
+ end
44
+ result
45
+ end
46
+
47
+ def write_query?(sql) # :nodoc:
48
+ !READ_QUERY.match?(sql)
49
+ rescue ArgumentError # Invalid encoding
50
+ !READ_QUERY.match?(sql.b)
51
+ end
52
+
53
+ def explain(arel, binds = [], options = [])
54
+ sql = build_explain_clause(options) + " " + to_sql(arel, binds)
55
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
56
+ result = internal_exec_query(sql, "EXPLAIN", binds)
57
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
58
+
59
+ MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
60
+ end
61
+
62
+ def exec_query(sql, name = "SQL", binds = [], prepare: false, **kwargs)
63
+ internal_exec_query(sql, name, binds, prepare: prepare, **kwargs)
64
+ end
65
+
66
+ def internal_exec_query(sql, name = "SQL", binds = [], prepare: false, async: false) # :nodoc:
67
+ sql = transform_query(sql)
68
+ check_if_write_query(sql)
69
+ mark_transaction_written_if_write(sql)
70
+
71
+ result = raw_execute(sql, name, async: async)
72
+ ActiveRecord::Result.new(result.fields, result.to_a)
73
+ end
74
+
75
+ def exec_insert(sql, name, binds, pk = nil, sequence_name = nil, returning: nil) # :nodoc:
76
+ sql = transform_query(sql)
77
+ check_if_write_query(sql)
78
+ mark_transaction_written_if_write(sql)
79
+
80
+ raw_execute(to_sql(sql, binds), name)
81
+ end
82
+
83
+ def exec_delete(sql, name = nil, binds = []) # :nodoc:
84
+ sql = transform_query(sql)
85
+ check_if_write_query(sql)
86
+ mark_transaction_written_if_write(sql)
87
+
88
+ result = raw_execute(to_sql(sql, binds), name)
89
+ result.affected_rows
90
+ end
91
+
92
+ alias :exec_update :exec_delete # :nodoc:
93
+
94
+ def high_precision_current_timestamp
95
+ HIGH_PRECISION_CURRENT_TIMESTAMP
96
+ end
97
+
98
+ def build_explain_clause(options = [])
99
+ return "EXPLAIN" if options.empty?
100
+
101
+ explain_clause = "EXPLAIN #{options.join(" ").upcase}"
102
+
103
+ if analyze_without_explain? && explain_clause.include?("ANALYZE")
104
+ explain_clause.sub("EXPLAIN ", "")
105
+ else
106
+ explain_clause
107
+ end
108
+ end
109
+
110
+ private
111
+ if ActiveRecord.version < ::Gem::Version.new('7.0.a') # ActiveRecord <= 6.1 support
112
+ def transform_query(sql); sql; end
113
+ def check_if_write_query(*args); end
114
+
115
+ if ActiveRecord.version < ::Gem::Version.new('6.1.a') # ActiveRecord <= 6.0 support
116
+ def mark_transaction_written_if_write(sql)
117
+ transaction = current_transaction
118
+ if transaction.respond_to?(:written) && transaction.open?
119
+ transaction.written ||= write_query?(sql)
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ def raw_execute(sql, name, async: false, materialize_transactions: true)
126
+ log_kwargs = {}
127
+ log_kwargs[:async] = async if ActiveRecord.version >= ::Gem::Version.new('7.0.a')
128
+ log(sql, name, **log_kwargs) do
129
+ with_trilogy_connection(materialize_transactions: materialize_transactions) do |conn|
130
+ sync_timezone_changes(conn)
131
+ conn.query(sql)
132
+ end
133
+ end
134
+ end
135
+
136
+ def last_inserted_id(result)
137
+ result.last_insert_id
138
+ end
139
+
140
+ def sync_timezone_changes(conn)
141
+ # Sync any changes since connection last established.
142
+ if default_timezone == :local
143
+ conn.query_flags |= ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
144
+ else
145
+ conn.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
146
+ end
147
+ end
148
+
149
+ def execute_batch(statements, name = nil)
150
+ statements = statements.map { |sql| transform_query(sql) } if respond_to?(:transform_query)
151
+ combine_multi_statements(statements).each do |statement|
152
+ with_trilogy_connection do |conn|
153
+ raw_execute(statement, name)
154
+ conn.next_result while conn.more_results_exist?
155
+ end
156
+ end
157
+ end
158
+
159
+ def multi_statements_enabled?
160
+ !!@config[:multi_statement]
161
+ end
162
+
163
+ def with_multi_statements
164
+ if multi_statements_enabled?
165
+ return yield
166
+ end
167
+
168
+ with_trilogy_connection do |conn|
169
+ conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_ON)
170
+
171
+ yield
172
+ ensure
173
+ conn.set_server_option(::Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF)
174
+ end
175
+ end
176
+
177
+ def combine_multi_statements(total_sql)
178
+ total_sql.each_with_object([]) do |sql, total_sql_chunks|
179
+ previous_packet = total_sql_chunks.last
180
+ if max_allowed_packet_reached?(sql, previous_packet)
181
+ total_sql_chunks << +sql
182
+ else
183
+ previous_packet << ";\n"
184
+ previous_packet << sql
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
@@ -4,83 +4,83 @@ require "trilogy"
4
4
  require "active_record/connection_adapters/abstract_mysql_adapter"
5
5
 
6
6
  require "active_record/tasks/trilogy_database_tasks"
7
- require "trilogy_adapter/lost_connection_exception_translator"
7
+ require "active_record/connection_adapters/trilogy/database_statements"
8
8
 
9
9
  module ActiveRecord
10
- module ConnectionAdapters
11
- class TrilogyAdapter < ::ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
12
- module DatabaseStatements
13
- READ_QUERY = ActiveRecord::ConnectionAdapters::AbstractAdapter.build_read_query_regexp(
14
- :desc, :describe, :set, :show, :use
15
- ) # :nodoc:
16
- private_constant :READ_QUERY
17
-
18
- HIGH_PRECISION_CURRENT_TIMESTAMP = Arel.sql("CURRENT_TIMESTAMP(6)").freeze # :nodoc:
19
- private_constant :HIGH_PRECISION_CURRENT_TIMESTAMP
20
-
21
- def write_query?(sql) # :nodoc:
22
- !READ_QUERY.match?(sql)
23
- rescue ArgumentError # Invalid encoding
24
- !READ_QUERY.match?(sql.b)
25
- end
26
-
27
- def explain(arel, binds = [])
28
- sql = "EXPLAIN #{to_sql(arel, binds)}"
29
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
30
- result = exec_query(sql, "EXPLAIN", binds)
31
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
10
+ # ActiveRecord <= 6.1 support
11
+ if ActiveRecord.version < ::Gem::Version.new('7.0.a')
12
+ class DatabaseConnectionError < ConnectionNotEstablished
13
+ def initialize(message = nil)
14
+ super(message || "Database connection error")
15
+ end
32
16
 
33
- MySQL::ExplainPrettyPrinter.new.pp(result, elapsed)
17
+ class << self
18
+ def hostname_error(hostname)
19
+ DatabaseConnectionError.new(<<~MSG)
20
+ There is an issue connecting with your hostname: #{hostname}.\n
21
+ Please check your database configuration and ensure there is a valid connection to your database.
22
+ MSG
34
23
  end
35
24
 
36
- def exec_query(sql, name = "SQL", binds = [], prepare: false, async: false)
37
- result = execute(sql, name, async: async)
38
- ActiveRecord::Result.new(result.fields, result.to_a)
25
+ def username_error(username)
26
+ DatabaseConnectionError.new(<<~MSG)
27
+ There is an issue connecting to your database with your username/password, username: #{username}.\n
28
+ Please check your database configuration to ensure the username/password are valid.
29
+ MSG
39
30
  end
31
+ end
32
+ end
40
33
 
41
- alias exec_without_stmt exec_query
34
+ NoDatabaseError.class_exec do
35
+ def self.db_error(db_name)
36
+ NoDatabaseError.new(<<~MSG)
37
+ We could not find your database: #{db_name}. Available database configurations can be found in config/database.yml file.
42
38
 
43
- def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
44
- execute(to_sql(sql, binds), name)
45
- end
39
+ To resolve this error:
46
40
 
47
- def exec_delete(sql, name = nil, binds = [])
48
- result = execute(to_sql(sql, binds), name)
49
- result.affected_rows
50
- end
51
-
52
- alias :exec_update :exec_delete
41
+ - Did you create the database for this app, or delete it? You may need to create your database.
42
+ - Has the database name changed? Check your database.yml config has the correct database name.
53
43
 
54
- def high_precision_current_timestamp
55
- HIGH_PRECISION_CURRENT_TIMESTAMP
56
- end
44
+ To create your database, run:\n\n bin/rails db:create
45
+ MSG
46
+ end
47
+ end
48
+ end
57
49
 
58
- private
59
- def last_inserted_id(result)
60
- result.last_insert_id
61
- end
50
+ if ActiveRecord.version < ::Gem::Version.new('6.1.a') # ActiveRecord <= 6.0 support
51
+ require "active_record/database_configurations"
52
+ DatabaseConfigurations.class_exec do
53
+ def resolve(config) # :nodoc:
54
+ @resolver ||= ::ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver.new(::ActiveRecord::Base.configurations)
55
+ @resolver.resolve(config)
62
56
  end
57
+ end
58
+ end
63
59
 
60
+ module ConnectionAdapters
61
+ class TrilogyAdapter < ::ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
64
62
  ER_BAD_DB_ERROR = 1049
63
+ ER_DBACCESS_DENIED_ERROR = 1044
65
64
  ER_ACCESS_DENIED_ERROR = 1045
65
+ ER_SERVER_SHUTDOWN = 1053
66
66
 
67
67
  ADAPTER_NAME = "Trilogy"
68
68
 
69
- include DatabaseStatements
69
+ include Trilogy::DatabaseStatements
70
70
 
71
71
  SSL_MODES = {
72
- SSL_MODE_DISABLED: Trilogy::SSL_DISABLED,
73
- SSL_MODE_PREFERRED: Trilogy::SSL_PREFERRED_NOVERIFY,
74
- SSL_MODE_REQUIRED: Trilogy::SSL_REQUIRED_NOVERIFY,
75
- SSL_MODE_VERIFY_CA: Trilogy::SSL_VERIFY_CA,
76
- SSL_MODE_VERIFY_IDENTITY: Trilogy::SSL_VERIFY_IDENTITY
72
+ SSL_MODE_DISABLED: ::Trilogy::SSL_DISABLED,
73
+ SSL_MODE_PREFERRED: ::Trilogy::SSL_PREFERRED_NOVERIFY,
74
+ SSL_MODE_REQUIRED: ::Trilogy::SSL_REQUIRED_NOVERIFY,
75
+ SSL_MODE_VERIFY_CA: ::Trilogy::SSL_VERIFY_CA,
76
+ SSL_MODE_VERIFY_IDENTITY: ::Trilogy::SSL_VERIFY_IDENTITY
77
77
  }.freeze
78
78
 
79
79
  class << self
80
80
  def new_client(config)
81
81
  config[:ssl_mode] = parse_ssl_mode(config[:ssl_mode]) if config[:ssl_mode]
82
82
  ::Trilogy.new(config)
83
- rescue Trilogy::ConnectionError, Trilogy::ProtocolError => error
83
+ rescue ::Trilogy::ConnectionError, ::Trilogy::ProtocolError => error
84
84
  raise translate_connect_error(config, error)
85
85
  end
86
86
 
@@ -88,7 +88,6 @@ module ActiveRecord
88
88
  return mode if mode.is_a? Integer
89
89
 
90
90
  m = mode.to_s.upcase
91
- # enable Mysql2 client compatibility
92
91
  m = "SSL_MODE_#{m}" unless m.start_with? "SSL_MODE_"
93
92
 
94
93
  SSL_MODES.fetch(m.to_sym, mode)
@@ -101,13 +100,58 @@ module ActiveRecord
101
100
  when ER_ACCESS_DENIED_ERROR
102
101
  ActiveRecord::DatabaseConnectionError.username_error(config[:username])
103
102
  else
104
- if error.message.match?(/TRILOGY_DNS_ERROR/)
103
+ if error.message.include?("TRILOGY_DNS_ERROR")
105
104
  ActiveRecord::DatabaseConnectionError.hostname_error(config[:host])
106
105
  else
107
106
  ActiveRecord::ConnectionNotEstablished.new(error.message)
108
107
  end
109
108
  end
110
109
  end
110
+
111
+ def dbconsole(config, options = {})
112
+ mysql_config = if ActiveRecord.version < ::Gem::Version.new('6.1.a')
113
+ config.config
114
+ else
115
+ config.configuration_hash
116
+ end
117
+
118
+ args = {
119
+ host: "--host",
120
+ port: "--port",
121
+ socket: "--socket",
122
+ username: "--user",
123
+ encoding: "--default-character-set",
124
+ sslca: "--ssl-ca",
125
+ sslcert: "--ssl-cert",
126
+ sslcapath: "--ssl-capath",
127
+ sslcipher: "--ssl-cipher",
128
+ sslkey: "--ssl-key",
129
+ ssl_mode: "--ssl-mode"
130
+ }.filter_map { |opt, arg| "#{arg}=#{mysql_config[opt]}" if mysql_config[opt] }
131
+
132
+ if mysql_config[:password] && options[:include_password]
133
+ args << "--password=#{mysql_config[:password]}"
134
+ elsif mysql_config[:password] && !mysql_config[:password].to_s.empty?
135
+ args << "-p"
136
+ end
137
+
138
+ args << mysql_config[:database]
139
+
140
+ find_cmd_and_exec(["mysql", "mysql5"], *args)
141
+ end
142
+
143
+ private
144
+ def initialize_type_map(m)
145
+ super if ActiveRecord.version >= ::Gem::Version.new('7.0.a')
146
+
147
+ m.register_type(%r(char)i) do |sql_type|
148
+ limit = extract_limit(sql_type)
149
+ Type.lookup(:string, adapter: :trilogy, limit: limit)
150
+ end
151
+
152
+ m.register_type %r(^enum)i, Type.lookup(:string, adapter: :trilogy)
153
+ m.register_type %r(^set)i, Type.lookup(:string, adapter: :trilogy)
154
+ end
111
155
  end
112
156
 
113
157
  def initialize(connection, logger, connection_options, config)
@@ -118,6 +162,8 @@ module ActiveRecord
118
162
  )
119
163
  end
120
164
 
165
+ TYPE_MAP = Type::TypeMap.new.tap { |m| initialize_type_map(m) }
166
+
121
167
  def supports_json?
122
168
  !mariadb? && database_version >= "5.7.8"
123
169
  end
@@ -143,7 +189,7 @@ module ActiveRecord
143
189
  end
144
190
 
145
191
  def quote_string(string)
146
- with_trilogy_connection(allow_retry: true, uses_transaction: false) do |conn|
192
+ with_trilogy_connection(allow_retry: true, materialize_transactions: false) do |conn|
147
193
  conn.escape(string)
148
194
  end
149
195
  end
@@ -157,36 +203,17 @@ module ActiveRecord
157
203
  @lock.synchronize do
158
204
  disconnect!
159
205
  connect
160
- rescue StandardError => original_exception
161
- raise translate_exception_class(original_exception, nil, nil)
162
206
  end
163
207
  end
164
208
 
165
- def with_trilogy_connection(uses_transaction: true, **_kwargs)
209
+ def with_trilogy_connection(materialize_transactions: true, **_kwargs)
166
210
  @lock.synchronize do
167
211
  verify!
168
- materialize_transactions if uses_transaction
212
+ materialize_transactions if materialize_transactions
169
213
  yield connection
170
214
  end
171
215
  end
172
216
 
173
- def raw_execute(sql, name, async: false, allow_retry: false, uses_transaction: true)
174
- mark_transaction_written_if_write(sql)
175
-
176
- log(sql, name, async: async) do
177
- with_trilogy_connection(allow_retry: allow_retry, uses_transaction: uses_transaction) do |conn|
178
- sync_timezone_changes(conn)
179
- conn.query(sql)
180
- end
181
- end
182
- end
183
-
184
- def execute(sql, name = nil, **kwargs)
185
- sql = transform_query(sql)
186
- check_if_write_query(sql)
187
- super
188
- end
189
-
190
217
  def active?
191
218
  return false if connection&.closed?
192
219
 
@@ -206,90 +233,78 @@ module ActiveRecord
206
233
  end
207
234
 
208
235
  def discard!
209
- self.connection = nil
236
+ super
237
+ unless connection.nil?
238
+ connection.discard!
239
+ self.connection = nil
240
+ end
210
241
  end
211
242
 
212
- def each_hash(result)
213
- return to_enum(:each_hash, result) unless block_given?
243
+ def self.find_cmd_and_exec(commands, *args) # :doc:
244
+ commands = Array(commands)
214
245
 
215
- keys = result.fields.map(&:to_sym)
216
- result.rows.each do |row|
217
- hash = {}
218
- idx = 0
219
- row.each do |value|
220
- hash[keys[idx]] = value
221
- idx += 1
222
- end
223
- yield hash
246
+ dirs_on_path = ENV["PATH"].to_s.split(File::PATH_SEPARATOR)
247
+ unless (ext = RbConfig::CONFIG["EXEEXT"]).empty?
248
+ commands = commands.map { |cmd| "#{cmd}#{ext}" }
224
249
  end
225
250
 
226
- nil
227
- end
251
+ full_path_command = nil
252
+ found = commands.detect do |cmd|
253
+ dirs_on_path.detect do |path|
254
+ full_path_command = File.join(path, cmd)
255
+ begin
256
+ stat = File.stat(full_path_command)
257
+ rescue SystemCallError
258
+ else
259
+ stat.file? && stat.executable?
260
+ end
261
+ end
262
+ end
228
263
 
229
- def error_number(exception)
230
- exception.error_code if exception.respond_to?(:error_code)
264
+ if found
265
+ exec full_path_command, *args
266
+ else
267
+ abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.")
268
+ end
231
269
  end
232
270
 
233
271
  private
234
- attr_accessor :connection
235
-
236
- def connect
237
- self.connection = self.class.new_client(@config)
238
- end
239
-
240
- def reconnect
241
- connection&.close
242
- self.connection = nil
243
- connect
272
+ def text_type?(type)
273
+ TYPE_MAP.lookup(type).is_a?(Type::String) || TYPE_MAP.lookup(type).is_a?(Type::Text)
244
274
  end
245
275
 
246
- def sync_timezone_changes(conn)
247
- # Sync any changes since connection last established.
248
- if ActiveRecord.default_timezone == :local
249
- conn.query_flags |= ::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
250
- else
251
- conn.query_flags &= ~::Trilogy::QUERY_FLAGS_LOCAL_TIMEZONE
252
- end
253
- end
276
+ def each_hash(result)
277
+ return to_enum(:each_hash, result) unless block_given?
254
278
 
255
- def execute_batch(statements, name = nil)
256
- statements = statements.map { |sql| transform_query(sql) }
257
- combine_multi_statements(statements).each do |statement|
258
- with_trilogy_connection do |conn|
259
- raw_execute(statement, name)
260
- conn.next_result while conn.more_results_exist?
279
+ keys = result.columns.map(&:to_sym)
280
+ result.rows.each do |row|
281
+ hash = {}
282
+ idx = 0
283
+ row.each do |value|
284
+ hash[keys[idx]] = value
285
+ idx += 1
261
286
  end
287
+ yield hash
262
288
  end
263
- end
264
289
 
265
- def multi_statements_enabled?
266
- !!@config[:multi_statement]
290
+ nil
267
291
  end
268
292
 
269
- def with_multi_statements
270
- if multi_statements_enabled?
271
- return yield
272
- end
293
+ def error_number(exception)
294
+ exception.error_code if exception.respond_to?(:error_code)
295
+ end
273
296
 
274
- with_trilogy_connection do |conn|
275
- conn.set_server_option(Trilogy::SET_SERVER_MULTI_STATEMENTS_ON)
297
+ attr_accessor :connection
276
298
 
277
- yield
278
- ensure
279
- conn.set_server_option(Trilogy::SET_SERVER_MULTI_STATEMENTS_OFF)
280
- end
299
+ def connect
300
+ self.connection = self.class.new_client(@config)
301
+ configure_connection
281
302
  end
282
303
 
283
- def combine_multi_statements(total_sql)
284
- total_sql.each_with_object([]) do |sql, total_sql_chunks|
285
- previous_packet = total_sql_chunks.last
286
- if max_allowed_packet_reached?(sql, previous_packet)
287
- total_sql_chunks << +sql
288
- else
289
- previous_packet << ";\n"
290
- previous_packet << sql
291
- end
292
- end
304
+ def reconnect
305
+ connection&.close
306
+ self.connection = nil
307
+ connect
293
308
  end
294
309
 
295
310
  def max_allowed_packet_reached?(current_packet, previous_packet)
@@ -312,21 +327,88 @@ module ActiveRecord
312
327
  end
313
328
 
314
329
  def get_full_version
315
- with_trilogy_connection(allow_retry: true, uses_transaction: false) do |conn|
330
+ with_trilogy_connection(allow_retry: true, materialize_transactions: false) do |conn|
316
331
  conn.server_info[:version]
317
332
  end
318
333
  end
319
334
 
320
- def translate_exception(exception, message:, sql:, binds:)
321
- error_code = exception.error_code if exception.respond_to?(:error_code)
322
-
323
- ::TrilogyAdapter::LostConnectionExceptionTranslator.
324
- new(exception, message, error_code).translate || super
335
+ if ActiveRecord.version < ::Gem::Version.new('7.0.a') # For ActiveRecord <= 6.1
336
+ def default_timezone
337
+ ActiveRecord::Base.default_timezone
338
+ end
339
+ else # For ActiveRecord 7.0
340
+ def default_timezone
341
+ ActiveRecord.default_timezone
342
+ end
325
343
  end
326
344
 
327
345
  def default_prepared_statements
328
346
  false
329
347
  end
348
+
349
+ if ActiveRecord.version < ::Gem::Version.new('6.1.a') # For ActiveRecord <= 6.0
350
+ def prepared_statements?
351
+ @prepared_statements && !prepared_statements_disabled_cache.include?(object_id)
352
+ end
353
+ end
354
+
355
+ def default_insert_value(column)
356
+ super unless column.auto_increment?
357
+ end
358
+
359
+ # https://mariadb.com/kb/en/analyze-statement/
360
+ def analyze_without_explain?
361
+ mariadb? && database_version >= "10.1.0"
362
+ end
363
+
364
+ ActiveRecord::Type.register(:immutable_string, adapter: :trilogy) do |_, **args|
365
+ Type::ImmutableString.new(true: "1", false: "0", **args)
366
+ end
367
+
368
+ ActiveRecord::Type.register(:string, adapter: :trilogy) do |_, **args|
369
+ Type::String.new(true: "1", false: "0", **args)
370
+ end
371
+
372
+ ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :trilogy)
373
+ end
374
+
375
+ if ActiveRecord.version < ::Gem::Version.new('6.1.a') # For ActiveRecord <= 6.0
376
+ class PoolConfig < ConnectionSpecification
377
+ def initialize(connection_class, db_config, *args)
378
+ super("primary", db_config, "#{db_config[:adapter]}_connection")
379
+ end
380
+ end
381
+
382
+ SchemaCache.class_exec do
383
+ def self.load_from(filename)
384
+ return unless File.file?(filename)
385
+
386
+ read(filename) do |file|
387
+ if filename.include?(".dump")
388
+ Marshal.load(file)
389
+ else
390
+ if YAML.respond_to?(:unsafe_load)
391
+ YAML.unsafe_load(file)
392
+ else
393
+ YAML.load(file)
394
+ end
395
+ end
396
+ end
397
+ end
398
+
399
+ def self.read(filename, &block)
400
+ if File.extname(filename) == ".gz"
401
+ Zlib::GzipReader.open(filename) { |gz|
402
+ yield gz.read
403
+ }
404
+ else
405
+ yield File.read(filename)
406
+ end
407
+ end
408
+ private_class_method :read
409
+ end
330
410
  end
411
+
412
+ ActiveSupport.run_load_hooks(:active_record_trilogyadapter, TrilogyAdapter)
331
413
  end
332
414
  end
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_record"
4
- require "trilogy_adapter/errors"
5
4
  require "trilogy_adapter/railtie"
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TrilogyAdapter
4
+ module Rails
5
+ module DBConsole
6
+ class AdapterAdapter < SimpleDelegator
7
+ def adapter
8
+ "mysql"
9
+ end
10
+ end
11
+
12
+ def db_config
13
+ if super.adapter == "trilogy"
14
+ AdapterAdapter.new(super)
15
+ else
16
+ super
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ module Rails
24
+ class DBConsole
25
+ # require "rails/commands/dbconsole/dbconsole_command"
26
+ if ActiveRecord.version < ::Gem::Version.new('6.1.a')
27
+ alias _brick_start start
28
+ def start
29
+ ENV["RAILS_ENV"] ||= @options[:environment] || environment
30
+
31
+ if config["adapter"] == "trilogy"
32
+ begin
33
+ ::ActiveRecord::ConnectionAdapters::TrilogyAdapter.dbconsole(config, @options)
34
+ rescue NotImplementedError
35
+ abort "Unknown command-line client for #{db_config.database}."
36
+ end
37
+ else
38
+ _brick_start
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TrilogyAdapter
4
- VERSION = "3.0.0"
4
+ VERSION = "3.1.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-trilogy-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - GitHub Engineering
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-06-16 00:00:00.000000000 Z
11
+ date: 2023-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: trilogy
@@ -30,20 +30,20 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '7.0'
33
+ version: 6.0.a
34
34
  - - "<"
35
35
  - !ruby/object:Gem::Version
36
- version: 7.1a
36
+ version: 7.1.a
37
37
  type: :runtime
38
38
  prerelease: false
39
39
  version_requirements: !ruby/object:Gem::Requirement
40
40
  requirements:
41
41
  - - ">="
42
42
  - !ruby/object:Gem::Version
43
- version: '7.0'
43
+ version: 6.0.a
44
44
  - - "<"
45
45
  - !ruby/object:Gem::Version
46
- version: 7.1a
46
+ version: 7.1.a
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: minitest
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -111,12 +111,12 @@ extra_rdoc_files:
111
111
  files:
112
112
  - LICENSE.md
113
113
  - README.md
114
+ - lib/active_record/connection_adapters/trilogy/database_statements.rb
114
115
  - lib/active_record/connection_adapters/trilogy_adapter.rb
115
116
  - lib/active_record/tasks/trilogy_database_tasks.rb
116
117
  - lib/activerecord-trilogy-adapter.rb
117
118
  - lib/trilogy_adapter/connection.rb
118
- - lib/trilogy_adapter/errors.rb
119
- - lib/trilogy_adapter/lost_connection_exception_translator.rb
119
+ - lib/trilogy_adapter/rails/dbconsole.rb
120
120
  - lib/trilogy_adapter/railtie.rb
121
121
  - lib/trilogy_adapter/version.rb
122
122
  homepage: https://github.com/trilogy-libraries/activerecord-trilogy-adapter
@@ -1,45 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TrilogyAdapter
4
- module Errors
5
- # ServerShutdown will be raised when the database server was shutdown.
6
- class ServerShutdown < ::ActiveRecord::QueryAborted
7
- end
8
-
9
- # ServerLost will be raised when the database connection was lost.
10
- class ServerLost < ::ActiveRecord::QueryAborted
11
- end
12
-
13
- # ServerGone will be raised when the database connection is gone.
14
- class ServerGone < ::ActiveRecord::QueryAborted
15
- end
16
-
17
- # BrokenPipe will be raised when a system process connection fails.
18
- class BrokenPipe < ::ActiveRecord::QueryAborted
19
- end
20
-
21
- # SocketError will be raised when Ruby encounters a network error.
22
- class SocketError < ::ActiveRecord::QueryAborted
23
- end
24
-
25
- # ConnectionResetByPeer will be raised when a network connection is closed
26
- # outside the sytstem process.
27
- class ConnectionResetByPeer < ::ActiveRecord::QueryAborted
28
- end
29
-
30
- # ClosedConnection will be raised when the Trilogy encounters a closed
31
- # connection.
32
- class ClosedConnection < ::ActiveRecord::QueryAborted
33
- end
34
-
35
- # InvalidSequenceId will be raised when Trilogy ecounters an invalid sequence
36
- # id.
37
- class InvalidSequenceId < ::ActiveRecord::QueryAborted
38
- end
39
-
40
- # UnexpectedPacket will be raised when Trilogy ecounters an unexpected
41
- # response packet.
42
- class UnexpectedPacket < ::ActiveRecord::QueryAborted
43
- end
44
- end
45
- end
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TrilogyAdapter
4
- class LostConnectionExceptionTranslator
5
- attr_reader :exception, :message, :error_number
6
-
7
- def initialize(exception, message, error_number)
8
- @exception = exception
9
- @message = message
10
- @error_number = error_number
11
- end
12
-
13
- def translate
14
- translate_database_exception || translate_ruby_exception || translate_trilogy_exception
15
- end
16
-
17
- private
18
- ER_SERVER_SHUTDOWN = 1053
19
- CR_SERVER_LOST = 2013
20
- CR_SERVER_LOST_EXTENDED = 2055
21
- CR_SERVER_GONE_ERROR = 2006
22
-
23
- def translate_database_exception
24
- case error_number
25
- when ER_SERVER_SHUTDOWN
26
- Errors::ServerShutdown.new(message)
27
- when CR_SERVER_LOST, CR_SERVER_LOST_EXTENDED
28
- Errors::ServerLost.new(message)
29
- when CR_SERVER_GONE_ERROR
30
- Errors::ServerGone.new(message)
31
- end
32
- end
33
-
34
- def translate_ruby_exception
35
- case exception
36
- when Errno::EPIPE
37
- Errors::BrokenPipe.new(message)
38
- when SocketError, IOError
39
- Errors::SocketError.new(message)
40
- when Trilogy::ConnectionError
41
- if message.match?(/Connection reset by peer/)
42
- Errors::ConnectionResetByPeer.new(message)
43
- end
44
- end
45
- end
46
-
47
- def translate_trilogy_exception
48
- return unless exception.is_a?(Trilogy::Error)
49
-
50
- case message
51
- when /TRILOGY_CLOSED_CONNECTION/
52
- Errors::ClosedConnection.new(message)
53
- when /TRILOGY_INVALID_SEQUENCE_ID/
54
- Errors::InvalidSequenceId.new(message)
55
- when /TRILOGY_UNEXPECTED_PACKET/
56
- Errors::UnexpectedPacket.new(message)
57
- end
58
- end
59
- end
60
- end