aws_advanced_ruby_driver_wrapper 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -2
  3. data/README.md +115 -6
  4. data/THIRD-PARTY-LICENSES +70 -11
  5. data/aws_advanced_ruby_driver_wrapper.gemspec +9 -0
  6. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_action_cable_postgresql_support.rb +40 -0
  7. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_connection_handler.rb +47 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +23 -9
  9. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +65 -10
  10. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
  11. data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +61 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +48 -0
  13. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
  14. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -81
  16. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +28 -0
  17. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
  20. data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
  21. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
  22. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +11 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +14 -0
  24. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +6 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
  26. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +125 -7
  27. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
  28. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
  29. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
  30. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
  31. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
  32. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
  33. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
  34. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
  35. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +10 -13
  36. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +95 -3
  37. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
  38. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
  39. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
  40. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
  41. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
  42. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
  43. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
  44. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
  45. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
  46. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
  48. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
  49. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
  50. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
  52. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +23 -1
  53. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +49 -23
  54. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
  55. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
  56. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
  57. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
  58. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +23 -4
  59. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
  60. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
  61. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
  62. data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
  63. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
  64. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
  65. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +18 -7
  66. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
  67. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
  68. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
  69. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
  70. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
  71. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
  72. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
  73. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
  74. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
  75. data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +29 -8
  77. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
  79. data/lib/aws_advanced_ruby_driver_wrapper/utils/{ar_constants.rb → telemetry/open_telemetry_counter.rb} +19 -5
  80. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
  81. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
  82. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
  83. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
  84. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
  85. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
  86. data/lib/aws_advanced_ruby_driver_wrapper.rb +47 -2
  87. metadata +57 -3
@@ -17,40 +17,82 @@
17
17
  require 'active_record/connection_adapters/postgresql_adapter'
18
18
  require_relative '../postgresql'
19
19
  require_relative '../errors'
20
- require_relative '../utils/ar_constants'
20
+ require_relative 'type_adapter_alias'
21
+ require_relative 'aws_connection_handler'
22
+ require_relative 'aws_action_cable_postgresql_support'
21
23
 
22
24
  module ActiveRecord
23
25
  module ConnectionAdapters
24
26
  class AwsPostgreSQLAdapter < PostgreSQLAdapter
25
- ADAPTER_NAME = 'AwsPostgreSQL'
26
-
27
- # ActiveRecord-only keys that should not be passed to the wrapper or native driver.
28
- AR_ONLY_KEYS = (AwsAdvancedRubyDriverWrapper::Utils::AR_COMMON_KEYS + %i[advisory_locks schema_search_path]).freeze
29
-
30
27
  # ActiveRecord uses :username and :database, but the PG gem expects :user and :dbname.
31
28
  # The parent adapter translates these in @connection_parameters, but also strips
32
29
  # non-PG keys (including wrapper properties) via slice!. Since we rebuild from @config
33
30
  # to preserve wrapper properties, we need to redo this translation ourselves.
34
31
  AR_TO_PG_KEY_MAP = { username: :user, database: :dbname }.freeze
35
32
 
33
+ # The connection options PG accepts, the same set the parent adapter forwards.
34
+ PG_CONNECTION_KEYS = (PG::Connection.conndefaults_hash.keys + [:requiressl]).to_set.freeze
35
+
36
36
  def initialize(...)
37
37
  # Capture the full config before the parent's initialize strips it
38
38
  # down to only PG-recognized keys in @connection_parameters.
39
39
  super
40
40
  @connection_broken = false
41
- # Store non-AR parameters in @wrapper_config.
42
- wrapper_config = @config.compact.except(*AR_ONLY_KEYS)
41
+ wrapper_config = @config.compact
43
42
  # Remap ActiveRecord key names to PG gem key names.
44
43
  AR_TO_PG_KEY_MAP.each { |ar_key, pg_key| wrapper_config[pg_key] = wrapper_config.delete(ar_key) if wrapper_config.key?(ar_key) }
