aws_advanced_ruby_driver_wrapper 1.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +175 -0
- data/NOTICE +1 -0
- data/README.md +168 -0
- data/THIRD-PARTY-LICENSES +473 -0
- data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
- data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
- data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
- data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
- data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
- data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
- data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
- data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
- data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
- data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
- data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
- data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
- data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
- data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
- data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
- metadata +227 -0
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
module AwsAdvancedRubyDriverWrapper
|
|
18
|
+
module Utils
|
|
19
|
+
module Events
|
|
20
|
+
# Event indicating data was accessed. Batched delivery (not immediate).
|
|
21
|
+
DataAccessEvent = Data.define(:data_type, :key) do
|
|
22
|
+
def immediate_delivery? = false
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
module AwsAdvancedRubyDriverWrapper
|
|
18
|
+
module Utils
|
|
19
|
+
module Events
|
|
20
|
+
# Event signaling that monitors for a cluster should be reset. Immediate delivery.
|
|
21
|
+
MonitorResetEvent = Data.define(:cluster_id, :endpoints) do
|
|
22
|
+
def immediate_delivery? = true
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
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 '../host/host_role'
|
|
18
|
+
require_relative 'rds_url_type'
|
|
19
|
+
require_relative 'rds_utils'
|
|
20
|
+
require_relative 'aurora_topology_utils'
|
|
21
|
+
|
|
22
|
+
module AwsAdvancedRubyDriverWrapper
|
|
23
|
+
module Utils
|
|
24
|
+
# Topology utilities specific to Global Aurora database clusters.
|
|
25
|
+
# Global Aurora clusters span multiple AWS regions, so topology queries return a region column
|
|
26
|
+
# and instance templates are keyed by region.
|
|
27
|
+
class GlobalAuroraTopologyUtils < AuroraTopologyUtils
|
|
28
|
+
# Query the database for topology information across global Aurora cluster regions.
|
|
29
|
+
#
|
|
30
|
+
# @param conn [Object] the connection to use to query the database.
|
|
31
|
+
# @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the HostInfo used to initially connect.
|
|
32
|
+
# @param instance_templates_by_region [Hash<String, AwsAdvancedRubyDriverWrapper::Host::HostInfo>]
|
|
33
|
+
# a map of AWS region to instance template HostInfo for constructing hosts.
|
|
34
|
+
# @return [Array<AwsAdvancedRubyDriverWrapper::Host::HostInfo>, nil] a list of HostInfo objects or nil.
|
|
35
|
+
def query_global_topology(conn, initial_host_info, instance_templates_by_region)
|
|
36
|
+
results = @dialect.execute(conn, @dialect.topology_query)
|
|
37
|
+
|
|
38
|
+
if results.fields.empty?
|
|
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. ' \
|
|
41
|
+
'This may occur if the topology query is executed when the server is failing over.')
|
|
42
|
+
return nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
verify_writer(build_global_hosts(results, initial_host_info, instance_templates_by_region))
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
raise "Invalid topology query: #{e.message}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Retrieves the AWS region for a given instance ID.
|
|
51
|
+
#
|
|
52
|
+
# @param instance_id [String] the database instance identifier.
|
|
53
|
+
# @param conn [Object] the database connection.
|
|
54
|
+
# @return [String, nil] the AWS region, or nil if it cannot be determined.
|
|
55
|
+
def query_region(instance_id, conn)
|
|
56
|
+
results = @dialect.execute_with_params(conn, @dialect.region_by_instance_id_query, [instance_id])
|
|
57
|
+
return nil if results.nil? || results.none?
|
|
58
|
+
|
|
59
|
+
row = results.first
|
|
60
|
+
region = row.values.first
|
|
61
|
+
region.nil? || region.to_s.empty? ? nil : region.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Parses a comma-separated instance templates string into a region-to-HostInfo map.
|
|
65
|
+
# Each comma-separated entry in the string can be one of the following formats:
|
|
66
|
+
# - "host_pattern"
|
|
67
|
+
# - "host_pattern:port"
|
|
68
|
+
# - "[region]host_pattern"
|
|
69
|
+
# - "[region]host_pattern:port"
|
|
70
|
+
#
|
|
71
|
+
# @param instance_templates_string [String] comma-separated list of instance template entries.
|
|
72
|
+
# @param host_validator [Proc] proc to validate each host pattern.
|
|
73
|
+
# @return [Hash<String, AwsAdvancedRubyDriverWrapper::Host::HostInfo>] map of region to instance template.
|
|
74
|
+
# @raise [AwsError] if the host or region in any of the template strings could not be parsed.
|
|
75
|
+
def parse_instance_templates(instance_templates_string, host_validator)
|
|
76
|
+
templates = {}
|
|
77
|
+
|
|
78
|
+
instance_templates_string.split(',').each do |entry|
|
|
79
|
+
entry = entry.strip
|
|
80
|
+
region, host_pattern, port = extract_region_host_and_port(entry)
|
|
81
|
+
raise Errors::AwsError, "Unable to parse region from '#{entry}'" if region.nil? || region.empty?
|
|
82
|
+
raise Errors::AwsError, "Unable to parse host from '#{entry}'" if host_pattern.nil? || host_pattern.empty?
|
|
83
|
+
|
|
84
|
+
# The region may be supplied explicitly in square brackets (e.g. "[us-east-1]..."), validate it.
|
|
85
|
+
unless RdsUtils.valid_region?(region)
|
|
86
|
+
raise Errors::AwsError, "Unknown or misspelled AWS region '#{region}' in instance template '#{entry}'"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
host_validator.call(host_pattern)
|
|
90
|
+
url_type = RdsUtils.identify_rds_type(host_pattern)
|
|
91
|
+
# assign HostRole of READER if using the reader cluster URL, otherwise assume a HostRole of WRITER
|
|
92
|
+
role = url_type == RdsUrlType::RDS_READER_CLUSTER ? Host::HostRole::READER : Host::HostRole::WRITER
|
|
93
|
+
templates[region.downcase] = Host::HostInfo.new(id: '?', host: host_pattern, port:, role:)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
logger.debug("Detected global database patterns: #{templates}")
|
|
97
|
+
templates
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Process global Aurora topology query results into a list of HostInfo objects.
|
|
103
|
+
# Each row in 'results' includes a region column used to select the appropriate instance template.
|
|
104
|
+
#
|
|
105
|
+
# @param results [Object] the query result set (enumerable of row hashes).
|
|
106
|
+
# @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the initial host info.
|
|
107
|
+
# @param instance_templates_by_region [Hash<String, AwsAdvancedRubyDriverWrapper::Host::HostInfo>]
|
|
108
|
+
# map of region to instance template.
|
|
109
|
+
# @return [Array<AwsAdvancedRubyDriverWrapper::Host::HostInfo>, nil] the parsed hosts or nil on failure.
|
|
110
|
+
def build_global_hosts(results, initial_host_info, instance_templates_by_region)
|
|
111
|
+
hosts_map = {}
|
|
112
|
+
|
|
113
|
+
results.each do |row|
|
|
114
|
+
host = build_host_from_row(row, initial_host_info, instance_templates_by_region)
|
|
115
|
+
hosts_map[host.host] = host
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
logger.debug("Error processing topology query results: #{e.message}")
|
|
118
|
+
return nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
hosts_map.values
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Builds a HostInfo from a single global Aurora topology query result row.
|
|
125
|
+
#
|
|
126
|
+
# Expected columns: host_id, is_writer, instance_lag, aws_region.
|
|
127
|
+
#
|
|
128
|
+
# @param row [Hash] a single row from the topology query result.
|
|
129
|
+
# @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the initial host info.
|
|
130
|
+
# @param instance_templates_by_region [Hash<String, AwsAdvancedRubyDriverWrapper::Host::HostInfo>]
|
|
131
|
+
# map of region to instance template.
|
|
132
|
+
# @return [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the constructed host info.
|
|
133
|
+
# @raise [AwsError] if no template is found for the row's region.
|
|
134
|
+
def build_host_from_row(row, initial_host_info, instance_templates_by_region)
|
|
135
|
+
instance_id = row_value(row, 'instance_id')
|
|
136
|
+
is_writer = to_boolean(row_value(row, 'is_writer'))
|
|
137
|
+
lag = to_float(row_value(row, 'instance_lag'))
|
|
138
|
+
aws_region = row_value(row, 'aws_region').to_s.downcase
|
|
139
|
+
|
|
140
|
+
weight = (lag.round * 100)
|
|
141
|
+
|
|
142
|
+
instance_template = instance_templates_by_region[aws_region]
|
|
143
|
+
raise Errors::AwsError, "Cannot find instance template for region '#{aws_region}'" if instance_template.nil?
|
|
144
|
+
|
|
145
|
+
build_host(instance_id, instance_id, is_writer, weight, Time.now, initial_host_info, instance_template)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Extracts the region, host pattern, and port from an instance template string.
|
|
149
|
+
# The region may be prefixed in square brackets or inferred from the host pattern, for example:
|
|
150
|
+
# - "[us-west-1]?.custom-host:5432"
|
|
151
|
+
# - "?.xyz.us-west-1.rds.amazonaws.com:5432"
|
|
152
|
+
#
|
|
153
|
+
# @param entry [String] a single instance template entry.
|
|
154
|
+
# @return [Array(String, String, Integer)] region, host_pattern, and port.
|
|
155
|
+
def extract_region_host_and_port(entry)
|
|
156
|
+
if entry.start_with?('[')
|
|
157
|
+
closing = entry.index(']')
|
|
158
|
+
raise ArgumentError, "Invalid instance template format: '#{entry}'" if closing.nil?
|
|
159
|
+
|
|
160
|
+
region = entry[1...closing]
|
|
161
|
+
host_pattern, port = parse_host_and_port(entry[(closing + 1)..])
|
|
162
|
+
else
|
|
163
|
+
host_pattern, port = parse_host_and_port(entry)
|
|
164
|
+
region = RdsUtils.rds_region(host_pattern)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
[region, host_pattern, port]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Parses "host_pattern" or "host_pattern:port" and returns [host_pattern, port].
|
|
171
|
+
def parse_host_and_port(value)
|
|
172
|
+
# Split from the right to handle host patterns that might not contain a colon.
|
|
173
|
+
# Only treat the last segment as a port if it's purely numeric.
|
|
174
|
+
last_colon = value.rindex(':')
|
|
175
|
+
if last_colon && value[(last_colon + 1)..].match?(/\A\d+\z/)
|
|
176
|
+
host_pattern = value[0...last_colon]
|
|
177
|
+
port = value[(last_colon + 1)..].to_i
|
|
178
|
+
[host_pattern, port]
|
|
179
|
+
else
|
|
180
|
+
[value, Host::HostInfo::NO_PORT]
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
module AwsAdvancedRubyDriverWrapper
|
|
18
|
+
module Utils
|
|
19
|
+
module HostListUtils
|
|
20
|
+
def self.writer(hosts) = hosts&.find { |host_info| host_info.role == HostRole::WRITER }
|
|
21
|
+
|
|
22
|
+
def self.contains_url?(hosts, url) = hosts&.any? { |host_info| host_info.url == url } || false
|
|
23
|
+
|
|
24
|
+
def self.to_host_urls_s(hosts) = hosts&.map(&:url)&.join(',') || ''
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
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 'uri'
|
|
18
|
+
require_relative 'rds_utils'
|
|
19
|
+
require_relative 'rds_url_type'
|
|
20
|
+
require_relative '../property_definition'
|
|
21
|
+
require_relative '../logging'
|
|
22
|
+
|
|
23
|
+
module AwsAdvancedRubyDriverWrapper
|
|
24
|
+
module Utils
|
|
25
|
+
module IamAuthUtils
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
TokenEntry = Data.define(:token, :expires_at) do
|
|
29
|
+
# Redact the token so it is never exposed if an instance is logged,
|
|
30
|
+
# interpolated, or rendered in a backtrace.
|
|
31
|
+
def inspect
|
|
32
|
+
"#<data TokenEntry token=#{AwsAdvancedRubyDriverWrapper::REDACTED.inspect}, " \
|
|
33
|
+
"expires_at=#{expires_at.inspect}>"
|
|
34
|
+
end
|
|
35
|
+
alias_method :to_s, :inspect
|
|
36
|
+
|
|
37
|
+
# `pp` / PrettyPrint does not call #inspect; route them through the
|
|
38
|
+
# redacted representation so `pp entry` cannot leak the token.
|
|
39
|
+
def pretty_print(pp)
|
|
40
|
+
pp.text(inspect)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
EXPIRY_BUFFER_SEC = 60
|
|
45
|
+
|
|
46
|
+
def parse_token_expiry(token)
|
|
47
|
+
URI.decode_www_form(URI.parse("https://#{token}").query)
|
|
48
|
+
.filter_map { |(key, value)| Integer(value, 10) if key.downcase == 'x-amz-expires' }
|
|
49
|
+
.first
|
|
50
|
+
rescue StandardError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def build_token_entry(token, fallback_expiry_sec)
|
|
55
|
+
expires_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) +
|
|
56
|
+
(parse_token_expiry(token) || fallback_expiry_sec) -
|
|
57
|
+
EXPIRY_BUFFER_SEC
|
|
58
|
+
TokenEntry.new(token:, expires_at:)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def valid_entry?(entry)
|
|
62
|
+
entry.is_a?(TokenEntry) && Process.clock_gettime(Process::CLOCK_MONOTONIC) < entry.expires_at
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def region_for(host:, props:, rds_type:, credentials_provider:, rds_client_func: nil)
|
|
66
|
+
explicit = PropertyDefinition::IAM_REGION.get(props)
|
|
67
|
+
return explicit if explicit && !explicit.empty?
|
|
68
|
+
|
|
69
|
+
return RdsUtils.rds_region(host) unless rds_type == RdsUrlType::RDS_GLOBAL_WRITER_CLUSTER
|
|
70
|
+
|
|
71
|
+
region_from_global_cluster(host, credentials_provider, rds_client: rds_client_func&.call)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def region_from_global_cluster(host, credentials_provider, rds_client: nil)
|
|
75
|
+
cluster_id = RdsUtils.rds_cluster_id(host)
|
|
76
|
+
client = rds_client || Aws::RDS::Client.new(credentials: credentials_provider)
|
|
77
|
+
arn = client.describe_global_clusters(global_cluster_identifier: cluster_id)
|
|
78
|
+
.global_clusters
|
|
79
|
+
.flat_map(&:global_cluster_members)
|
|
80
|
+
.find(&:is_writer)
|
|
81
|
+
&.db_cluster_arn
|
|
82
|
+
# The partition segment of an ARN varies across AWS partitions, so match any of them.
|
|
83
|
+
arn&.match(/\Aarn:aws(?:-[a-z]+)*:rds:(?<region>[^:]+)/)&.[](:region)
|
|
84
|
+
end
|
|
85
|
+
|
|
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
|
+
end
|
|
91
|
+
|
|
92
|
+
def resolve_host(iam_host, host_info)
|
|
93
|
+
return iam_host if iam_host && !iam_host.empty?
|
|
94
|
+
|
|
95
|
+
host_info.host
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def resolve_port(iam_default_port, host_info, dialect_default_port)
|
|
99
|
+
iam_port = iam_default_port.to_i
|
|
100
|
+
return iam_port if iam_port.positive?
|
|
101
|
+
|
|
102
|
+
return host_info.port.to_i if host_info.port_specified?
|
|
103
|
+
|
|
104
|
+
dialect_default_port
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def cache_key(region, host, port, user, credentials_identity)
|
|
108
|
+
"#{region}:#{host}:#{port}:#{user}:#{credentials_identity}"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
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 'topology_utils'
|
|
18
|
+
|
|
19
|
+
module AwsAdvancedRubyDriverWrapper
|
|
20
|
+
module Utils
|
|
21
|
+
# Topology utilities specific to Multi-AZ database clusters.
|
|
22
|
+
# In Multi-AZ clusters, the writer is identified via a separate query rather than
|
|
23
|
+
# from a column in the topology results.
|
|
24
|
+
class MultiAzTopologyUtils
|
|
25
|
+
include TopologyUtils
|
|
26
|
+
|
|
27
|
+
def initialize(dialect:)
|
|
28
|
+
raise ArgumentError, 'dialect cannot be nil' if dialect.nil?
|
|
29
|
+
|
|
30
|
+
@dialect = dialect
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Evaluate whether the given connection is to a writer instance.
|
|
34
|
+
# In Multi-AZ, when connected to a writer, the writer ID query returns no rows.
|
|
35
|
+
#
|
|
36
|
+
# @param conn [Object] the connection to evaluate.
|
|
37
|
+
# @return [Boolean] true if the connection is to a writer instance, false otherwise.
|
|
38
|
+
def writer_instance?(conn)
|
|
39
|
+
results = @dialect.execute(conn, @dialect.writer_id_query)
|
|
40
|
+
# When connected to a writer, the result is empty; otherwise it contains a single row.
|
|
41
|
+
results.nil? || results.none?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Process Multi-AZ topology query results into a list of HostInfo objects.
|
|
45
|
+
# First determines the writer ID, then parses each row using endpoint and id columns.
|
|
46
|
+
#
|
|
47
|
+
# @param conn [Object] the database connection.
|
|
48
|
+
# @param results [Object] the query result set (enumerable of row hashes).
|
|
49
|
+
# @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the initial host info.
|
|
50
|
+
# @param instance_template [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the template for building hosts.
|
|
51
|
+
# @return [Array<AwsAdvancedRubyDriverWrapper::Host::HostInfo>, nil] the parsed hosts or nil on failure.
|
|
52
|
+
def build_hosts(conn, results, initial_host_info, instance_template)
|
|
53
|
+
hosts_map = {}
|
|
54
|
+
writer_id = query_writer_id(conn)
|
|
55
|
+
|
|
56
|
+
results.each do |row|
|
|
57
|
+
host = build_host_from_row(row, initial_host_info, instance_template, writer_id)
|
|
58
|
+
|
|
59
|
+
# Ensure newer records replace the older ones if there are duplicate keys.
|
|
60
|
+
existing = hosts_map[host.host]
|
|
61
|
+
if existing.nil? || (existing.last_update_time && host.last_update_time &&
|
|
62
|
+
existing.last_update_time < host.last_update_time)
|
|
63
|
+
hosts_map[host.host] = host
|
|
64
|
+
end
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
logger.debug("Error processing topology query results: #{e.message}")
|
|
67
|
+
return nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
hosts_map.values
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
# Determines the writer instance ID by querying the database.
|
|
76
|
+
# The writer ID query returns the writer ID when connected to a reader.
|
|
77
|
+
# When connected to a writer, the query returns no rows, so we fall back to
|
|
78
|
+
# getting the host ID of the current connection.
|
|
79
|
+
#
|
|
80
|
+
# @param conn [Object] the database connection.
|
|
81
|
+
# @return [String, nil] the writer instance ID, or nil if it cannot be determined.
|
|
82
|
+
def query_writer_id(conn)
|
|
83
|
+
results = @dialect.execute(conn, @dialect.writer_id_query)
|
|
84
|
+
|
|
85
|
+
if results&.any?
|
|
86
|
+
row = results.first
|
|
87
|
+
writer_id_column = @dialect.respond_to?(:writer_id_column_name) ? @dialect.writer_id_column_name : 'writer_id'
|
|
88
|
+
writer_id = row_value(row, writer_id_column)
|
|
89
|
+
return writer_id unless writer_id.nil? || writer_id.to_s.empty?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# The writer ID is only returned when connected to a reader.
|
|
93
|
+
# If the query does not return a value, we are connected to the writer.
|
|
94
|
+
instance_id, = @dialect.instance_identity(conn)
|
|
95
|
+
instance_id
|
|
96
|
+
rescue StandardError
|
|
97
|
+
nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Builds a HostInfo from a single Multi-AZ topology query result row.
|
|
101
|
+
# Expected columns: endpoint.
|
|
102
|
+
#
|
|
103
|
+
# @param row [Hash] a single row from the topology query result.
|
|
104
|
+
# @param initial_host_info [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the initial host info.
|
|
105
|
+
# @param instance_template [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the template for building hosts.
|
|
106
|
+
# @param writer_id [String, nil] the writer instance ID.
|
|
107
|
+
# @return [AwsAdvancedRubyDriverWrapper::Host::HostInfo] the constructed host info.
|
|
108
|
+
def build_host_from_row(row, initial_host_info, instance_template, writer_id)
|
|
109
|
+
instance_id = row_value(row, 'instance_id')
|
|
110
|
+
endpoint = row_value(row, 'endpoint')
|
|
111
|
+
# Extract instance name from the endpoint (everything before the first dot).
|
|
112
|
+
instance_name = endpoint&.split('.')&.first
|
|
113
|
+
build_host(instance_id, instance_name, instance_id == writer_id, 0, Time.now, initial_host_info, instance_template)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
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
|
+
module AwsAdvancedRubyDriverWrapper
|
|
18
|
+
module Utils
|
|
19
|
+
module Parser
|
|
20
|
+
module EncryptionAnnotationParser
|
|
21
|
+
# Both placeholder styles are accepted, so that the same annotation works for mysql2's
|
|
22
|
+
# positional +?+ and pg's numbered +$1+.
|
|
23
|
+
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
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
# Returns a 1-based map of parameter index => "table.column" for each
|
|
32
|
+
# /*@encrypt:table.column*/ ? or /*@encrypt:table.column*/ $n placeholder.
|
|
33
|
+
# @param sql [String]
|
|
34
|
+
# @return [Hash{Integer => String}]
|
|
35
|
+
def parse_annotations(sql)
|
|
36
|
+
return {} unless sql.is_a?(String) && !sql.empty?
|
|
37
|
+
|
|
38
|
+
question_marks = unquoted_question_mark_positions(sql)
|
|
39
|
+
|
|
40
|
+
sql.to_enum(:scan, ANNOTATION_PATTERN)
|
|
41
|
+
.each_with_object({}) do |_, result|
|
|
42
|
+
match = Regexp.last_match
|
|
43
|
+
next unless match
|
|
44
|
+
|
|
45
|
+
# A numbered placeholder states its own position; a question mark is located by
|
|
46
|
+
# counting the question marks that precede it.
|
|
47
|
+
if match[2]
|
|
48
|
+
result[match[2].to_i] = match[1]
|
|
49
|
+
else
|
|
50
|
+
param_index = question_marks.index(match.end(0) - 1)
|
|
51
|
+
result[param_index + 1] = match[1] if param_index
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def strip_annotations(sql)
|
|
57
|
+
return sql unless sql.is_a?(String) && !sql.empty?
|
|
58
|
+
|
|
59
|
+
sql.gsub(STRIP_PATTERN, '')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def annotations?(sql)
|
|
63
|
+
return false unless sql.is_a?(String) && !sql.empty?
|
|
64
|
+
|
|
65
|
+
ANNOTATION_PATTERN.match?(sql)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# 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
|
+
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
|
+
|
|
94
|
+
positions
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|