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.
Files changed (150) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +175 -0
  4. data/NOTICE +1 -0
  5. data/README.md +168 -0
  6. data/THIRD-PARTY-LICENSES +473 -0
  7. data/aws_advanced_ruby_driver_wrapper.gemspec +73 -0
  8. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_mysql2_adapter.rb +73 -0
  9. data/lib/aws_advanced_ruby_driver_wrapper/active_record/aws_postgresql_adapter.rb +95 -0
  10. data/lib/aws_advanced_ruby_driver_wrapper/custom_configuration.rb +58 -0
  11. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_mysql_dialect.rb +103 -0
  12. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/aurora_pg_dialect.rb +124 -0
  13. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/dialect_codes.rb +38 -0
  14. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_mysql_dialect.rb +91 -0
  15. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/global_pg_dialect.rb +92 -0
  16. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_mysql_dialect.rb +95 -0
  17. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/multi_az_cluster_pg_dialect.rb +86 -0
  18. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/mysql_dialect.rb +98 -0
  19. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/pg_dialect.rb +95 -0
  20. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_mysql_dialect.rb +88 -0
  21. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/rds_pg_dialect.rb +86 -0
  22. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/unknown_dialect.rb +72 -0
  23. data/lib/aws_advanced_ruby_driver_wrapper/db_dialects/utils/dialect_utils.rb +71 -0
  24. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect.rb +154 -0
  25. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/driver_dialect_manager.rb +55 -0
  26. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/mysql_driver_dialect.rb +165 -0
  27. data/lib/aws_advanced_ruby_driver_wrapper/driver_dialects/pg_driver_dialect.rb +201 -0
  28. data/lib/aws_advanced_ruby_driver_wrapper/errors/error_handler.rb +62 -0
  29. data/lib/aws_advanced_ruby_driver_wrapper/errors/mysql_error_handler.rb +80 -0
  30. data/lib/aws_advanced_ruby_driver_wrapper/errors/pg_error_handler.rb +126 -0
  31. data/lib/aws_advanced_ruby_driver_wrapper/errors.rb +59 -0
  32. data/lib/aws_advanced_ruby_driver_wrapper/host/connection_string_host_list_provider.rb +95 -0
  33. data/lib/aws_advanced_ruby_driver_wrapper/host/global_aurora_host_list_provider.rb +65 -0
  34. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability.rb +24 -0
  35. data/lib/aws_advanced_ruby_driver_wrapper/host/host_availability_strategy.rb +27 -0
  36. data/lib/aws_advanced_ruby_driver_wrapper/host/host_info.rb +137 -0
  37. data/lib/aws_advanced_ruby_driver_wrapper/host/host_role.rb +25 -0
  38. data/lib/aws_advanced_ruby_driver_wrapper/host/random_host_selector.rb +40 -0
  39. data/lib/aws_advanced_ruby_driver_wrapper/host/rds_host_list_provider.rb +206 -0
  40. data/lib/aws_advanced_ruby_driver_wrapper/logging.rb +110 -0
  41. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/cluster_topology_monitor.rb +709 -0
  42. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/global_cluster_topology_monitor.rb +72 -0
  43. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor.rb +99 -0
  44. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_connection.rb +57 -0
  45. data/lib/aws_advanced_ruby_driver_wrapper/monitoring/monitor_state.rb +25 -0
  46. data/lib/aws_advanced_ruby_driver_wrapper/mysql.rb +429 -0
  47. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/blue_green_plugin.rb +205 -0
  48. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/host_mapper.rb +132 -0
  49. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/iam_host_tracker.rb +84 -0
  50. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interim_status.rb +92 -0
  51. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/interval_rate.rb +27 -0
  52. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase.rb +69 -0
  53. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_event_log.rb +85 -0
  54. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/phase_time_info.rb +25 -0
  55. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/role.rb +38 -0
  56. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/base_routing.rb +83 -0
  57. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/reject_connect_routing.rb +40 -0
  58. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/substitute_connect_routing.rb +136 -0
  59. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_connect_routing.rb +53 -0
  60. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_execute_routing.rb +52 -0
  61. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/routing/suspend_until_corresponding_host_found_connect_routing.rb +83 -0
  62. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status.rb +68 -0
  63. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_builder.rb +244 -0
  64. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_info.rb +30 -0
  65. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_monitor.rb +564 -0
  66. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/status_provider.rb +414 -0
  67. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_state.rb +98 -0
  68. data/lib/aws_advanced_ruby_driver_wrapper/plugins/blue_green/switchover_timer.rb +46 -0
  69. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_monitor.rb +266 -0
  70. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/custom_endpoint_plugin.rb +158 -0
  71. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/info.rb +111 -0
  72. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/member_list_type.rb +31 -0
  73. data/lib/aws_advanced_ruby_driver_wrapper/plugins/custom_endpoint/role.rb +45 -0
  74. data/lib/aws_advanced_ruby_driver_wrapper/plugins/default_plugin.rb +108 -0
  75. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_mode.rb +43 -0
  76. data/lib/aws_advanced_ruby_driver_wrapper/plugins/failover_plugin.rb +467 -0
  77. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_mode.rb +68 -0
  78. data/lib/aws_advanced_ruby_driver_wrapper/plugins/gdb/gdb_failover_plugin.rb +403 -0
  79. data/lib/aws_advanced_ruby_driver_wrapper/plugins/iam_auth_plugin.rb +159 -0
  80. data/lib/aws_advanced_ruby_driver_wrapper/plugins/initial_connection_strategy_plugin.rb +485 -0
  81. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/audit_logger.rb +157 -0
  82. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_cipher.rb +159 -0
  83. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/column_encryption_config.rb +61 -0
  84. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/connection_source.rb +91 -0
  85. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/data_key_cache.rb +220 -0
  86. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_algorithm.rb +75 -0
  87. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_config.rb +146 -0
  88. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/encryption_service.rb +391 -0
  89. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/error_context.rb +198 -0
  90. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/errors.rb +259 -0
  91. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_management_utility.rb +435 -0
  92. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_manager.rb +378 -0
  93. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/key_metadata.rb +86 -0
  94. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_plugin.rb +890 -0
  95. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/kms_encryption_utility.rb +281 -0
  96. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/metadata_manager.rb +332 -0
  97. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sanitizer.rb +147 -0
  98. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_name.rb +70 -0
  99. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/schema_validator.rb +211 -0
  100. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/sql_runner.rb +147 -0
  101. data/lib/aws_advanced_ruby_driver_wrapper/plugins/kms_encryption/type_marker.rb +109 -0
  102. data/lib/aws_advanced_ruby_driver_wrapper/plugins/secrets_manager_plugin.rb +358 -0
  103. data/lib/aws_advanced_ruby_driver_wrapper/postgresql.rb +659 -0
  104. data/lib/aws_advanced_ruby_driver_wrapper/property_definition.rb +409 -0
  105. data/lib/aws_advanced_ruby_driver_wrapper/ruby_method.rb +122 -0
  106. data/lib/aws_advanced_ruby_driver_wrapper/services/connection_service.rb +143 -0
  107. data/lib/aws_advanced_ruby_driver_wrapper/services/dialect_service.rb +267 -0
  108. data/lib/aws_advanced_ruby_driver_wrapper/services/host_service.rb +199 -0
  109. data/lib/aws_advanced_ruby_driver_wrapper/services/monitor_service.rb +186 -0
  110. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_call_context.rb +63 -0
  111. data/lib/aws_advanced_ruby_driver_wrapper/services/plugin_manager.rb +273 -0
  112. data/lib/aws_advanced_ruby_driver_wrapper/services/service_container.rb +30 -0
  113. data/lib/aws_advanced_ruby_driver_wrapper/services/service_utility.rb +78 -0
  114. data/lib/aws_advanced_ruby_driver_wrapper/services/session_state_service.rb +56 -0
  115. data/lib/aws_advanced_ruby_driver_wrapper/utils/accessible_regions.rb +52 -0
  116. data/lib/aws_advanced_ruby_driver_wrapper/utils/ar_constants.rb +25 -0
  117. data/lib/aws_advanced_ruby_driver_wrapper/utils/aurora_topology_utils.rb +99 -0
  118. data/lib/aws_advanced_ruby_driver_wrapper/utils/aws_credentials_utils.rb +62 -0
  119. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config.rb +91 -0
  120. data/lib/aws_advanced_ruby_driver_wrapper/utils/connection_config_parser.rb +368 -0
  121. data/lib/aws_advanced_ruby_driver_wrapper/utils/conversion_utils.rb +51 -0
  122. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/batching_event_publisher.rb +119 -0
  123. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/data_access_event.rb +26 -0
  124. data/lib/aws_advanced_ruby_driver_wrapper/utils/events/monitor_reset_event.rb +26 -0
  125. data/lib/aws_advanced_ruby_driver_wrapper/utils/global_aurora_topology_utils.rb +185 -0
  126. data/lib/aws_advanced_ruby_driver_wrapper/utils/host_list_utils.rb +27 -0
  127. data/lib/aws_advanced_ruby_driver_wrapper/utils/iam_auth_utils.rb +112 -0
  128. data/lib/aws_advanced_ruby_driver_wrapper/utils/multi_az_topology_utils.rb +117 -0
  129. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/encryption_annotation_parser.rb +99 -0
  130. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/mysql_statement_analyzer.rb +641 -0
  131. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/pg_statement_analyzer.rb +502 -0
  132. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_analysis.rb +63 -0
  133. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/query_type.rb +35 -0
  134. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint.rb +27 -0
  135. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/routing_hint_parser.rb +50 -0
  136. data/lib/aws_advanced_ruby_driver_wrapper/utils/parser/sql_parser.rb +139 -0
  137. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_url_type.rb +71 -0
  138. data/lib/aws_advanced_ruby_driver_wrapper/utils/rds_utils.rb +575 -0
  139. data/lib/aws_advanced_ruby_driver_wrapper/utils/retry_util.rb +153 -0
  140. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_encoding.rb +56 -0
  141. data/lib/aws_advanced_ruby_driver_wrapper/utils/sql_method_analyzer.rb +195 -0
  142. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/cache_entry.rb +56 -0
  143. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/expiration_cache.rb +108 -0
  144. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/sliding_expiration_cache.rb +137 -0
  145. data/lib/aws_advanced_ruby_driver_wrapper/utils/storage/storage_service.rb +172 -0
  146. data/lib/aws_advanced_ruby_driver_wrapper/utils/topology_utils.rb +127 -0
  147. data/lib/aws_advanced_ruby_driver_wrapper/version.rb +19 -0
  148. data/lib/aws_advanced_ruby_driver_wrapper/wrapper_property.rb +64 -0
  149. data/lib/aws_advanced_ruby_driver_wrapper.rb +116 -0
  150. 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