45
- @wrapper_config = wrapper_config
44
+ # Keep only what PG or the wrapper understands. Every other key is an ActiveRecord setting,
45
+ # and PG rejects any connection option it does not recognize.
46
+ @wrapper_config = wrapper_config.select { |key, _| self.class.forwarded_key?(key) }
47
+ end
48
+
49
+ # @return [Boolean] whether the config key is passed on to the wrapper connection
50
+ def self.forwarded_key?(key)
51
+ PG_CONNECTION_KEYS.include?(key) ||
52
+ AwsAdvancedRubyDriverWrapper::PropertyDefinition.wrapper_property?(key) ||
53
+ AwsAdvancedRubyDriverWrapper::PropertyDefinition::KNOWN_PREFIXES.any? { |prefix| key.to_s.start_with?(prefix) }
54
+ end
55
+
56
+ # Resolve native database types from the wrapped PostgreSQLAdapter, overriding only :datetime.
57
+ # PostgreSQLAdapter builds its type hash from two things: the shared NATIVE_DATABASE_TYPES
58
+ # constant and the per-class datetime_type class_attribute. Delegating to the parent keeps a
59
+ # single source of truth, but the parent reads *its own* datetime_type, so this adapter's
60
+ # datetime_type (e.g. :timestamptz) must be re-applied here or it is silently ignored and
61
+ # t.datetime columns are created as plain timestamp.
62
+ def self.native_database_types
63
+ superclass.native_database_types.merge(datetime: NATIVE_DATABASE_TYPES[datetime_type])
46
64
  end
47
65
 
48
- def adapter_name = ADAPTER_NAME
66
+ # SQLSTATEs and FATAL-message signals for an authentication/authorization rejection (invalid
67
+ # password, PAM failure, pg_hba.conf rejection). A failed *connect* usually carries no result, so
68
+ # the message text is the reliable signal and the SQLSTATE is only a bonus when one is present.
69
+ AUTH_REJECTION_SQLSTATES = %w[28P01 28000].freeze
70
+ AUTH_REJECTION_MESSAGE = /password authentication failed|PAM authentication failed|pg_hba\.conf/
49
71
 
50
72
  # Note that this config includes wrapper properties.
73
+ #
74
+ # Mirrors the parent PostgreSQLAdapter.new_client so ActiveRecord can tell a missing database
75
+ # (which db:prepare creates) or rejected credentials from other failures.
51
76
  def self.new_client(config)
52
77
  AwsAdvancedRubyDriverWrapper::WrapperPgConnection.new(**config)
78
+ rescue ::PG::Error => e
79
+ dbname, user, host = config.values_at(:dbname, :user, :host)
80
+ # The postgres maintenance database always exists, so a failure there is not a missing database.
81
+ raise ActiveRecord::ConnectionNotEstablished, e.message if dbname == 'postgres'
82
+ raise ActiveRecord::NoDatabaseError.db_error(dbname) if dbname && e.message.include?(dbname) && !auth_rejection?(e)
83
+ raise ActiveRecord::DatabaseConnectionError.username_error(user) if user && e.message.include?(user)
84
+ raise ActiveRecord::DatabaseConnectionError.hostname_error(host) if host && e.message.include?(host)
85
+
86
+ raise ActiveRecord::ConnectionNotEstablished, e.message
87
+ end
88
+
89
+ # @return [Boolean] whether the PG error is an authentication/authorization rejection, by SQLSTATE
90
+ # when the error carries a result and otherwise by its canonical FATAL text.
91
+ def self.auth_rejection?(error)
92
+ sqlstate = error.result&.error_field(PG::PG_DIAG_SQLSTATE)
93
+ AUTH_REJECTION_SQLSTATES.include?(sqlstate) || error.message.match?(AUTH_REJECTION_MESSAGE)
53
94
  end
95
+ private_class_method :auth_rejection?
54
96
 
55
97
  def connect
56
98
  @connection_broken = false
@@ -71,10 +113,23 @@ module ActiveRecord
71
113
  super
72
114
  end
73
115
 
116
+ # Used by db:drop, db:reset, db:test:prepare and db:purge. ActiveRecord disconnects its own
117
+ # connections first, but the topology monitor shared by this cluster's connections, and the Blue/Green
118
+ # status providers, keep their own connections to the database, and PostgreSQL refuses to drop a
119
+ # database other sessions are using.
120
+ def drop_database(name)
121
+ raw_connection.stop_topology_monitor
122
+ AwsAdvancedRubyDriverWrapper::Plugins::BlueGreen::BlueGreenPlugin.clean_up_providers
123
+ super
124
+ end
125
+
74
126
  def translate_exception(exception, message:, sql:, binds:)
