44
55import logging
66import re
7+ from typing import Dict , List , Optional , Tuple
78
89import psycopg
910
2627
2728EXPLAIN_QUERY = 'SELECT {explain_function}(%s)'
2829
30+ # Errors raised when PostgreSQL can't resolve a parameter's type while preparing or explaining a parameterized
31+ # query (e.g. untyped NULL parameters from ORMs using the extended query protocol: "operator does not exist:
32+ # bigint = text"). The parameter types that made the original query valid aren't available in pg_stat_activity,
33+ # so these queries can't be explained and the failure is deterministic for a given query signature.
34+ EXPECTED_PARAMETER_TYPE_ERRORS = (
35+ psycopg .errors .IndeterminateDatatype ,
36+ psycopg .errors .UndefinedFunction ,
37+ psycopg .errors .DatatypeMismatch ,
38+ )
39+
2940
3041def agent_check_getter (self ):
3142 return self ._check
@@ -73,7 +84,9 @@ def __init__(self, check, config, explain_function):
7384 self ._explain_function = explain_function
7485
7586 @tracked_method (agent_check_getter = agent_check_getter )
76- def explain_statement (self , dbname , statement , obfuscated_statement , query_signature ):
87+ def explain_statement (
88+ self , dbname : str , statement : str , obfuscated_statement : str , query_signature : str
89+ ) -> Tuple [Optional [Dict ], Optional [DBExplainError ], Optional [str ]]:
7790 if self ._check .version < V12 :
7891 # if pg version < 12, skip explaining parameterized queries because
7992 # plan_cache_mode is not supported
@@ -86,18 +99,27 @@ def explain_statement(self, dbname, statement, obfuscated_statement, query_signa
8699 with self ._check .db_pool .get_connection (dbname ) as conn :
87100 try :
88101 self ._set_plan_cache_mode (conn )
89- self ._create_prepared_statement (conn , statement , obfuscated_statement , query_signature )
90- except psycopg .errors .IndeterminateDatatype as e :
91- return None , DBExplainError .indeterminate_datatype , '{}' .format (type (e ))
92- except psycopg .errors .UndefinedFunction as e :
93- return None , DBExplainError .undefined_function , '{}' .format (type (e ))
102+ prepared_statement_error = self ._create_prepared_statement (
103+ conn , statement , obfuscated_statement , query_signature
104+ )
94105 except Exception as e :
95- # if we fail to create a prepared statement, we cannot explain the query
106+ # an unexpected failure creating the prepared statement means we cannot explain the query
96107 return None , DBExplainError .failed_to_explain_with_prepared_statement , '{}' .format (type (e ))
97108
109+ if prepared_statement_error is not None :
110+ # an expected, deterministic parameter type-resolution failure (e.g. untyped NULL parameters).
111+ error_code , err_msg = prepared_statement_error
112+ return None , error_code , err_msg
113+
98114 try :
99- result = self ._explain_prepared_statement (conn , statement , obfuscated_statement , query_signature )
100- if result :
115+ result , explain_error = self ._explain_prepared_statement (
116+ conn , statement , obfuscated_statement , query_signature
117+ )
118+ if explain_error is not None :
119+ # an expected, deterministic parameter type-resolution failure surfaced during EXPLAIN EXECUTE
120+ error_code , err_msg = explain_error
121+ return None , error_code , err_msg
122+ elif result :
101123 plan = result [0 ][0 ][0 ]
102124 return plan , DBExplainError .explained_with_prepared_statement , None
103125 else :
@@ -117,24 +139,57 @@ def _set_plan_cache_mode(self, conn):
117139 self ._execute_query (conn , "SET plan_cache_mode = force_generic_plan" )
118140
119141 @tracked_method (agent_check_getter = agent_check_getter )
120- def _create_prepared_statement (self , conn , statement , obfuscated_statement , query_signature ):
142+ def _create_prepared_statement (
143+ self , conn , statement : str , obfuscated_statement : str , query_signature : str
144+ ) -> Optional [Tuple [DBExplainError , str ]]:
145+ # Returns None on success, or a (DBExplainError, err_msg) tuple when the query can't be prepared because
146+ # a parameter's type can't be resolved. Other unexpected errors are re-raised.
121147 try :
122148 self ._execute_query (
123149 conn ,
124150 PREPARE_STATEMENT_QUERY .format (query_signature = query_signature , statement = statement ),
125151 )
152+ return None
153+ except EXPECTED_PARAMETER_TYPE_ERRORS as e :
154+ # The parameter types can't be resolved, so this query can't be prepared (and therefore can't be
155+ # explained). Map the failure to the corresponding explain error code.
156+ self ._log_failed_statement (
157+ 'Failed to create prepared statement when explaining statement(%s)=[%s] | err=[%s]' ,
158+ statement ,
159+ obfuscated_statement ,
160+ query_signature ,
161+ e ,
162+ )
163+ return self ._map_parameter_type_error (e )
126164 except Exception as e :
127- logged_statement = obfuscated_statement
128- if self ._config .log_unobfuscated_plans :
129- logged_statement = statement
130- logger .debug (
165+ self ._log_failed_statement (
131166 'Failed to create prepared statement when explaining statement(%s)=[%s] | err=[%s]' ,
167+ statement ,
168+ obfuscated_statement ,
132169 query_signature ,
133- logged_statement ,
134170 e ,
135171 )
136172 raise
137173
174+ def _map_parameter_type_error (self , e : Exception ) -> Tuple [DBExplainError , str ]:
175+ # Map an unresolved parameter-type error to its specific explain error code so cached responses
176+ # and emitted error tags reflect the actual failure rather than a generic one.
177+ if isinstance (e , psycopg .errors .IndeterminateDatatype ):
178+ return DBExplainError .indeterminate_datatype , '{}' .format (type (e ))
179+ if isinstance (e , psycopg .errors .DatatypeMismatch ):
180+ return DBExplainError .datatype_mismatch , '{}' .format (type (e ))
181+ return DBExplainError .undefined_function , '{}' .format (type (e ))
182+
183+ def _log_failed_statement (
184+ self , message : str , statement : str , obfuscated_statement : str , query_signature : str , e : Exception
185+ ) -> None :
186+ # Logs the obfuscated statement by default, falling back to the raw statement only when explicitly
187+ # configured. The message is expected to interpolate (query_signature, statement, error) in that order.
188+ logged_statement = obfuscated_statement
189+ if self ._config .log_unobfuscated_plans :
190+ logged_statement = statement
191+ logger .debug (message , query_signature , logged_statement , e )
192+
138193 @tracked_method (agent_check_getter = agent_check_getter )
139194 def _get_number_of_parameters_for_prepared_statement (self , conn , query_signature ):
140195 rows = self ._execute_query_and_fetch_rows (conn , PARAM_TYPES_COUNT_QUERY .format (query_signature = query_signature ))
@@ -152,22 +207,35 @@ def _generate_prepared_statement_query(self, conn, query_signature: str) -> str:
152207 return EXECUTE_PREPARED_STATEMENT_QUERY .format (prepared_statement = query_signature , parameters = parameters )
153208
154209 @tracked_method (agent_check_getter = agent_check_getter )
155- def _explain_prepared_statement (self , conn , statement , obfuscated_statement , query_signature ):
210+ def _explain_prepared_statement (
211+ self , conn , statement : str , obfuscated_statement : str , query_signature : str
212+ ) -> Tuple [Optional [List ], Optional [Tuple [DBExplainError , str ]]]:
213+ # Returns (rows, None) on success, or (None, (DBExplainError, err_msg)) when a parameter's type can't be
214+ # resolved during EXPLAIN EXECUTE. Other unexpected errors are re-raised.
156215 try :
157216 prepared_statement_query = self ._generate_prepared_statement_query (conn , query_signature )
158- return self ._execute_query_and_fetch_rows (
217+ rows = self ._execute_query_and_fetch_rows (
159218 conn ,
160219 EXPLAIN_QUERY .format (explain_function = self ._explain_function ),
161220 (prepared_statement_query ,),
162221 )
222+ return rows , None
223+ except EXPECTED_PARAMETER_TYPE_ERRORS as e :
224+ # The parameter types couldn't be resolved during EXPLAIN EXECUTE, so the query can't be explained.
225+ self ._log_failed_statement (
226+ 'Failed to explain parameterized statement(%s)=[%s] | err=[%s]' ,
227+ statement ,
228+ obfuscated_statement ,
229+ query_signature ,
230+ e ,
231+ )
232+ return None , self ._map_parameter_type_error (e )
163233 except Exception as e :
164- logged_statement = obfuscated_statement
165- if self ._config .log_unobfuscated_plans :
166- logged_statement = statement
167- logger .debug (
234+ self ._log_failed_statement (
168235 'Failed to explain parameterized statement(%s)=[%s] | err=[%s]' ,
236+ statement ,
237+ obfuscated_statement ,
169238 query_signature ,
170- logged_statement ,
171239 e ,
172240 )
173241 raise
0 commit comments