Skip to content

Commit c606dfa

Browse files
jasonmp85claude
andauthored
Add parameterized query support to QueryExecutor (#23469)
* [datadog_checks_base] Add parameterized query support to QueryExecutor Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [datadog_checks_base] Add changelog entry for parameterized query support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [datadog_checks_base] Rename changelog entry to match PR number Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * [datadog_checks_base] Add tests for parameterized query support in QueryExecutor Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent afae40b commit c606dfa

4 files changed

Lines changed: 65 additions & 4 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add parameterized query support to QueryExecutor.

datadog_checks_base/datadog_checks/base/utils/db/core.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,9 @@ def execute(self, extra_tags=None):
8484
try:
8585
if self.track_operation_time:
8686
with tracked_query(check=self.submitter, operation=query_name):
87-
rows = self.execute_query(query.query)
87+
rows = self.execute_query(query.query, query.params)
8888
else:
89-
rows = self.execute_query(query.query)
89+
rows = self.execute_query(query.query, query.params)
9090
except Exception as e:
9191
if self.error_handler:
9292
self.logger.error('Error querying %s: %s', query_name, self.error_handler(str(e)))
@@ -158,12 +158,12 @@ def _is_row_valid(self, query, row):
158158
return False
159159
return True
160160

161-
def execute_query(self, query):
161+
def execute_query(self, query, params=None):
162162
"""
163163
Called by `execute`, this triggers query execution to check for errors immediately in a way that is compatible
164164
with any library. If there are no errors, this is guaranteed to return an iterator over the result set.
165165
"""
166-
rows = self.executor(query)
166+
rows = self.executor(query, params=params) if params else self.executor(query)
167167
if rows is None:
168168
return iter([])
169169
else:

datadog_checks_base/datadog_checks/base/utils/db/query.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,16 @@ def __init__(self, query_data):
4545
The query will be run at the next check run after the collection interval has passed.
4646
- (Optional) metric_prefix (str): The prefix to add to the metric name.
4747
Note: If the metric prefix is None, the default metric prefix `<INTEGRATION>.` will be used.
48+
- (Optional) params (Sequence): Bound parameters to pass alongside the query at execution time.
49+
The sequence is forwarded as-is to the executor's cursor.execute(query, params) call.
4850
'''
4951
# Contains the data to fill the rest of the attributes
5052
self.query_data = deepcopy(query_data or {}) # type: Dict[str, Any]
5153
self.name = None # type: str
5254
# The actual query
5355
self.query = None # type: str
56+
# Optional bound parameters forwarded to the executor alongside the query string
57+
self.params = None # type: Any
5458
# Contains a mapping of column_name -> column_type, transformer
5559
self.column_transformers = None # type: Tuple[Tuple[str, Tuple[str, Transformer]]]
5660
# These transformers are used to collect extra metrics calculated from the query result
@@ -250,6 +254,7 @@ def compile(
250254

251255
self.name = query_name
252256
self.query = query
257+
self.params = self.query_data.get('params')
253258
self.column_transformers = tuple(column_data)
254259
self.extra_transformers = tuple(extra_data)
255260
self.base_tags = tags

datadog_checks_base/tests/base/utils/db/test_query_executor.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,58 @@ def test_query_with_metric_prefix_with_exception(self, metric_prefix, expected_e
265265

266266
with pytest.raises(expected_exception):
267267
qe.compile_queries()
268+
269+
def test_query_with_params_forwards_params_to_executor(self, aggregator):
270+
"""Test that bound params are forwarded to the executor as a keyword argument."""
271+
captured = {}
272+
273+
def executor(query, params=None):
274+
captured['params'] = params
275+
return [[99]]
276+
277+
queries = [
278+
{
279+
'name': 'parameterized',
280+
'query': 'SELECT metric FROM t WHERE id = ?',
281+
'params': ('some_id',),
282+
'columns': [{'name': 'test.metric', 'type': 'gauge'}],
283+
}
284+
]
285+
286+
check = AgentCheck('test', {}, [{}])
287+
qe = QueryExecutor(executor, check, queries=queries)
288+
qe.compile_queries()
289+
qe.execute()
290+
291+
assert captured['params'] == ('some_id',)
292+
aggregator.assert_metric('test.metric', 99, metric_type=aggregator.GAUGE)
293+
294+
@pytest.mark.parametrize(
295+
'params_value,include_in_query',
296+
[
297+
pytest.param((), True, id='empty_params_tuple'),
298+
pytest.param(None, False, id='no_params_key'),
299+
],
300+
)
301+
def test_query_without_params_does_not_pass_params_to_executor(self, aggregator, params_value, include_in_query):
302+
"""Test that executor is called without params when params are absent or empty."""
303+
received_kwargs = {}
304+
305+
def executor(query, **kwargs):
306+
received_kwargs.update(kwargs)
307+
return [[1]]
308+
309+
query = {
310+
'name': 'simple',
311+
'query': 'SELECT 1',
312+
'columns': [{'name': 'test.metric', 'type': 'gauge'}],
313+
}
314+
if include_in_query:
315+
query['params'] = params_value
316+
317+
check = AgentCheck('test', {}, [{}])
318+
qe = QueryExecutor(executor, check, queries=[query])
319+
qe.compile_queries()
320+
qe.execute()
321+
322+
assert 'params' not in received_kwargs

0 commit comments

Comments
 (0)