75
127
  return super unless exception.is_a?(AwsAdvancedRubyDriverWrapper::Errors::AwsError)
76
128
 
77
129
  if exception.needs_reconfiguration
130
+ # The wrapper has reconnected this connection object to a new physical connection, where the
131
+ # statements prepared on the old one do not exist. Forget them so they are prepared again.
132
+ clear_cache!(new_connection: true)
78
133
  configure_connection
79
134
  exception
80
135
  elsif exception.is_a?(AwsAdvancedRubyDriverWrapper::Errors::FailoverFailedError)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'active_record/connection_adapters/trilogy_adapter'
18
+ require_relative '../trilogy'
19
+ require_relative '../errors'
20
+ require_relative 'type_adapter_alias'
21
+ require_relative 'aws_connection_handler'
22
+
23
+ module ActiveRecord
24
+ module ConnectionAdapters
25
+ class AwsTrilogyAdapter < TrilogyAdapter
26
+ # Connects as the parent adapter does, through the wrapper instead of trilogy itself: the ssl_mode
27
+ # name is turned into the constant trilogy reads, and the whole config is passed on, since the
28
+ # wrapper takes its own properties out and trilogy ignores the ActiveRecord-only keys.
29
+ #
30
+ # Translates connection errors the same way the parent adapter does, so ActiveRecord can tell a
31
+ # missing database (which db:prepare creates) or rejected credentials from other failures.
32
+ def self.new_client(config)
33
+ config[:ssl_mode] = parse_ssl_mode(config[:ssl_mode]) if config[:ssl_mode]
34
+ AwsAdvancedRubyDriverWrapper::WrapperTrilogyClient.new(config)
35
+ rescue ::Trilogy::Error => e
36
+ raise translate_connect_error(config, e)
37
+ end
38
+
39
+ def connect
40
+ @connection_broken = false
41
+ super
42
+ end
43
+
44
+ def reconnect
45
+ @connection_broken = false
46
+ super
47
+ end
48
+
49
+ def active?
50
+ return false if @connection_broken
51
+
52
+ super
53
+ end
54
+
55
+ def translate_exception(exception, message:, sql:, binds:)
56
+ return super unless exception.is_a?(AwsAdvancedRubyDriverWrapper::Errors::AwsError)
57
+
58
+ if exception.needs_reconfiguration
59
+ # The wrapper has reconnected this connection object to a new physical connection, where the
60
+ # session ActiveRecord configured on the old one does not exist. Forget what was cached about
61
+ # that connection and configure the new one.
62
+ clear_cache!(new_connection: true)
63
+ configure_connection
64
+ exception
65
+ elsif exception.is_a?(AwsAdvancedRubyDriverWrapper::Errors::FailoverFailedError)
66
+ @connection_broken = true
67
+ ActiveRecord::ConnectionFailed.new(message, sql:, binds:, connection_pool: @pool)
68
+ else
69
+ exception
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ ActiveRecord::ConnectionAdapters.register(
77
+ 'aws_trilogy',
78
+ 'ActiveRecord::ConnectionAdapters::AwsTrilogyAdapter',
79
+ 'aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter'
80
+ )
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require 'active_record'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ # ActiveRecord's adapter-specific type registry (ActiveRecord::Type) keys every type
21
+ # registration and lookup by the *config adapter name*. The wrapped adapters register nothing of
22
+ # their own: they rely entirely on the vanilla PostgreSQL/Mysql2 adapters, which register their
23
+ # OID / native types under adapter: :postgresql and adapter: :mysql2. A lookup for an
24
+ # :aws_postgresql / :aws_mysql2 connection would therefore find none of them and raise
25
+ # "Unknown type" for things like :interval, :point, array/range columns, or MySQL's
26
+ # :unsigned_integer.
27
+ #
28
+ # Rather than copy the parent's registrations (which misses types registered after the adapter
29
+ # loads, needs a duplicate guard, and reaches into the registry's private internals), alias the
30
+ # wrapper adapter names to their parents inside the registry's own register/add_modifier/lookup.
31
+ # Every registration and every lookup — including the ones ActiveRecord routes through
32
+ # Type.adapter_name_from — passes through the registry, so normalizing the adapter there makes the
33
+ # wrapped adapters resolve the exact same types as the vanilla ones, for all current and future
34
+ # types, across Rails 7.2–8.1.
35
+ #
36
+ # Side effect: in an app that uses BOTH a plain postgresql and an aws_postgresql connection, a
37
+ # type explicitly registered for :aws_postgresql is normalized to :postgresql and so also applies
38
+ # to the plain postgresql connections (and vice versa). This is documented in the CHANGELOG.
39
+ module TypeAdapterAlias
40
+ ADAPTER_ALIASES = { aws_postgresql: :postgresql, aws_mysql2: :mysql2, aws_trilogy: :trilogy }.freeze
41
+
42
+ # Prepended onto ActiveRecord::Type::AdapterSpecificRegistry. Normalizing the adapter in lookup
43
+ # covers both the framework path and a direct Type.lookup(..., adapter: :aws_postgresql); any
44
+ # type registered explicitly for a wrapper adapter lands under the parent's key.
45
+ module RegistryAlias
46
+ def register(type_name, klass = nil, adapter: nil, **, &)
47
+ super(type_name, klass, adapter: ADAPTER_ALIASES.fetch(adapter, adapter), **, &)
48
+ end
49
+
50
+ def add_modifier(options, klass, adapter: nil, **args)
51
+ super(options, klass, adapter: ADAPTER_ALIASES.fetch(adapter, adapter), **args)
52
+ end
53
+
54
+ def lookup(symbol, *, adapter: nil, **)
55
+ super(symbol, *, adapter: ADAPTER_ALIASES.fetch(adapter, adapter), **)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ ActiveRecord::Type::AdapterSpecificRegistry.prepend(AwsAdvancedRubyDriverWrapper::TypeAdapterAlias::RegistryAlias)
@@ -33,6 +33,17 @@ module AwsAdvancedRubyDriverWrapper
33
33
 
