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,502 @@
|
|
|
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 'query_analysis'
|
|
18
|
+
require_relative 'query_type'
|
|
19
|
+
|
|
20
|
+
module AwsAdvancedRubyDriverWrapper
|
|
21
|
+
module Utils
|
|
22
|
+
module Parser
|
|
23
|
+
# Uses the pg_query gem (libpg_query C extension) for accurate AST-based parsing.
|
|
24
|
+
module PgStatementAnalyzer
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Loads pg_query. The gem is not a dependency of the wrapper, so applications that never parse
|
|
28
|
+
# PostgreSQL statements are not made to install it (and compile its C extension).
|
|
29
|
+
#
|
|
30
|
+
# @raise [LoadError] when the pg_query gem is not installed
|
|
31
|
+
def load_parser
|
|
32
|
+
require 'pg_query'
|
|
33
|
+
rescue LoadError
|
|
34
|
+
raise LoadError,
|
|
35
|
+
'pg_query gem is required for PostgreSQL SQL parsing. Add gem "pg_query" to your Gemfile.'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def analyze(sql)
|
|
39
|
+
load_parser
|
|
40
|
+
|
|
41
|
+
return QueryAnalysis.unknown unless sql.is_a?(String) && !sql.strip.empty?
|
|
42
|
+
|
|
43
|
+
begin
|
|
44
|
+
result = ::PgQuery.parse(sql)
|
|
45
|
+
# stmts is a protobuf-generated accessor on the parse tree, defined dynamically at load
|
|
46
|
+
# time, so static analysers cannot resolve it and flag a false "cannot find stmts".
|
|
47
|
+
statements = result.tree.stmts
|
|
48
|
+
stmt = statements.first&.stmt
|
|
49
|
+
return QueryAnalysis.unknown unless stmt
|
|
50
|
+
|
|
51
|
+
stmt_hash = stmt.to_h
|
|
52
|
+
analysis = extract_from_stmt(stmt_hash, parameterized?(stmt_hash))
|
|
53
|
+
statements.length > 1 ? with_trailing_statements(analysis, statements) : analysis
|
|
54
|
+
rescue ::PgQuery::ParseError
|
|
55
|
+
fallback_analysis(sql)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Only the first statement of a multi-statement string is analyzed, so what the rest of them
|
|
60
|
+
# write is unknown. Their tables are still collected, since a caller that has to decide
|
|
61
|
+
# whether the string touches an encrypted column needs to know about them.
|
|
62
|
+
# @return [QueryAnalysis]
|
|
63
|
+
def with_trailing_statements(analysis, statements)
|
|
64
|
+
tables = statements.flat_map { |wrapped| statement_tables(wrapped.stmt.to_h) }
|
|
65
|
+
analysis.with(
|
|
66
|
+
tables: (analysis.tables | tables).freeze,
|
|
67
|
+
write_columns_complete: false
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# A SELECT names its tables in a FROM clause; the statements that write name the one they
|
|
72
|
+
# write in a relation of their own. A PREPARE names whatever the statement it carries names.
|
|
73
|
+
def statement_tables(stmt)
|
|
74
|
+
prepared = stmt.dig(:prepare_stmt, :query)
|
|
75
|
+
return statement_tables(prepared) if prepared.is_a?(Hash)
|
|
76
|
+
return extract_tables_from_clause(Array(stmt.dig(:select_stmt, :from_clause))) if stmt.key?(:select_stmt)
|
|
77
|
+
|
|
78
|
+
written = stmt.values_at(:insert_stmt, :update_stmt, :delete_stmt, :copy_stmt, :merge_stmt).compact.first
|
|
79
|
+
written ? [written.dig(:relation, :relname)].compact : []
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Dispatches a parsed statement hash to the appropriate extract_* method.
|
|
83
|
+
# @param stmt [Hash] top-level statement hash from pg_query
|
|
84
|
+
# @param parameterized [Boolean]
|
|
85
|
+
# @return [QueryAnalysis]
|
|
86
|
+
def extract_from_stmt(stmt, parameterized)
|
|
87
|
+
case stmt
|
|
88
|
+
in { select_stmt: inner_stmt } then extract_select(inner_stmt, parameterized)
|
|
89
|
+
in { insert_stmt: inner_stmt } then extract_insert(inner_stmt, parameterized)
|
|
90
|
+
in { update_stmt: inner_stmt } then extract_update(inner_stmt, parameterized)
|
|
91
|
+
in { delete_stmt: inner_stmt } then extract_delete(inner_stmt, parameterized)
|
|
92
|
+
in { create_stmt: inner_stmt } then extract_create(inner_stmt)
|
|
93
|
+
in { drop_stmt: inner_stmt } then extract_drop(inner_stmt)
|
|
94
|
+
in { copy_stmt: inner_stmt } then extract_copy(inner_stmt, parameterized)
|
|
95
|
+
in { merge_stmt: inner_stmt } then extract_merge(inner_stmt, parameterized)
|
|
96
|
+
in { prepare_stmt: inner_stmt } then extract_prepare(inner_stmt, parameterized)
|
|
97
|
+
else QueryAnalysis.unknown
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A SELECT can carry a data-modifying common table expression, as in
|
|
102
|
+
# +WITH w AS (INSERT INTO t ... RETURNING ...) SELECT ... FROM w+. The write happens through
|
|
103
|
+
# the CTE even though the top-level statement is a SELECT, so the columns those CTEs write
|
|
104
|
+
# are surfaced here rather than the statement being taken for a plain read; without this a
|
|
105
|
+
# value bound into the CTE would reach the server in the clear. A CTE that only deletes
|
|
106
|
+
# stores nothing and is left out. The result is reported as an INSERT so the caller treats it
|
|
107
|
+
# as a write: it is enumerable (and its parameters mapped by their own numbers) when every
|
|
108
|
+
# writing CTE could be read, and fails closed otherwise.
|
|
109
|
+
#
|
|
110
|
+
# @param ctes [Array<Hash>] the +ctes+ of a +with_clause+
|
|
111
|
+
# @return [QueryAnalysis, nil] the combined write of the data-modifying CTEs, or nil if none write
|
|
112
|
+
def data_modifying_cte(ctes, parameterized)
|
|
113
|
+
writes = ctes.filter_map { |entry| entry.dig(:common_table_expr, :ctequery) }
|
|
114
|
+
.select { |query| query.key?(:insert_stmt) || query.key?(:update_stmt) }
|
|
115
|
+
return nil if writes.empty?
|
|
116
|
+
|
|
117
|
+
analyses = writes.map { |query| extract_from_stmt(query, parameterized) }
|
|
118
|
+
QueryAnalysis.new(
|
|
119
|
+
query_type: QueryType::INSERT,
|
|
120
|
+
tables: analyses.flat_map(&:tables).uniq.freeze,
|
|
121
|
+
write_columns: analyses.flat_map(&:write_columns).freeze,
|
|
122
|
+
where_columns: [].freeze,
|
|
123
|
+
for_update: false,
|
|
124
|
+
parameterized: parameterized,
|
|
125
|
+
unbound_write_columns: analyses.flat_map(&:unbound_write_columns).freeze,
|
|
126
|
+
write_columns_complete: analyses.all?(&:write_columns_complete)
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# A MERGE writes through its WHEN clauses: +WHEN MATCHED THEN UPDATE SET ...+ assigns like an
|
|
131
|
+
# UPDATE, and +WHEN NOT MATCHED THEN INSERT (...) VALUES (...)+ fills columns like an INSERT.
|
|
132
|
+
# Both are read here so a value bound into either is paired with the column it fills; a clause
|
|
133
|
+
# that only deletes stores nothing. A value written with something other than a bind parameter
|
|
134
|
+
# is reported unbound, and an INSERT clause without a column list is reported as not
|
|
135
|
+
# enumerable, so a write that cannot be read fails closed rather than every MERGE being refused.
|
|
136
|
+
def extract_merge(stmt, parameterized)
|
|
137
|
+
table = stmt.dig(:relation, :relname)
|
|
138
|
+
bound = []
|
|
139
|
+
unbound = []
|
|
140
|
+
complete = true
|
|
141
|
+
|
|
142
|
+
Array(stmt[:merge_when_clauses]).each do |wrapped|
|
|
143
|
+
clause = wrapped[:merge_when_clause]
|
|
144
|
+
next unless clause
|
|
145
|
+
|
|
146
|
+
# No else: a CMD_DELETE or CMD_NOTHING clause stores nothing, so it contributes no
|
|
147
|
+
# written columns and is intentionally skipped.
|
|
148
|
+
case clause[:command_type]
|
|
149
|
+
when :CMD_UPDATE
|
|
150
|
+
set_bound, set_unbound = extract_assignments(table, Array(clause[:target_list]))
|
|
151
|
+
bound.concat(set_bound)
|
|
152
|
+
unbound.concat(set_unbound)
|
|
153
|
+
when :CMD_INSERT
|
|
154
|
+
declared = Array(clause[:target_list]).filter_map { |entry| entry.dig(:res_target, :name) }
|
|
155
|
+
insert_bound, insert_unbound, insert_complete =
|
|
156
|
+
extract_values(table, declared, [{ list: { items: Array(clause[:values]) } }])
|
|
157
|
+
bound.concat(insert_bound)
|
|
158
|
+
unbound.concat(insert_unbound)
|
|
159
|
+
complete &&= insert_complete
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
QueryAnalysis.new(
|
|
164
|
+
query_type: QueryType::INSERT,
|
|
165
|
+
tables: table ? [table].freeze : [].freeze,
|
|
166
|
+
write_columns: bound.freeze,
|
|
167
|
+
where_columns: [].freeze,
|
|
168
|
+
for_update: false,
|
|
169
|
+
parameterized: parameterized,
|
|
170
|
+
unbound_write_columns: unbound.freeze,
|
|
171
|
+
write_columns_complete: complete
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# A PREPARE is the statement it carries, as far as what gets written where goes: the columns
|
|
176
|
+
# the carried statement writes are the columns the +EXECUTE+ that runs it later writes, and the
|
|
177
|
+
# parameters it declares are the ones that +EXECUTE+ binds. Reading it here is what lets a
|
|
178
|
+
# value written into the body itself, rather than left as a parameter, be seen at the moment
|
|
179
|
+
# the PREPARE is sent, which is the only moment its text is in hand.
|
|
180
|
+
def extract_prepare(stmt, parameterized)
|
|
181
|
+
query = stmt[:query]
|
|
182
|
+
return QueryAnalysis.unknown unless query.is_a?(Hash) && !query.empty?
|
|
183
|
+
|
|
184
|
+
extract_from_stmt(query, parameterized)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def extract_select(stmt, parameterized)
|
|
188
|
+
writing_cte = data_modifying_cte(Array(stmt.dig(:with_clause, :ctes)), parameterized)
|
|
189
|
+
return writing_cte if writing_cte
|
|
190
|
+
|
|
191
|
+
tables = extract_tables_from_clause(Array(stmt[:from_clause]))
|
|
192
|
+
where_cols = parameterized ? extract_where_columns(stmt[:where_clause]) : []
|
|
193
|
+
for_update = Array(stmt[:locking_clause]).any?
|
|
194
|
+
|
|
195
|
+
QueryAnalysis.new(
|
|
196
|
+
query_type: QueryType::SELECT,
|
|
197
|
+
tables: tables.freeze,
|
|
198
|
+
write_columns: [].freeze,
|
|
199
|
+
where_columns: where_cols.freeze,
|
|
200
|
+
for_update: for_update,
|
|
201
|
+
parameterized: parameterized
|
|
202
|
+
)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def extract_insert(stmt, parameterized)
|
|
206
|
+
table = stmt.dig(:relation, :relname)
|
|
207
|
+
declared = Array(stmt[:cols]).filter_map { |column_entry| column_entry.dig(:res_target, :name) }
|
|
208
|
+
value_rows = Array(stmt.dig(:select_stmt, :select_stmt, :values_lists))
|
|
209
|
+
bound, unbound, complete = extract_values(table, declared, value_rows)
|
|
210
|
+
upsert_bound, upsert_unbound = extract_assignments(table, Array(stmt.dig(:on_conflict_clause, :target_list)))
|
|
211
|
+
|
|
212
|
+
QueryAnalysis.new(
|
|
213
|
+
query_type: QueryType::INSERT,
|
|
214
|
+
tables: table ? [table].freeze : [].freeze,
|
|
215
|
+
write_columns: (bound + upsert_bound).freeze,
|
|
216
|
+
where_columns: [].freeze,
|
|
217
|
+
for_update: false,
|
|
218
|
+
parameterized: parameterized,
|
|
219
|
+
unbound_write_columns: (unbound + upsert_unbound).freeze,
|
|
220
|
+
write_columns_complete: complete
|
|
221
|
+
)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def extract_update(stmt, parameterized)
|
|
225
|
+
table = stmt.dig(:relation, :relname)
|
|
226
|
+
set_cols, unbound = extract_assignments(table, Array(stmt[:target_list]))
|
|
227
|
+
where_cols = parameterized ? extract_where_columns(stmt[:where_clause]) : []
|
|
228
|
+
|
|
229
|
+
QueryAnalysis.new(
|
|
230
|
+
query_type: QueryType::UPDATE,
|
|
231
|
+
tables: table ? [table].freeze : [].freeze,
|
|
232
|
+
write_columns: set_cols.freeze,
|
|
233
|
+
where_columns: where_cols.freeze,
|
|
234
|
+
for_update: false,
|
|
235
|
+
parameterized: parameterized,
|
|
236
|
+
unbound_write_columns: unbound.freeze
|
|
237
|
+
)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Pairs each declared column of an INSERT with the value expression that fills it, for every
|
|
241
|
+
# row of the VALUES list, and reports which parameter supplies it.
|
|
242
|
+
#
|
|
243
|
+
# An INSERT can only be read column by column when it does both of those things. Without a
|
|
244
|
+
# column list the values are positional over the table's own column order, which the
|
|
245
|
+
# statement does not carry; with a nested SELECT the values never pass through the client at
|
|
246
|
+
# all. Either way the columns it writes cannot be enumerated.
|
|
247
|
+
#
|
|
248
|
+
# @return [Array(Array<ColumnInfo>, Array<ColumnInfo>, Boolean)] the columns filled by a bind
|
|
249
|
+
# parameter, those filled by something else, and whether every written column was found
|
|
250
|
+
def extract_values(table, declared, value_rows)
|
|
251
|
+
return [[], [], false] if declared.empty? || value_rows.empty?
|
|
252
|
+
|
|
253
|
+
bound = []
|
|
254
|
+
unbound = []
|
|
255
|
+
complete = true
|
|
256
|
+
|
|
257
|
+
value_rows.each do |row|
|
|
258
|
+
values = Array(row.dig(:list, :items))
|
|
259
|
+
complete = false unless values.length == declared.length
|
|
260
|
+
|
|
261
|
+
declared.each_with_index do |column_name, position|
|
|
262
|
+
value = values[position]
|
|
263
|
+
if param_ref?(value)
|
|
264
|
+
bound << ColumnInfo.new(table_name: table, column_name: column_name, parameter_index: param_number(value))
|
|
265
|
+
elsif !null_const?(value)
|
|
266
|
+
unbound << ColumnInfo.new(table_name: table, column_name: column_name)
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
[bound, unbound, complete]
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# The assignments of an UPDATE's SET clause, or of an +ON CONFLICT DO UPDATE SET+ clause,
|
|
275
|
+
# which have the same shape.
|
|
276
|
+
#
|
|
277
|
+
# @return [Array(Array<ColumnInfo>, Array<ColumnInfo>)] the columns assigned from a bind
|
|
278
|
+
# parameter, and those assigned from something else
|
|
279
|
+
def extract_assignments(table, target_list)
|
|
280
|
+
bound = []
|
|
281
|
+
unbound = []
|
|
282
|
+
|
|
283
|
+
target_list.each do |target|
|
|
284
|
+
res_target = target[:res_target]
|
|
285
|
+
column_name = res_target && res_target[:name]
|
|
286
|
+
next unless column_name
|
|
287
|
+
|
|
288
|
+
value = res_target[:val]
|
|
289
|
+
if param_ref?(value)
|
|
290
|
+
bound << ColumnInfo.new(table_name: table, column_name: column_name, parameter_index: param_number(value))
|
|
291
|
+
elsif !null_const?(value)
|
|
292
|
+
unbound << ColumnInfo.new(table_name: table, column_name: column_name)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
[bound, unbound]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def extract_delete(stmt, parameterized)
|
|
300
|
+
table = stmt.dig(:relation, :relname)
|
|
301
|
+
where_cols = parameterized ? extract_where_columns(stmt[:where_clause]) : []
|
|
302
|
+
|
|
303
|
+
QueryAnalysis.new(
|
|
304
|
+
query_type: QueryType::DELETE,
|
|
305
|
+
tables: table ? [table].freeze : [].freeze,
|
|
306
|
+
write_columns: [].freeze,
|
|
307
|
+
where_columns: where_cols.freeze,
|
|
308
|
+
for_update: false,
|
|
309
|
+
parameterized: parameterized
|
|
310
|
+
)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def extract_create(stmt)
|
|
314
|
+
table = stmt.dig(:relation, :relname)
|
|
315
|
+
QueryAnalysis.new(
|
|
316
|
+
query_type: QueryType::CREATE,
|
|
317
|
+
tables: table ? [table].freeze : [].freeze,
|
|
318
|
+
write_columns: [].freeze,
|
|
319
|
+
where_columns: [].freeze,
|
|
320
|
+
for_update: false,
|
|
321
|
+
parameterized: false
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def extract_drop(stmt)
|
|
326
|
+
tables = Array(stmt[:objects]).filter_map do |obj|
|
|
327
|
+
obj.dig(:list, :items, -1, :string, :sval) ||
|
|
328
|
+
obj.dig(:string, :sval)
|
|
329
|
+
end
|
|
330
|
+
QueryAnalysis.new(
|
|
331
|
+
query_type: QueryType::DROP,
|
|
332
|
+
tables: tables.freeze,
|
|
333
|
+
write_columns: [].freeze,
|
|
334
|
+
where_columns: [].freeze,
|
|
335
|
+
for_update: false,
|
|
336
|
+
parameterized: false
|
|
337
|
+
)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# A COPY moves rows between a table and the client in bulk. +COPY table FROM+ stores them, and
|
|
341
|
+
# is reported as a kind of its own: its values arrive as a stream on the connection rather than
|
|
342
|
+
# as bind parameters, so every column it names is a column written with something no caller can
|
|
343
|
+
# substitute a value for, which is what +unbound_write_columns+ says. Without a column list the
|
|
344
|
+
# stream is positional over the table's own column order, which the statement does not carry,
|
|
345
|
+
# so which columns it writes cannot be enumerated at all.
|
|
346
|
+
#
|
|
347
|
+
# +COPY table TO+ reads the table, and +COPY (SELECT ...) TO+ reads whatever the statement it
|
|
348
|
+
# carries reads, which is the one worth reporting.
|
|
349
|
+
def extract_copy(stmt, parameterized)
|
|
350
|
+
table = stmt.dig(:relation, :relname)
|
|
351
|
+
tables = (table ? [table] : []).freeze
|
|
352
|
+
|
|
353
|
+
unless stmt[:is_from]
|
|
354
|
+
query = stmt[:query]
|
|
355
|
+
return extract_from_stmt(query, parameterized) if query.is_a?(Hash) && !query.empty?
|
|
356
|
+
|
|
357
|
+
return QueryAnalysis.new(query_type: QueryType::SELECT, tables: tables, write_columns: [].freeze,
|
|
358
|
+
where_columns: [].freeze, for_update: false, parameterized: false)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
declared = Array(stmt[:attlist]).filter_map { |column_entry| column_entry.dig(:string, :sval) }
|
|
362
|
+
QueryAnalysis.new(
|
|
363
|
+
query_type: QueryType::COPY,
|
|
364
|
+
tables: tables,
|
|
365
|
+
write_columns: [].freeze,
|
|
366
|
+
where_columns: [].freeze,
|
|
367
|
+
for_update: false,
|
|
368
|
+
parameterized: false,
|
|
369
|
+
unbound_write_columns: declared.map { |name| ColumnInfo.new(table_name: table, column_name: name) }.freeze,
|
|
370
|
+
write_columns_complete: declared.any?
|
|
371
|
+
)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def extract_tables_from_clause(from_clause)
|
|
375
|
+
from_clause.flat_map do |from_entry|
|
|
376
|
+
if from_entry[:range_var]
|
|
377
|
+
[from_entry[:range_var][:relname]]
|
|
378
|
+
elsif from_entry[:join_expr]
|
|
379
|
+
extract_tables_from_join(from_entry[:join_expr])
|
|
380
|
+
else
|
|
381
|
+
[]
|
|
382
|
+
end
|
|
383
|
+
end.compact.uniq
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def extract_tables_from_join(join)
|
|
387
|
+
[join[:larg], join[:rarg]].flat_map do |side|
|
|
388
|
+
if side[:range_var]
|
|
389
|
+
[side[:range_var][:relname]]
|
|
390
|
+
elsif side[:join_expr]
|
|
391
|
+
extract_tables_from_join(side[:join_expr])
|
|
392
|
+
else
|
|
393
|
+
[]
|
|
394
|
+
end
|
|
395
|
+
end.compact
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def extract_where_columns(where_clause)
|
|
399
|
+
return [] unless where_clause
|
|
400
|
+
|
|
401
|
+
cols = []
|
|
402
|
+
extract_param_columns(where_clause, cols)
|
|
403
|
+
cols
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def extract_param_columns(expr, cols)
|
|
407
|
+
return unless expr.is_a?(Hash)
|
|
408
|
+
|
|
409
|
+
if expr[:a_expr]
|
|
410
|
+
extract_param_columns_from_a_expr(expr[:a_expr], cols)
|
|
411
|
+
elsif expr[:bool_expr]
|
|
412
|
+
Array(expr[:bool_expr][:args]).each { |arg| extract_param_columns(arg, cols) }
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def extract_param_columns_from_a_expr(a_expr, cols)
|
|
417
|
+
lexpr = a_expr[:lexpr]
|
|
418
|
+
rexpr = a_expr[:rexpr]
|
|
419
|
+
|
|
420
|
+
case a_expr[:kind]
|
|
421
|
+
when :AEXPR_IN, :AEXPR_BETWEEN, :AEXPR_BETWEEN_SYM
|
|
422
|
+
# These read as +column IN/BETWEEN (params...)+, so the column is on the left and rexpr is
|
|
423
|
+
# the list of parameters.
|
|
424
|
+
col_name = column_ref_name(lexpr)
|
|
425
|
+
return unless col_name
|
|
426
|
+
|
|
427
|
+
Array(rexpr.dig(:list, :items)).each do |item|
|
|
428
|
+
cols << ColumnInfo.new(table_name: nil, column_name: col_name, parameter_index: param_number(item)) if param_ref?(item)
|
|
429
|
+
end
|
|
430
|
+
else
|
|
431
|
+
# AEXPR_OP, AEXPR_OP_ANY, AEXPR_OP_ALL — a single node on each side. The column can be on
|
|
432
|
+
# either side of the operator, so +$1 = ssn+ maps just as +ssn = $1+ does.
|
|
433
|
+
map_operator_param(lexpr, rexpr, cols)
|
|
434
|
+
map_operator_param(rexpr, lexpr, cols)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Maps +param_expr+ to +column_expr+ when the one is a bind parameter and the other a column
|
|
439
|
+
# reference; a no-op otherwise, so a comparison of two columns or two parameters maps nothing.
|
|
440
|
+
def map_operator_param(column_expr, param_expr, cols)
|
|
441
|
+
col_name = column_ref_name(column_expr)
|
|
442
|
+
return unless col_name && param_ref?(param_expr)
|
|
443
|
+
|
|
444
|
+
cols << ColumnInfo.new(table_name: nil, column_name: col_name, parameter_index: param_number(param_expr))
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def column_ref_name(expr)
|
|
448
|
+
expr&.dig(:column_ref, :fields, -1, :string, :sval)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def param_ref?(expr)
|
|
452
|
+
return false unless expr.is_a?(Hash)
|
|
453
|
+
|
|
454
|
+
expr.key?(:param_ref)
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# @return [Integer, nil] which parameter the expression is, 1-based, as written in the SQL
|
|
458
|
+
def param_number(expr)
|
|
459
|
+
expr.dig(:param_ref, :number) if expr.is_a?(Hash)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# A NULL is the one value that needs no encrypting, so a column filled with one is not a
|
|
463
|
+
# column written in the clear.
|
|
464
|
+
def null_const?(expr)
|
|
465
|
+
expr.is_a?(Hash) && expr.dig(:a_const, :isnull) == true
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def parameterized?(hash)
|
|
469
|
+
return true if hash.key?(:param_ref)
|
|
470
|
+
|
|
471
|
+
hash.any? { |_, v| (v.is_a?(Hash) && parameterized?(v)) || (v.is_a?(Array) && v.any? { |e| e.is_a?(Hash) && parameterized?(e) }) }
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# Keyword fallback when pg_query raises a parse error. Nothing beyond the kind of statement
|
|
475
|
+
# is known here, so nothing it writes has been enumerated.
|
|
476
|
+
# @param sql [String]
|
|
477
|
+
# @return [QueryAnalysis]
|
|
478
|
+
def fallback_analysis(sql)
|
|
479
|
+
normalized_sql = sql.upcase.lstrip
|
|
480
|
+
query_type = case normalized_sql
|
|
481
|
+
when /\ASELECT\b/ then QueryType::SELECT
|
|
482
|
+
when /\AINSERT\b/ then QueryType::INSERT
|
|
483
|
+
when /\AUPDATE\b/ then QueryType::UPDATE
|
|
484
|
+
when /\ADELETE\b/ then QueryType::DELETE
|
|
485
|
+
when /\ACREATE\b/ then QueryType::CREATE
|
|
486
|
+
when /\ADROP\b/ then QueryType::DROP
|
|
487
|
+
else QueryType::UNKNOWN
|
|
488
|
+
end
|
|
489
|
+
QueryAnalysis.new(
|
|
490
|
+
query_type: query_type,
|
|
491
|
+
tables: [].freeze,
|
|
492
|
+
write_columns: [].freeze,
|
|
493
|
+
where_columns: [].freeze,
|
|
494
|
+
for_update: false,
|
|
495
|
+
parameterized: sql.match?(/\$\d+/), # fallback only; AST unavailable here
|
|
496
|
+
write_columns_complete: false
|
|
497
|
+
)
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
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 'query_type'
|
|
18
|
+
|
|
19
|
+
module AwsAdvancedRubyDriverWrapper
|
|
20
|
+
module Utils
|
|
21
|
+
module Parser
|
|
22
|
+
# What one statement was found to be doing.
|
|
23
|
+
#
|
|
24
|
+
# +write_columns+ are the columns whose value comes from a bind parameter, which are the only
|
|
25
|
+
# ones a caller can substitute a value for. +unbound_write_columns+ are columns the statement
|
|
26
|
+
# also writes, but with something no caller can reach: a literal, an expression, a DEFAULT, or
|
|
27
|
+
# a nested SELECT. +write_columns_complete+ says whether those two together name every column
|
|
28
|
+
# the statement writes; false means the statement writes columns that could not be enumerated
|
|
29
|
+
# at all, which is what an INSERT with no column list, an +INSERT ... SELECT+, and a statement
|
|
30
|
+
# that would not parse all look like.
|
|
31
|
+
QueryAnalysis = Data.define(:query_type, :tables, :write_columns, :where_columns, :for_update,
|
|
32
|
+
:parameterized, :unbound_write_columns, :write_columns_complete) do
|
|
33
|
+
def initialize(query_type:, tables:, write_columns:, where_columns:, for_update:, parameterized:,
|
|
34
|
+
unbound_write_columns: [].freeze, write_columns_complete: true)
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A statement nothing could be established about. Its written columns are reported as not
|
|
39
|
+
# enumerated, since a statement that could not be read may well be storing values.
|
|
40
|
+
def self.unknown
|
|
41
|
+
new(
|
|
42
|
+
query_type: QueryType::UNKNOWN,
|
|
43
|
+
tables: [].freeze,
|
|
44
|
+
write_columns: [].freeze,
|
|
45
|
+
where_columns: [].freeze,
|
|
46
|
+
for_update: false,
|
|
47
|
+
parameterized: false,
|
|
48
|
+
write_columns_complete: false
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# One column a statement touches. +parameter_index+ is the 1-based position of the bind
|
|
54
|
+
# parameter that supplies its value, when the analyzer could work that out; nil means the
|
|
55
|
+
# caller has to fall back to the order the columns were reported in.
|
|
56
|
+
ColumnInfo = Data.define(:table_name, :column_name, :parameter_index) do
|
|
57
|
+
def initialize(table_name:, column_name:, parameter_index: nil)
|
|
58
|
+
super
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
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 QueryType
|
|
21
|
+
SELECT = :select
|
|
22
|
+
INSERT = :insert
|
|
23
|
+
UPDATE = :update
|
|
24
|
+
DELETE = :delete
|
|
25
|
+
CREATE = :create
|
|
26
|
+
DROP = :drop
|
|
27
|
+
# A COPY that stores rows. It is a kind of its own rather than an INSERT because its values
|
|
28
|
+
# reach the server as a stream instead of as bind parameters, so what a caller can do about
|
|
29
|
+
# a column it writes is not the same.
|
|
30
|
+
COPY = :copy
|
|
31
|
+
UNKNOWN = :unknown
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
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 Parser
|
|
20
|
+
module RoutingHint
|
|
21
|
+
READER = :reader
|
|
22
|
+
WRITER = :writer
|
|
23
|
+
KEEP = :keep
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
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 'routing_hint'
|
|
18
|
+
|
|
19
|
+
module AwsAdvancedRubyDriverWrapper
|
|
20
|
+
module Utils
|
|
21
|
+
module Parser
|
|
22
|
+
module RoutingHintParser
|
|
23
|
+
HINT_PATTERN = %r{/\*\s*@\s*(reader|writer|keep)\s*\*/}i
|
|
24
|
+
|
|
25
|
+
HINT_MAP = {
|
|
26
|
+
'reader' => RoutingHint::READER,
|
|
27
|
+
'writer' => RoutingHint::WRITER,
|
|
28
|
+
'keep' => RoutingHint::KEEP
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def parse_routing_hint(sql)
|
|
34
|
+
return nil unless sql.is_a?(String) && !sql.empty?
|
|
35
|
+
|
|
36
|
+
hint_match = HINT_PATTERN.match(sql)
|
|
37
|
+
return nil unless hint_match
|
|
38
|
+
|
|
39
|
+
HINT_MAP[hint_match[1].downcase]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def strip_routing_hint(sql)
|
|
43
|
+
return sql unless sql.is_a?(String) && !sql.empty?
|
|
44
|
+
|
|
45
|
+
sql.gsub(HINT_PATTERN, '').strip
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|