aws_advanced_ruby_driver_wrapper 1.0.1 → 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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -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_postgresql_adapter.rb +17 -3
  7. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_trilogy_adapter.rb +80 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/type_adapter_alias.rb +1 -1
  9. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +43 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +11 -4
  11. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql2_driver_dialect.rb +129 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +9 -91
  13. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +18 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/trilogy_driver_dialect.rb +118 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +16 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +10 -5
  17. data/lib/aws_advanced_ruby_driver_wrapper/errors/trilogy_error_handler.rb +127 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +1 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +1 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +27 -9
  21. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +115 -7
  22. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/drop_blue_connections_marker.rb +84 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +3 -1
  24. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +7 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +68 -41
  26. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +4 -2
  27. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +5 -3
  28. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +7 -1
  29. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +1 -1
  30. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +6 -13
  31. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +90 -3
  32. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +11 -1
  33. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +4 -1
  34. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +14 -3
  35. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +68 -3
  36. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/base_saml_auth_plugin.rb +266 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/federated_auth_plugin.rb +134 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/plugins/federated_auth/okta_auth_plugin.rb +113 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +30 -9
  40. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +16 -2
  41. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +4 -1
  42. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +6 -3
  43. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +13 -4
  44. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +29 -6
  45. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_mysql.sql +108 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql/trigger_pg.sql +54 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +14 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +41 -23
  49. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +63 -1
  50. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +3 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +20 -7
  52. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +34 -8
  53. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +1 -1
  54. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +80 -29
  55. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +10 -2
  56. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +4 -1
  57. data/lib/aws_advanced_ruby_driver_wrapper/trilogy.rb +167 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +1 -1
  59. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +25 -15
  60. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +2 -2
  61. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +13 -4
  62. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +15 -31
  63. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +55 -76
  64. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_text.rb +215 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +11 -20
  66. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +2 -3
  67. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +21 -14
  68. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +2 -1
  69. data/lib/aws_advanced_ruby_driver_wrapper/utils/saml_utils.rb +133 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +9 -5
  71. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/null_telemetry.rb +89 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_context.rb +124 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_counter.rb +39 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_factory.rb +122 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/open_telemetry_gauge.rb +36 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_const.rb +50 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/utils/telemetry/telemetry_factory.rb +143 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +9 -2
  79. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +1 -1
  80. data/lib/aws_advanced_ruby_driver_wrapper.rb +9 -2
  81. metadata +54 -2
@@ -14,6 +14,8 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative '../utils/telemetry/null_telemetry'
18
+
17
19
  module AwsAdvancedRubyDriverWrapper
18
20
  module Services
19
21
  ServiceContainer = Struct.new(
@@ -24,7 +26,13 @@ module AwsAdvancedRubyDriverWrapper
24
26
  :plugin_manager,
25
27
  :session_state_service,
26
28
  :storage_service,
27
- :monitor_service
28
- )
29
+ :monitor_service,
30
+ :telemetry_factory
31
+ ) do
32
+ # A container that was given no telemetry factory records no telemetry.
33
+ def telemetry_factory
34
+ self[:telemetry_factory] || Utils::Telemetry::NullTelemetryFactory::INSTANCE
35
+ end
36
+ end
29
37
  end
30
38
  end
@@ -23,6 +23,7 @@ require_relative 'monitor_service'
23
23
  require_relative 'session_state_service'
24
24
  require_relative '../utils/storage/storage_service'
25
25
  require_relative '../utils/events/batching_event_publisher'
26
+ require_relative '../utils/telemetry/telemetry_factory'
26
27
 
27
28
  module AwsAdvancedRubyDriverWrapper
28
29
  module Services
@@ -50,9 +51,10 @@ module AwsAdvancedRubyDriverWrapper
50
51
  end
51
52
 
52
53
  module ServiceUtility
53
- def self.create_standard_container(config)
54
+ def self.create_standard_container(config, telemetry_factory: nil)
54
55
  container = ServiceContainer.new
55
56
  container.event_publisher = CoreServices.event_publisher
57
+ container.telemetry_factory = telemetry_factory || Utils::Telemetry::TelemetryFactory.new(config.wrapper_props)
56
58
  container.connection_service = ConnectionService.new(container, config)
57
59
  container.dialect_service = DialectService.new(container.connection_service, config.driver_name)
58
60
  container.host_service = HostService.new(container)
@@ -71,6 +73,7 @@ module AwsAdvancedRubyDriverWrapper
71
73
  container.event_publisher = parent_container.event_publisher