34
34
  DEFAULT_MONITORING_TIMEOUT_SEC = 5
35
35
 
36
+ # @return [String] the name of the Ruby driver gem this dialect drives
37
+ def name
38
+ raise NotImplementedError
39
+ end
40
+
41
+ # The database engine the driver talks to, which decides the database dialects that apply.
42
+ # @return [Symbol] +:mysql+ or +:postgresql+
43
+ def database_engine
44
+ raise NotImplementedError
45
+ end
46
+
36
47
  def connect(host_info, config)
37
48
  raise NotImplementedError
38
49
  end
@@ -115,6 +126,11 @@ module AwsAdvancedRubyDriverWrapper
115
126
  raise NotImplementedError
116
127
  end
117
128
 
129
+ # Releases a connection inherited from the parent process after a fork, without telling the
130
+ # server: the parent still owns the session. Garbage collection or process exit in the child
131
+ # must not end it.
132
+ def abandon_connection(connection); end
133
+
118
134
  def sql_state(_exception)
119
135
  nil
120
136
  end
@@ -133,6 +149,29 @@ module AwsAdvancedRubyDriverWrapper
133
149
  :user
134
150
  end
135
151
 
152
+ # Returns the property key the underlying driver expects for the TLS CA certificate file.
153
+ # Used by Blue/Green substitute routing for drivers that overload the host for both the socket
154
+ # address and the TLS server name (mysql2, trilogy): it tells whether TLS is active before
155
+ # deciding to connect by hostname rather than raw IP, avoiding a certificate-name mismatch.
156
+ # Drivers that can pin the socket separately (see #props_pinned_to_ip) do not use this.
157
+ # mysql2 names it +:sslca+ (the default); override where it differs (trilogy uses +:ssl_ca+).
158
+ def tls_ca_property_key
159
+ :sslca
160
+ end
161
+
162
+ # Blue/Green substitute routing must connect to a specific instance IP — so a DNS lookup during
163
+ # the switchover window cannot resolve to the old instance — while still presenting the original
164
+ # hostname for TLS certificate verification and IAM token generation. A driver that can pin the
165
+ # socket address separately from the TLS/auth server name returns a copy of +driver_props+ that
166
+ # connects to +ip+ but keeps the hostname for verification; a driver that overloads the host for
167
+ # both returns +nil+, and the routing falls back to the #tls_ca_property_key hostname check.
168
+ # @param driver_props [Hash]
169
+ # @param ip [String] the instance IP to pin the socket to
170
+ # @return [Hash, nil] props pinned to +ip+, or +nil+ when the driver cannot split socket from TLS name
171
+ def props_pinned_to_ip(_driver_props, _ip)
172
+ nil
173
+ end
174
+
136
175
  # Applies default socket/connect timeouts to monitoring connection driver props.
