@@ -208,6 +208,63 @@ def test_lookup_updates_lru_order(self):
208208 _ , misses = lk .lookup ({(2 , 2 , 2 )})
209209 assert (2 , 2 , 2 ) in misses
210210
211+ # --- negative cache (ignored keys) ---
212+
213+ def test_mark_ignored_excludes_from_hits_and_misses (self ):
214+ """A negatively-cached key is neither a hit nor a miss on lookup."""
215+ lk = self ._make_lookup ()
216+ lk .mark_ignored ({(1 , 1 , 1 )})
217+ hits , misses = lk .lookup ({(1 , 1 , 1 ), (2 , 1 , 1 )})
218+ assert (1 , 1 , 1 ) not in hits
219+ assert (1 , 1 , 1 ) not in misses
220+ assert misses == {(2 , 1 , 1 )}
221+ assert lk .ignored_map_size == 1
222+
223+ def test_ignored_keys_do_not_increment_miss_counter (self ):
224+ lk = self ._make_lookup ()
225+ lk .mark_ignored ({(1 , 1 , 1 )})
226+ lk .reset_stats ()
227+ lk .lookup ({(1 , 1 , 1 )})
228+ assert lk .misses == 0
229+ assert lk .hits == 0
230+
231+ def test_evict_forgets_ignored_key (self ):
232+ """Evicting a vanished key clears its negative-cache entry so it can be re-evaluated."""
233+ lk = self ._make_lookup ()
234+ lk .mark_ignored ({(1 , 1 , 1 )})
235+ lk .evict ({(1 , 1 , 1 )})
236+ assert lk .ignored_map_size == 0
237+ _ , misses = lk .lookup ({(1 , 1 , 1 )})
238+ assert (1 , 1 , 1 ) in misses
239+
240+ def test_ignored_keys_lru_trimmed_to_maxsize (self ):
241+ lk = self ._make_lookup (maxsize = 2 )
242+ lk .mark_ignored ({(1 , 1 , 1 ), (2 , 2 , 2 ), (3 , 3 , 3 )})
243+ assert lk .ignored_map_size == 2
244+
245+ def test_mark_ignored_drops_stale_positive_mapping (self ):
246+ """An ignored key must not resurface as a hit via a stale tier-1 mapping.
247+
248+ Reproduces the case where a key keeps its tier-1 mapping after its tier-2
249+ signature was evicted: marking it ignored must drop the tier-1 entry so that,
250+ even after the negative entry is trimmed and the signature is repopulated by
251+ another key, the ignored key never produces a positive hit.
252+ """
253+ lk = self ._make_lookup ()
254+ # Two keys share the same normalized SQL (one signature).
255+ lk .populate ({(1 , 1 , 1 ): 'SELECT 1' , (2 , 1 , 1 ): 'SELECT 1' })
256+ assert lk .queryid_map_size == 2
257+
258+ # Key (1, 1, 1) turns out to be ignorable; its tier-1 mapping must be dropped.
259+ lk .mark_ignored ({(1 , 1 , 1 )})
260+ assert (1 , 1 , 1 ) not in lk ._key_to_sig
261+
262+ # The shared signature is still cached (via the other key), but the ignored key
263+ # must not hit it.
264+ hits , misses = lk .lookup ({(1 , 1 , 1 )})
265+ assert (1 , 1 , 1 ) not in hits
266+ assert (1 , 1 , 1 ) not in misses
267+
211268
212269# ---------------------------------------------------------------------------
213270# PostgresStatementMetricsV2 — unit tests (no live database)
@@ -300,6 +357,42 @@ def test_resolve_obfuscations_partial_filter(self):
300357 assert bad_key not in result
301358 assert good_key in result
302359
360+ def test_resolve_obfuscations_skips_known_ddignore_keys_on_later_cycles (self ):
361+ """A DDIGNORE key is fetched once, negative-cached, then skipped (no fetch) on later cycles."""
362+ v2 = self ._make ()
363+ ddignore_key = (1 , 1 , 1 )
364+
365+ with mock .patch .object (
366+ v2 , '_fetch_query_texts' , return_value = {ddignore_key : '/* DDIGNORE */ SELECT 1' }
367+ ) as fetch :
368+ v2 ._resolve_obfuscations ({ddignore_key }, set ())
369+ assert fetch .call_count == 1
370+ assert ddignore_key in v2 ._obfuscation_lookup ._ignored_keys
371+
372+ # Second cycle: same key changes again but is now skipped before the fetch.
373+ result = v2 ._resolve_obfuscations ({ddignore_key }, set ())
374+ assert result == {}
375+ assert fetch .call_count == 1
376+
377+ def test_resolve_obfuscations_does_not_fetch_when_all_keys_ignored (self ):
378+ """When every changed key is already negative-cached, no text fetch is issued."""
379+ v2 = self ._make ()
380+ ddignore_key = (1 , 1 , 1 )
381+ v2 ._obfuscation_lookup .mark_ignored ({ddignore_key })
382+ with mock .patch .object (v2 , '_fetch_query_texts' ) as fetch :
383+ result = v2 ._resolve_obfuscations ({ddignore_key }, set ())
384+ assert result == {}
385+ fetch .assert_not_called ()
386+
387+ def test_resolve_obfuscations_forgets_ignored_key_when_vanished (self ):
388+ """An ignored key that vanishes from pgss is dropped from the negative cache via evict."""
389+ v2 = self ._make ()
390+ ddignore_key = (1 , 1 , 1 )
391+ v2 ._obfuscation_lookup .mark_ignored ({ddignore_key })
392+ with mock .patch .object (v2 , '_fetch_query_texts' , return_value = {}):
393+ v2 ._resolve_obfuscations (set (), {ddignore_key })
394+ assert ddignore_key not in v2 ._obfuscation_lookup ._ignored_keys
395+
303396 # --- execute query cancel event ---
304397
305398 def test_execute_query_raises_when_cancelled (self ):
0 commit comments