72
74
  container.dialect_service = parent_container.dialect_service
73
75
  container.host_service = parent_container.host_service
76
+ container.telemetry_factory = parent_container.telemetry_factory
74
77
  container
75
78
  end
76
79
  end
@@ -0,0 +1,167 @@
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 '../aws_advanced_ruby_driver_wrapper'
18
+ require_relative 'utils/connection_config_parser'
19
+ require_relative 'utils/sql_encoding'
20
+ require_relative 'services/service_utility'
21
+ require_relative 'ruby_method'
22
+ require_relative 'errors'
23
+
24
+ module AwsAdvancedRubyDriverWrapper
25
+ # A drop-in for a Trilogy connection. Trilogy has no prepared statements and no asynchronous or
26
+ # streaming reads, and it reads a result off the wire in full before returning it, so the only calls
27
+ # that reach the server are made on the connection itself, and results are handed back unwrapped.
28
+ class WrapperTrilogyClient
29
+ # Trilogy.new takes its options as a single hash, so this does too, and keyword arguments as well.
30
+ def self.new(options = {}, **)
31
+ instance = allocate
32
+ instance.send(:initialize, options, **)
33
+ instance
34
+ end
35
+
36
+ def initialize(options = {}, **)
37
+ ensure_trilogy!
38
+ config = Utils::ConnectionConfigParser.parse(:trilogy, **options, **)
39
+ @service_container = Services::ServiceUtility.create_standard_container(config)
40
+ @service_container.host_service.refresh_host_list
41
+ @query_conn = nil
42
+ @last_sql = nil
43
+ conn_service = @service_container.connection_service
44
+ @service_container.plugin_manager.connect(conn_service.initial_host_info, conn_service.driver_props, true)
45
+ end
46
+
47
+ # Explicitly define critical methods (bypass method_missing to avoid method_missing overhead)
48
+
49
+ # The connection a statement was sent on is remembered, and so is its SQL, because the further
50
+ # result sets a statement leaves are read by calls of their own (next_result, abandon_results!),
51
+ # which carry neither and can only be made on that connection.
52
+ #
53
+ # What is remembered is the copy of the SQL that plugins inspect. The driver is sent the SQL as
54
+ # the application wrote it.
55
+ def query(sql)
56
+ ensure_connected!
57
+ inspected = Utils::SqlEncoding.inspectable(sql)
58
+ result = pm.execute(RubyMethod::CONNECTION_QUERY, current_conn, ->(*a) { current_conn.query(*a) }, sql,
59
+ sql: inspected)
60
+ @query_conn = current_conn
61
+ @last_sql = inspected
62
+ result
63
+ end
64
+
65
+ def ping
66
+ ensure_connected!
67
+ pm.execute(RubyMethod::CONNECTION_PING, current_conn, -> { current_conn.ping })
68
+ end
69
+
70
+ def close
71
+ return if current_conn.nil?
72
+
73
+ pm.execute(RubyMethod::CONNECTION_CLOSE, current_conn, -> { current_conn.close })
74
+ end
75
+
76
+ def next_result
77
+ pm.execute(RubyMethod::CONNECTION_NEXT_RESULT, current_conn, -> { current_conn.next_result },
78
+ bounded_conn: @query_conn, sql: @last_sql)
79
+ end
80
+
81
+ # Trilogy's own query_with_flags calls query on the driver connection directly, so it is defined
82
+ # here to send the statement through the plugin pipeline instead.
83
+ def query_with_flags(sql, flags)
84
+ ensure_connected!
85
+ old_flags = current_conn.query_flags
86
+ current_conn.query_flags = flags
87
+ query(sql)
88
+ ensure
89
+ current_conn.query_flags = old_flags if old_flags
90
+ end
91
+
92
+ # -- method_missing: covers non-network calls and rarely used network calls --
93
+
94
+ # The network calls that are rare enough not to be worth a method of their own. They are entered
95
+ # into the pipeline under the name the pipeline knows them by rather than as a bare string, so
96
+ # that the connection each is bound to is checked.
97
+ DYNAMIC_METHODS = {
98
+ abandon_results!: RubyMethod::CONNECTION_ABANDON_RESULTS,
99
+ change_db: RubyMethod::CONNECTION_CHANGE_DB,
100
+ set_server_option: RubyMethod::CONNECTION_SET_SERVER_OPTION
101
+ }.freeze
102
+
103
+ # Draining what is left of a statement can only be done on the connection it was sent on.
104
+ BOUNDED_TO_QUERY = Set[:abandon_results!].freeze
105
+
106
+ def method_missing(method_name, *, **, &)
107
+ conn = ensure_connected!
108
+ raise NoMethodError, "undefined method `#{method_name}' for #{self.class}" unless conn.respond_to?(method_name)
109
+
110
+ method_key = "connection.#{method_name}"
111
+ return conn.send(method_name, *, **, &) unless network_bound_methods.include?(method_key)
112
+
113
+ bounded_conn = BOUNDED_TO_QUERY.include?(method_name) ? @query_conn : nil
114
+ pm.execute(
115
+ DYNAMIC_METHODS[method_name] || method_key, current_conn,
116
+ ->(*a, **opts, &b) { current_conn.send(method_name, *a, **opts, &b) },
117
+ *, **, bounded_conn: bounded_conn, &
118
+ )
119
+ end
120
+
121
+ def respond_to_missing?(method, include_private = false)
122
+ current_conn.respond_to?(method, include_private) || super
123
+ end
124
+
125
+ # A concise representation that never exposes the connection config (which carries
126
+ # credentials) or the cached SQL text this instance holds. Defined so a default
127
+ # dump - via logging, interpolation, `pp`, or a backtrace - cannot leak either.
128
+ def inspect
129
+ format('#<%<class>s:0x%<addr>016x>', class: self.class.name, addr: object_id << 1)
130
+ end
131
+ alias to_s inspect
132
+
133
+ def pretty_print(pp)
134
+ pp.text(inspect)
135
+ end
136
+
137
+ private
138
+
139
+ def ensure_trilogy!
140
+ require 'trilogy'
141
+ rescue LoadError
142
+ raise LoadError, "WrapperTrilogyClient requires 'trilogy'. Add it to your Gemfile: gem 'trilogy'"
143
+ end
144
+
145
+ def current_conn
146
+ @service_container.connection_service.current_connection
147
+ end
148
+
149
+ # Guards the entry points that reach for the current connection directly (the query paths and
150
+ # method_missing). Without a live connection AR expects the driver's own "not connected" error,
151
+ # which it translates to a connection error; a bare NoMethodError on nil would not be recognized.
152
+ def ensure_connected!
153
+ conn = current_conn
154
+ raise Trilogy::BaseConnectionError, 'MySQL client is not connected' if conn.nil?
155
+
156
+ conn
157
+ end
158
+
159
+ def pm
160
+ @service_container.plugin_manager
161
+ end
162
+
163
+ def network_bound_methods
164
+ @network_bound_methods ||= @service_container.dialect_service.driver_dialect.network_bound_methods
165
+ end
166
+ end
167
+ end
@@ -49,7 +49,7 @@ module AwsAdvancedRubyDriverWrapper
49
49
  end