137
176
  # These ensure that a query or close on a dead connection raises a timeout error
138
177
  # rather than hanging indefinitely or segfaulting.
@@ -149,6 +188,15 @@ module AwsAdvancedRubyDriverWrapper
149
188
  def reported_in_transaction(_connection)
150
189
  nil
151
190
  end
191
+
192
+ # The server version that decides which versioned /*!80023 ... */ executable comments the server
193
+ # runs, as MySQL numbers it (8.0.39 is 80039). Only MySQL has executable comments.
194
+ # @param connection [Object]
195
+ # @return [Integer, nil] nil when the driver's SQL has no executable comments, or the version
196
+ # cannot be read
197
+ def executable_comment_version(_connection)
198
+ nil
199
+ end
152
200
  end
153
201
  end
154
202
  end
@@ -14,26 +14,33 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
- require_relative 'mysql_driver_dialect'
17
+ require_relative 'mysql2_driver_dialect'
18
18
  require_relative 'pg_driver_dialect'
19
+ require_relative 'trilogy_driver_dialect'
19
20
  require_relative '../errors'
20
21
  require_relative '../errors/mysql_error_handler'
21
22
  require_relative '../errors/pg_error_handler'
23
+ require_relative '../errors/trilogy_error_handler'
22
24
 
23
25
  module AwsAdvancedRubyDriverWrapper
24
26
  module DriverDialects
25
27
  module DriverDialectManager
26
- MYSQL_DIALECT = MysqlDriverDialect.new.freeze
28
+ MYSQL2_DIALECT = Mysql2DriverDialect.new.freeze
27
29
  PG_DIALECT = PgDriverDialect.new.freeze
30
+ TRILOGY_DIALECT = TrilogyDriverDialect.new.freeze
28
31
 
29
32
  REGISTRY = {
30
33
  mysql2: {
31
- driver_dialect: MYSQL_DIALECT,
32
- error_handler: Errors::MysqlErrorHandler.new(MYSQL_DIALECT)
34
+ driver_dialect: MYSQL2_DIALECT,
35
+ error_handler: Errors::MysqlErrorHandler.new(MYSQL2_DIALECT)
33
36
  },
34
37
  postgresql: {
35
38
  driver_dialect: PG_DIALECT,
36
39
  error_handler: Errors::PgErrorHandler.new(PG_DIALECT)
40
+ },
41
+ trilogy: {
42
+ driver_dialect: TRILOGY_DIALECT,
43
+ error_handler: Errors::TrilogyErrorHandler.new(TRILOGY_DIALECT)
37
44
  }
38
45
  }.freeze
