activerecord-trilogy-adapter 3.0.0 → 3.1.1

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