50
50
 
51
51
  def cluster_id
52
- PropertyDefinition::CLUSTER_ID.get(wrapper_props)
52
+ PropertyDefinition::CLUSTER_ID.get_string(wrapper_props)
53
53
  end
54
54
 
55
55
  # Redact sensitive property values (passwords, IAM tokens, Secrets Manager
@@ -32,9 +32,10 @@ module AwsAdvancedRubyDriverWrapper
32
32
  module_function
33
33
 
34
34
  # Main entry point. The driver_name is provided by the wrapper class
35
- # (e.g. :postgresql from WrapperPgConnection, :mysql2 from WrapperMysql2Client).
35
+ # (e.g. :postgresql from WrapperPgConnection, :mysql2 from WrapperMysql2Client, :trilogy from
36
+ # WrapperTrilogyClient).
36
37
  #
37
- # @param driver_name [Symbol] :postgresql or :mysql2
38
+ # @param driver_name [Symbol] :postgresql, :mysql2 or :trilogy
38
39
  def parse(driver_name, *args, **kwargs)
39
40
  if driver_name == :mysql2 && !args.empty? && !args.first.is_a?(Hash)
40
41
  raise ArgumentError,
@@ -192,7 +193,9 @@ module AwsAdvancedRubyDriverWrapper
192
193
  # Validates that values in a config hash match the expected type of their corresponding WrapperProperty.
193
194
  # Raises TypeError if a value is present but incompatible. Strings that represent valid integers
194
195
  # are accepted for Integer-typed properties. Float-typed properties also accept Integers and