39
46
 
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License").
6
+ # You may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require_relative 'mysql_driver_dialect'
18
+
19
+ module AwsAdvancedRubyDriverWrapper
20
+ module DriverDialects
21
+ # How the mysql2 driver is called. The MySQL grammar and what every MySQL driver shares are
22
+ # inherited.
23
+ class Mysql2DriverDialect < MysqlDriverDialect
24
+ # Every mysql2 call that talks to the server. A call that is not listed here is handed straight
25
+ # to the driver, bypassing the plugin pipeline.
26
+ #
27
+ # Not listed, because libmysql answers them without talking to the server: escape, the row and
28
+ # column counts, last_id, affected_rows, info, warning_count, thread_id, server_info,
29
+ # session_track, and the connection's own settings.
30
+ NETWORK_BOUND_METHODS = (COMMON_NETWORK_BOUND_METHODS | Set[
31
+ RubyMethod::CONNECTION_QUERY.name,
32
+ RubyMethod::CONNECTION_ASYNC_RESULT.name,
33
+ RubyMethod::CONNECTION_SELECT_DB.name,
34
+ RubyMethod::CONNECTION_MORE_RESULTS.name,
35
+ RubyMethod::CONNECTION_NEXT_RESULT.name,
36
+ RubyMethod::CONNECTION_STORE_RESULT.name,
37
+ RubyMethod::CONNECTION_ABANDON_RESULTS.name,
38
+ RubyMethod::CONNECTION_SET_SERVER_OPTION.name,
39
+ RubyMethod::RESULT_EACH.name,
40
+ RubyMethod::RESULT_FREE.name
41
+ ]).freeze
42
+
43
+ def name
44
+ 'mysql2'
45
+ end
46
+
47
+ def connect(host_info, config)
48
+ ::Mysql2::Client.new(**prepare_connect_config(host_info, config))
49
+ end
50
+
51
+ def execute(connection, sql)
52
+ raise Mysql2::Error, 'MySQL client is not connected' if connection.nil? || connection.closed?
53
+
54
+ connection.query(sql)
55
+ end
56
+
57
+ def execute_with_params(connection, sql, params)
58
+ raise Mysql2::Error, 'MySQL client is not connected' if connection.nil? || connection.closed?
59
+
60
+ stmt = connection.prepare(sql)
61
+ result = stmt.execute(*params)
62
+ # Everything the caller needs must be read off the statement before the ensure closes its
63
+ # handle. A SELECT result is bound to the handle, so materialize its rows now - otherwise a
64
+ # later iteration (e.g. SqlRunner calling #to_a) raises "Statement handle already closed".
65
+ # A non-SELECT statement returns nil, and its affected-row count must come from the statement
66
+ # too: once the handle is closed the connection-level mysql_affected_rows returns its -1 error
67
+ # sentinel and mysql2 raises. So return that count for #affected_rows to hand back.
68
+ result.is_a?(::Mysql2::Result) ? result.to_a : stmt.affected_rows
69
+ ensure
70
+ stmt&.close
71
+ end
72
+
73
+ # mysql2 binds a blob parameter as raw binary bytes.
74
+ def binary_param(bytes)
75
+ bytes.b
76
+ end
77
+
78
+ # mysql2 hands back a blob column as a string that only needs its encoding forced to binary.
79
+ def read_binary(value)
80
+ value.b
81
+ end
82
+
83
+ # A prepared statement (execute_with_params) hands back its own affected-row count, since the
84
+ # connection-level count is unreadable once the statement handle is closed. A plain
85
+ # connection.query (execute) leaves nil, so the count is read off the connection instead.
86
+ def affected_rows(connection, result)
87
+ result.is_a?(Integer) ? result : connection.affected_rows.to_i
88
+ end
89
+
90
+ # mysql2 has no RETURNING clause, so the generated id is read from the connection afterwards.
91
+ def insert_returning_id(connection, sql, params, _id_column)
92
+ execute_with_params(connection, sql, params)
93
+ id = connection.last_id
94
+ id&.positive? ? id : nil
95
+ end
96
+
97
+ # With automatic_close off, mysql2 invalidates the socket instead of sending COM_QUIT when
98
+ # the client is collected, leaving the parent's session open.
99
+ def abandon_connection(connection)
100
+ return if connection.nil? || connection.closed?
101
+
102
+ connection.automatic_close = false
103
+ rescue StandardError => e
104
+ logger.debug("Failed to abandon MySQL connection: #{e.message}")
105
+ end
106
+
107
+ # mysql2 reports the version it learned when it connected, so reading it costs no round trip.
108
+ def executable_comment_version(connection)
109
+ return nil if connection.nil?
110
+
111
+ info = connection.server_info
112
+ info.is_a?(Hash) ? info[:id] : nil
113
+ rescue StandardError => e
114
+ logger.debug { "The MySQL server version could not be read: #{e.message}" }
115
+ nil
116
+ end
117
+
118
+ def sql_state(exception)
119
+ return nil unless exception.respond_to?(:sql_state)
120
+
121
+ exception.sql_state
122
+ end
123
+
124
+ def network_bound_methods
125
+ NETWORK_BOUND_METHODS
126
+ end
127
+ end
128
+ end
129
+ end
@@ -19,84 +19,22 @@ require_relative '../host/connection_string_host_list_provider'
19
19
 
