88
99import psycopg2
1010import psycopg2 .extras
11+ import psycopg2 .sql
1112from dotenv import load_dotenv
1213
1314ALL_CONFIG_KEYS = {
2021 "results_table" : ("SOURCE_DB_RESULT_TABLE" , "" ),
2122}
2223
24+ # Grace period after a linked JIRA ticket is closed before a known-issue row
25+ # is excluded from matching. Distinct from the unrelated lookback_hours=4
26+ # recency windows used elsewhere in this pipeline.
27+ TICKET_CLOSED_GRACE_HOURS = 4
28+
29+ _TICKET_COLUMNS = ("ticket_link" , "ticket_resolve_datetime_gmt" )
30+
31+ # Per-table cache so the schema is only probed once per process, not once per query.
32+ _ticket_columns_present : dict [str , bool ] = {}
33+
34+
35+ def _has_ticket_columns (conn : Any , table : str ) -> bool :
36+ if table not in _ticket_columns_present :
37+ with conn .cursor (cursor_factory = psycopg2 .extensions .cursor ) as cur :
38+ cur .execute (
39+ "SELECT column_name FROM information_schema.columns"
40+ " WHERE table_name = %s AND column_name = ANY(%s)" ,
41+ (table , list (_TICKET_COLUMNS )),
42+ )
43+ found = {row [0 ] for row in cur .fetchall ()}
44+ present = set (_TICKET_COLUMNS ) <= found
45+ if not present :
46+ print (
47+ f"[WARN] { table } is missing { ', ' .join (_TICKET_COLUMNS )} ; "
48+ "known-issue ticket filtering disabled, treating all known issues as active" ,
49+ file = sys .stderr ,
50+ )
51+ _ticket_columns_present [table ] = present
52+ return _ticket_columns_present [table ]
53+
2354
2455def load_config (required : tuple [str , ...] = ("name" , "user" , "password" )) -> dict [str , Any ]:
2556 env_file = os .path .join (os .path .dirname (__file__ ), ".." , ".env" )
@@ -39,6 +70,45 @@ def load_config(required: tuple[str, ...] = ("name", "user", "password")) -> dic
3970 return config
4071
4172
73+ def known_issue_active_sql (
74+ conn : Any ,
75+ alias : str = "" ,
76+ * ,
77+ table : str ,
78+ ) -> psycopg2 .sql .Composable :
79+ """Boolean SQL fragment (no leading AND): TRUE unless the row's linked
80+ ticket is closed and has been for TICKET_CLOSED_GRACE_HOURS+.
81+
82+ ticket_link and ticket_resolve_datetime_gmt live directly on the row
83+ being filtered (the results table).
84+
85+ A row is excluded only when ticket_link is set and
86+ ticket_resolve_datetime_gmt is old enough. Every other combination (no
87+ ticket, or an unknown/recent resolve time) is active.
88+
89+ If the results table is missing ticket_link/ticket_resolve_datetime_gmt
90+ (schema drift), ticket filtering is disabled and every row is treated as
91+ active -- a warning is printed once per table.
92+ """
93+
94+ def col (name : str ) -> psycopg2 .sql .Identifier :
95+ return psycopg2 .sql .Identifier (alias , name ) if alias else psycopg2 .sql .Identifier (name )
96+
97+ if not table :
98+ raise ValueError ("table is required for known_issue_active_sql" )
99+
100+ if not _has_ticket_columns (conn , table ):
101+ return psycopg2 .sql .SQL ("TRUE" )
102+
103+ return psycopg2 .sql .SQL (
104+ "({tl} IS NULL OR {rd} IS NULL OR {rd} > NOW() - make_interval(hours => {h}))"
105+ ).format (
106+ tl = col ("ticket_link" ),
107+ rd = col ("ticket_resolve_datetime_gmt" ),
108+ h = psycopg2 .sql .Literal (TICKET_CLOSED_GRACE_HOURS ),
109+ )
110+
111+
42112def connect_db (config : dict [str , Any ], * , use_dict_cursor : bool = False ) -> Any :
43113 kwargs : dict [str , Any ] = {
44114 "host" : config ["host" ],
0 commit comments