195
- # strings that represent a valid integer or decimal number.
196
+ # strings that represent a valid integer or decimal number. String-typed properties also accept
197
+ # numbers and Symbol-typed properties also accept strings, since YAML (e.g. database.yml) parses
198
+ # unquoted values like `cluster_id: 1` as numbers and never produces symbols.
196
199
  def validate_props!(config)
197
200
  config.each do |key, value|
198
201
  prop = PropertyDefinition::KNOWN_PROPERTIES[key]
@@ -200,23 +203,30 @@ module AwsAdvancedRubyDriverWrapper
200
203
  next if value.nil?
201
204
  next if prop.type.nil?
202
205
 
203
- valid = if prop.type == :boolean
204
- value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
205
- (value.is_a?(String) && BOOLEAN_STRINGS.include?(value.downcase))
206
- elsif prop.type == Float
207
- value.is_a?(Float) || value.is_a?(Integer) ||
208
- (value.is_a?(String) && value.match?(/\A-?\d+(\.\d+)?\z/))
209
- else
210
- value.is_a?(prop.type) ||
211
- (prop.type == Integer && value.is_a?(String) && value.match?(/\A-?\d+\z/))
212
- end
213
-
214
- raise TypeError, "#{key}: expected #{prop.type}, got #{value.class}" unless valid
206
+ raise TypeError, "#{key}: expected #{prop.type}, got #{value.class}" unless valid_type?(prop.type, value)
215
207
 
216
208
  prop.validate!(value)
217
209
  end
218
210
  end
219
211
 
212
+ def valid_type?(type, value)
213
+ if type == :boolean
214
+ value.is_a?(TrueClass) || value.is_a?(FalseClass) ||
215
+ (value.is_a?(String) && BOOLEAN_STRINGS.include?(value.downcase))
216
+ elsif type == Float
217
+ value.is_a?(Float) || value.is_a?(Integer) ||
218
+ (value.is_a?(String) && value.match?(/\A-?\d+(\.\d+)?\z/))
219
+ elsif type == Integer
220
+ value.is_a?(Integer) || (value.is_a?(String) && value.match?(/\A-?\d+\z/))
221
+ elsif type == String
222
+ value.is_a?(String) || value.is_a?(Numeric)
223
+ elsif type == Symbol
224
+ value.is_a?(Symbol) || value.is_a?(String)
225
+ else
226
+ value.is_a?(type)
227
+ end
228
+ end
229
+
220
230
  # Forms a HostInfo object from a URI host section string.
221
231
  # Each host entry may or may not have a port; missing ports are represented as -1.
222
232
  # The resulting port is a comma-delimited string of per-host ports.
@@ -35,9 +35,9 @@ module AwsAdvancedRubyDriverWrapper
35
35
  def query_global_topology(conn, initial_host_info, instance_templates_by_region)
36
36
  results = @dialect.execute(conn, @dialect.topology_query)
37
37
 
38
- if results.fields.empty?
38
+ if topology_result_empty?(results)
39
39
  # We expect at least 4 columns. Note that the server may return 0 columns if failover has occurred.