20
20
  module AwsAdvancedRubyDriverWrapper
21
21
  module DriverDialects
22
+ # What every MySQL driver shares: the server's SQL grammar, and the connection options and
23
+ # connection calls the MySQL drivers name alike. A driver's own dialect subclasses this and adds
24
+ # how the driver itself is called.
22
25
  class MysqlDriverDialect
23
26
  include DriverDialect
24
27
 
25
- # Every mysql2 call that talks to the server. A call that is not listed here is handed straight
26
- # to the driver, bypassing the plugin pipeline.
27
- #
28
- # Not listed, because libmysql answers them without talking to the server: escape, the row and
29
- # column counts, last_id, affected_rows, info, warning_count, thread_id, server_info,
30
- # session_track, and the connection's own settings.
31
- NETWORK_BOUND_METHODS = (COMMON_NETWORK_BOUND_METHODS | Set[
32
- RubyMethod::CONNECTION_QUERY.name,
33
- RubyMethod::CONNECTION_ASYNC_RESULT.name,
34
- RubyMethod::CONNECTION_SELECT_DB.name,
35
- RubyMethod::CONNECTION_MORE_RESULTS.name,
36
- RubyMethod::CONNECTION_NEXT_RESULT.name,
37
- RubyMethod::CONNECTION_STORE_RESULT.name,
38
- RubyMethod::CONNECTION_ABANDON_RESULTS.name,
39
- RubyMethod::CONNECTION_SET_SERVER_OPTION.name,
40
- RubyMethod::RESULT_EACH.name,
41
- RubyMethod::RESULT_FREE.name
42
- ]).freeze
43
-
44
- def connect(host_info, config)
45
- ::Mysql2::Client.new(**prepare_connect_config(host_info, config))
46
- end
47
-
48
- def execute(connection, sql)
49
- raise Mysql2::Error, 'MySQL client is not connected' if connection.nil? || connection.closed?
50
-
51
- connection.query(sql)
52
- end
53
-
54
- def execute_with_params(connection, sql, params)
55
- raise Mysql2::Error, 'MySQL client is not connected' if connection.nil? || connection.closed?
56
-
57
- stmt = connection.prepare(sql)
58
- result = stmt.execute(*params)
59
- # Everything the caller needs must be read off the statement before the ensure closes its
60
- # handle. A SELECT result is bound to the handle, so materialize its rows now - otherwise a
61
- # later iteration (e.g. SqlRunner calling #to_a) raises "Statement handle already closed".
62
- # A non-SELECT statement returns nil, and its affected-row count must come from the statement
63
- # too: once the handle is closed the connection-level mysql_affected_rows returns its -1 error
64
- # sentinel and mysql2 raises. So return that count for #affected_rows to hand back.
65
- result.is_a?(::Mysql2::Result) ? result.to_a : stmt.affected_rows
66
- ensure
67
- stmt&.close
28
+ def database_engine
29
+ :mysql
68
30
  end
69
31
 
70
- # mysql2 uses +?+ placeholders, so the SQL is already in its native form.
32
+ # MySQL uses +?+ placeholders, so the SQL is already in its native form.
71
33
  def translate_placeholders(sql)
72
34
  sql
73
35
  end
74
36
 
75
- # mysql2 binds a blob parameter as raw binary bytes.
76
- def binary_param(bytes)
77
- bytes.b
78
- end
79
-
80
- # mysql2 hands back a blob column as a string that only needs its encoding forced to binary.
81
- def read_binary(value)
82
- value.b
83
- end
84
-
85
- # A prepared statement (execute_with_params) hands back its own affected-row count, since the
86
- # connection-level count is unreadable once the statement handle is closed. A plain
87
- # connection.query (execute) leaves nil, so the count is read off the connection instead.
88
- def affected_rows(connection, result)
89
- result.is_a?(Integer) ? result : connection.affected_rows.to_i
90
- end
91
-
92
- # mysql2 has no RETURNING clause, so the generated id is read from the connection afterwards.
93
- def insert_returning_id(connection, sql, params, _id_column)
94
- execute_with_params(connection, sql, params)
95
- id = connection.last_id
96
- id&.positive? ? id : nil
97
- end
98
-
99
- # mysql2 upserts with ON DUPLICATE KEY UPDATE, reading the incoming row from VALUES().
37
+ # MySQL upserts with ON DUPLICATE KEY UPDATE, reading the incoming row from VALUES().
100
38
  def upsert_clause(_conflict_columns, update_columns)
101
39
  assignments = update_columns.map { |column| "#{column} = VALUES(#{column})" }.join(', ')
102
40
  "ON DUPLICATE KEY UPDATE #{assignments}"
@@ -131,16 +69,6 @@ module AwsAdvancedRubyDriverWrapper
131
69
  logger.error("Failed to close MySQL connection: #{e.message}")
132
70
  end
133
71
 
134
- def sql_state(exception)
135
- return nil unless exception.respond_to?(:sql_state)
136
-
137
- exception.sql_state
138
- end
139
-
140
- def network_bound_methods
141
- NETWORK_BOUND_METHODS
142
- end
143
-
144
72
  def prepare_connect_config(host_info, config)
145
73
  cfg = {}
146
74
  config.each { |k, v| cfg[k] = v }
@@ -153,9 +81,9 @@ module AwsAdvancedRubyDriverWrapper
153
81
  :username
154
82
  end
155
83
 
84
+ # Read, write and connect timeouts (in seconds) make a query or a close on a dead socket raise a
85
+ # timeout error rather than hang. With mysql2 they also keep it from segfaulting.
156
86
  def apply_monitoring_defaults(driver_props)
157
- # mysql2 read_timeout / write_timeout (in seconds) ensure that queries and closes
158
- # on a dead socket raise Mysql2::Error::TimeoutError instead of segfaulting.
159
87
  driver_props[:read_timeout] ||= DEFAULT_MONITORING_TIMEOUT_SEC
160
88
  driver_props[:write_timeout] ||= DEFAULT_MONITORING_TIMEOUT_SEC
161
89
  driver_props[:connect_timeout] ||= DEFAULT_MONITORING_TIMEOUT_SEC
@@ -85,6 +85,14 @@ module AwsAdvancedRubyDriverWrapper
85
85
  RubyMethod::CONNECTION_LO_TRUNCATE.name
86
86
  ]).freeze
87
87
 
88
+ def name
89
+ 'pg'
90
+ end
91
+
92
+ def database_engine
93
+ :postgresql
94
+ end
95
+
88
96
  def connect(host_info, config)
89
97
  ::PG::Connection.new(**prepare_connect_config(host_info, config))
90
98
  end
@@ -162,6 +170,16 @@ module AwsAdvancedRubyDriverWrapper
162
170
  logger.error("Failed to close PostgreSQL connection: #{e.message}")
163
171
  end
164
172
 
173
+ # Points the socket at /dev/null, so the Terminate message libpq sends when the connection
174
+ # is finished or collected never reaches the server.
175
+ def abandon_connection(connection)
176
+ return if connection.nil? || connection.finished?
177
+
178
+ connection.socket_io.reopen(IO::NULL)
179
+ rescue StandardError => e
180
+ logger.debug("Failed to abandon PostgreSQL connection: #{e.message}")
181
+ end
182
+
165
183
  def sql_state(exception)
166
184
  return nil unless exception.is_a?(::PG::Error) && exception.result
167
185
 
@@ -172,6 +190,16 @@ module AwsAdvancedRubyDriverWrapper
172
190
  NETWORK_BOUND_METHODS
173
191
  end
174
192
 
193
+ # libpq separates the socket target (+hostaddr+) from the TLS/IAM server name (+host+), so a pg
194
+ # connection can be pinned to a specific instance IP while still verifying the certificate
195
+ # against - and generating an IAM token for - the original hostname. Blue/Green substitute
196
+ # routing uses this so a pinned connection reaches the intended instance regardless of what DNS
197
+ # resolves to mid-switchover, and still passes +verify-full+ whatever the CA source - instead of
198
+ # connecting to a raw IP (certificate-name mismatch) or falling back to the hostname (DNS risk).
199
+ def props_pinned_to_ip(driver_props, ip)
200
+ driver_props.dup.tap { |props| props[:hostaddr] = ip }
201
+ end
202
+
175
203
  def prepare_connect_config(host_info, config)
176
204
  cfg = {}
177
205
  config.each { |k, v| cfg[k] = v }