40
- logger.debug('The topology query returned a result with 0 columns. ' \
40
+ logger.debug('The topology query returned a result with no rows or rows with no columns. ' \
41
41
  'This may occur if the topology query is executed when the server is failing over.')
42
42
  return nil
43
43
  end
@@ -19,10 +19,14 @@ require_relative 'rds_utils'
19
19
  require_relative 'rds_url_type'
20
20
  require_relative '../property_definition'
21
21
  require_relative '../logging'
22
+ require_relative 'telemetry/null_telemetry'
23
+ require_relative 'telemetry/telemetry_const'
22
24
 
23
25
  module AwsAdvancedRubyDriverWrapper
24
26
  module Utils
25
27
  module IamAuthUtils
28
+ TELEMETRY_FETCH_TOKEN = 'fetch authentication token'
29
+
26
30
  module_function
27
31
 
28
32
  TokenEntry = Data.define(:token, :expires_at) do
@@ -83,10 +87,15 @@ module AwsAdvancedRubyDriverWrapper
83
87
  arn&.match(/\Aarn:aws(?:-[a-z]+)*:rds:(?<region>[^:]+)/)&.[](:region)
84
88
  end
85
89
 
86
- def generate_token(region:, hostname:, port:, user:, credentials_provider:)
87
- Aws::RDS::AuthTokenGenerator
88
- .new(credentials: credentials_provider)
89
- .auth_token(region:, endpoint: "#{hostname}:#{port}", user_name: user)
90
+ # @param telemetry_factory [Telemetry::TelemetryFactory, Telemetry::NullTelemetryFactory] records
91
+ # the token generation as a span
92
+ def generate_token(region:, hostname:, port:, user:, credentials_provider:,
93
+ telemetry_factory: Telemetry::NullTelemetryFactory::INSTANCE)
94
+ telemetry_factory.in_context(TELEMETRY_FETCH_TOKEN, Telemetry::TraceLevel::NESTED, record_failure: true) do
95
+ Aws::RDS::AuthTokenGenerator
96
+ .new(credentials: credentials_provider)
97
+ .auth_token(region:, endpoint: "#{hostname}:#{port}", user_name: user)
98
+ end
90
99
  end
91
100
 
92
101
  def resolve_host(iam_host, host_info)
@@ -14,6 +14,8 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
+ require_relative 'mysql_text'
18
+
17
19
  module AwsAdvancedRubyDriverWrapper
18
20
  module Utils
19
21
  module Parser
@@ -21,21 +23,23 @@ module AwsAdvancedRubyDriverWrapper
21
23
  # Both placeholder styles are accepted, so that the same annotation works for mysql2's
22
24
  # positional +?+ and pg's numbered +$1+.
23
25
  ANNOTATION_PATTERN = %r{/\*@encrypt:([\w.]+)\*/\s*(?:\?|\$(\d+))}
24
- STRIP_PATTERN = %r{/\*@encrypt:[\w.]+\*/\s*}
25
- # The quote characters a literal can be wrapped in. A backtick quotes an identifier rather than
26
- # a value, but is included so a +?+ inside a backtick-quoted name is not miscounted either.
27
- QUOTES = ["'", '"', '`'].freeze
26
+ # Whitespace after an annotation is taken with it, but not a line break, which may be what ends
27
+ # a line comment the annotation sits in.
28
+ STRIP_PATTERN = %r{/\*@encrypt:[\w.]+\*/[^\S\n]*}
28
29
 
29
30
  module_function
30
31
 
31
32
  # Returns a 1-based map of parameter index => "table.column" for each
32
33
  # /*@encrypt:table.column*/ ? or /*@encrypt:table.column*/ $n placeholder.
33
34
  # @param sql [String]
35
+ # @param server_version [Integer, nil] the MySQL server's version, which decides whether a
36
+ # versioned +/*!+ comment is part of the statement (see MysqlText.each_segment)
34
37
  # @return [Hash{Integer => String}]
35
- def parse_annotations(sql)
36
- return {} unless sql.is_a?(String) && !sql.empty?
38
+ def parse_annotations(sql, server_version: nil)
39
+ # Most statements carry no annotation, so they are not scanned for placeholders at all.
40
+ return {} unless annotations?(sql)
37
41
 
38
- question_marks = unquoted_question_mark_positions(sql)
42
+ question_marks = placeholder_positions(sql, server_version)
39
43
 
40
44
  sql.to_enum(:scan, ANNOTATION_PATTERN)
41
45
  .each_with_object({}) do |_, result|
@@ -66,31 +70,11 @@ module AwsAdvancedRubyDriverWrapper
66
70
  end
67
71
 
68
72
  # The character positions of every +?+ that is a bind placeholder, skipping any inside a quoted
69
- # literal so a literal question mark does not shift the count of the placeholder an annotation
70
- # points at. A doubled quote reads as one closing and one opening quote, and a backslash escapes
71
- # the next character except inside a backtick, which matches how the drivers read a literal.
72
- def unquoted_question_mark_positions(sql)
73
+ # literal or a comment so that it does not shift the count of the placeholder an annotation
74
+ # points at. Only mysql2 binds with +?+, so the text is read by MySQL's rules.
75
+ def placeholder_positions(sql, server_version)
73
76
  positions = []
74
- quote = nil
75
- index = 0
76
-
77
- while index < sql.length
78
- char = sql[index]
79
- if quote
80
- if char == '\\' && quote != '`'
81
- index += 2
82
- next
83
- end
84
-
85
- quote = nil if char == quote
86
- elsif QUOTES.include?(char)
87
- quote = char
88
- elsif char == '?'
89
- positions << index
90
- end
91
- index += 1
92
- end
93
-
77
+ MysqlText.each_code_char(sql, server_version: server_version) { |char, index| positions << index if char == '?' }
94
78
  positions
95
79
  end
96
80
  end