@@ -41,6 +41,7 @@ def setup_database():
4141 init_sql = f .read ()
4242 with conn .cursor () as cur :
4343 cur .execute ("DROP TABLE IF EXISTS gaps CASCADE;" )
44+ cur .execute ("DROP TABLE IF EXISTS reasons CASCADE;" )
4445 cur .execute ("DROP TABLE IF EXISTS collections CASCADE;" )
4546 cur .execute (init_sql )
4647
@@ -59,16 +60,24 @@ def setup_test_data():
5960
6061 with get_db_connection () as conn :
6162 with conn .cursor () as cur :
62- cur .execute ("TRUNCATE TABLE gaps" )
63+ # Clear existing data
64+ cur .execute ("TRUNCATE TABLE gaps CASCADE" )
65+ cur .execute ("TRUNCATE TABLE reasons CASCADE" )
6366 cur .execute ("TRUNCATE TABLE collections CASCADE" )
67+
68+ # Disable trigger to prevent automatic gap creation
6469 cur .execute ("ALTER TABLE collections DISABLE TRIGGER collection_insert_trigger" )
70+
71+ # Insert test collections
6572 cur .execute ("""
6673 INSERT INTO collections (collection_id, temporal_extent_start, temporal_extent_end)
6774 VALUES (%s, %s, %s), (%s, %s, %s)
6875 """ , (
6976 TEST_COLLECTION_ID , DEFAULT_DATE , DEFAULT_END_DATE ,
7077 SECOND_COLLECTION_ID , DEFAULT_DATE , DEFAULT_END_DATE
7178 ))
79+
80+ # Re-enable trigger
7281 cur .execute ("ALTER TABLE collections ENABLE TRIGGER collection_insert_trigger" )
7382 conn .commit ()
7483
@@ -86,28 +95,48 @@ def mock_sql():
8695
8796# Helper Functions
8897def create_test_partition (collection_id , conn ):
89- """Create a test partition for a collection ."""
98+ """Create test partitions for both gaps and reasons tables ."""
9099 with conn .cursor () as cur :
91100 safe_collection_id = re .sub (r'\W+' , '_' , collection_id )
92- partition_name = f"gaps_{ safe_collection_id } "
101+
102+ # Create gaps partition
103+ gaps_partition_name = f"gaps_{ safe_collection_id } "
93104 cur .execute ("""
94105 SELECT 1 FROM pg_class c
95106 JOIN pg_namespace n ON n.oid = c.relnamespace
96107 WHERE c.relname = %s AND n.nspname = 'public'
97- """ , (partition_name ,))
108+ """ , (gaps_partition_name ,))
109+
98110 if cur .fetchone () is None :
99111 cur .execute (
100112 SQL ("CREATE TABLE {} PARTITION OF gaps FOR VALUES IN ({})" ).format (
101- Identifier (partition_name ), Literal (collection_id )
113+ Identifier (gaps_partition_name ), Literal (collection_id )
102114 )
103115 )
104- constraint_name = f"{ partition_name } _no_overlap"
116+ # Add exclusion constraint to prevent overlapping time ranges
117+ constraint_name = f"{ gaps_partition_name } _no_overlap"
105118 cur .execute (
106119 SQL ("ALTER TABLE {} ADD CONSTRAINT {} EXCLUDE USING gist (tsrange(start_ts, end_ts) WITH &&)" ).format (
107- Identifier (partition_name ), Identifier (constraint_name )
120+ Identifier (gaps_partition_name ), Identifier (constraint_name )
121+ )
122+ )
123+
124+ # Create reasons partition
125+ reasons_partition_name = f"reasons_{ safe_collection_id } "
126+ cur .execute ("""
127+ SELECT 1 FROM pg_class c
128+ JOIN pg_namespace n ON n.oid = c.relnamespace
129+ WHERE c.relname = %s AND n.nspname = 'public'
130+ """ , (reasons_partition_name ,))
131+
132+ if cur .fetchone () is None :
133+ cur .execute (
134+ SQL ("CREATE TABLE {} PARTITION OF reasons FOR VALUES IN ({})" ).format (
135+ Identifier (reasons_partition_name ), Literal (collection_id )
108136 )
109137 )
110- return partition_name
138+
139+ return gaps_partition_name , reasons_partition_name
111140
112141def create_granule (start , end , collection_id = TEST_COLLECTION_ID ):
113142 return {
@@ -144,14 +173,33 @@ def create_sqs_event(records_data):
144173 return {"Records" : sqs_records }
145174
146175def insert_gap (collection_id , start_ts , end_ts ):
176+ """Insert a gap without a reason."""
177+ from utils import get_db_connection
178+
179+ with get_db_connection () as conn :
180+ with conn .cursor () as cur :
181+ cur .execute ("""
182+ INSERT INTO gaps (collection_id, start_ts, end_ts)
183+ VALUES (%s, %s, %s)
184+ """ , (collection_id , start_ts , end_ts ))
185+
186+ def insert_gap_with_reason (collection_id , start_ts , end_ts , reason ):
187+ """Insert a gap and its associated reason."""
147188 from utils import get_db_connection
148189
149190 with get_db_connection () as conn :
150191 with conn .cursor () as cur :
192+ # Insert the gap
151193 cur .execute ("""
152194 INSERT INTO gaps (collection_id, start_ts, end_ts)
153195 VALUES (%s, %s, %s)
154196 """ , (collection_id , start_ts , end_ts ))
197+
198+ # Insert the reason
199+ cur .execute ("""
200+ INSERT INTO reasons (collection_id, start_ts, end_ts, reason)
201+ VALUES (%s, %s, %s, %s)
202+ """ , (collection_id , start_ts , end_ts , reason ))
155203
156204def get_gaps (collection_id ):
157205 from utils import get_db_connection
@@ -164,6 +212,25 @@ def get_gaps(collection_id):
164212 )
165213 return cur .fetchall ()
166214
215+ def get_gaps_with_reasons (collection_id ):
216+ """Get gaps with their associated reasons."""
217+ from utils import get_db_connection
218+
219+ with get_db_connection () as conn :
220+ with conn .cursor () as cur :
221+ cur .execute ("""
222+ SELECT g.start_ts, g.end_ts, r.reason
223+ FROM gaps g
224+ LEFT JOIN reasons r ON (
225+ g.collection_id = r.collection_id
226+ AND g.start_ts = r.start_ts
227+ AND g.end_ts = r.end_ts
228+ )
229+ WHERE g.collection_id = %s
230+ ORDER BY g.start_ts
231+ """ , (collection_id ,))
232+ return cur .fetchall ()
233+
167234def get_gap_count (collection_id ):
168235 from utils import get_db_connection
169236
@@ -195,62 +262,85 @@ def create_api_test_event(http_method, path, body=None, query_string_parameters=
195262 return event
196263
197264def get_record (conn , collection_id , start_ts , end_ts ):
265+ """Get a gap record with its reason if available."""
198266 with conn .cursor () as cur :
199267 cur .execute ("""
200- SELECT gap_id, collection_id, start_ts, end_ts, reason
201- FROM gaps
202- WHERE collection_id = %s
203- AND start_ts = %s
204- AND end_ts = %s
268+ SELECT g.gap_id, g.collection_id, g.start_ts, g.end_ts, r.reason
269+ FROM gaps g
270+ LEFT JOIN reasons r ON (
271+ g.collection_id = r.collection_id
272+ AND g.start_ts = r.start_ts
273+ AND g.end_ts = r.end_ts
274+ )
275+ WHERE g.collection_id = %s
276+ AND g.start_ts = %s
277+ AND g.end_ts = %s
205278 """ , (collection_id , start_ts , end_ts ))
206279 return cur .fetchone ()
207280
208281def seed_test_data (test_data , collection_id = TEST_COLLECTION_ID ):
209282 """Seed test data for knownGap tests."""
210-
211283 from utils import get_db_connection
284+
212285 with get_db_connection () as conn :
213286 with conn .cursor () as cur :
214- cur .execute ("SELECT 1 FROM collections WHERE collection_id = %s" , (TEST_COLLECTION_ID ,))
287+ # Ensure collection exists
288+ cur .execute ("SELECT 1 FROM collections WHERE collection_id = %s" , (collection_id ,))
215289 if not cur .fetchone ():
216290 cur .execute (
217291 "INSERT INTO collections (collection_id, temporal_extent_start, temporal_extent_end) VALUES (%s, %s, %s)" ,
218- (TEST_COLLECTION_ID , DEFAULT_DATE , DEFAULT_END_DATE )
292+ (collection_id , DEFAULT_DATE , DEFAULT_END_DATE )
219293 )
220294
221- safe_id = re .sub (r'\W+' , '_' , TEST_COLLECTION_ID )
222- partition_name = f"gaps_{ safe_id } "
223-
224- # Check if partition exists
225- cur .execute (f"""
226- SELECT 1 FROM pg_class c
227- JOIN pg_namespace n ON n.oid = c.relnamespace
228- WHERE c.relname = '{ partition_name } ' AND n.nspname = 'public'
229- """ )
230-
231- if not cur .fetchone ():
232- # Create partition
233- cur .execute (f"""
234- CREATE TABLE { partition_name } PARTITION OF gaps
235- FOR VALUES IN ('{ TEST_COLLECTION_ID } ')
236- """ )
237- cur .execute (f"""
238- ALTER TABLE { partition_name } ADD CONSTRAINT { partition_name } _no_overlap
239- EXCLUDE USING gist (tsrange(start_ts, end_ts) WITH &&)
240- """ )
295+ # Ensure partitions exist
296+ create_test_partition (collection_id , conn )
241297
242- cur .execute ("DELETE FROM gaps WHERE collection_id = %s" , (TEST_COLLECTION_ID ,))
298+ # Clear existing test data
299+ cur .execute ("DELETE FROM gaps WHERE collection_id = %s" , (collection_id ,))
300+ cur .execute ("DELETE FROM reasons WHERE collection_id = %s" , (collection_id ,))
243301
244302 # Insert test data
245303 for data in test_data :
304+ start_ts = data .get ('start_ts' )
305+ end_ts = data .get ('end_ts' )
306+ reason = data .get ('reason' )
307+
308+ # Insert gap
246309 cur .execute (
247- "INSERT INTO gaps (collection_id, start_ts, end_ts, reason) VALUES (%s, %s, %s, %s)" ,
248- (
249- TEST_COLLECTION_ID ,
250- data .get ('start_ts' ),
251- data .get ('end_ts' ),
252- data .get ('reason' )
253- )
310+ "INSERT INTO gaps (collection_id, start_ts, end_ts) VALUES (%s, %s, %s)" ,
311+ (collection_id , start_ts , end_ts )
254312 )
313+
314+ # Insert reason if provided
315+ if reason :
316+ cur .execute (
317+ "INSERT INTO reasons (collection_id, start_ts, end_ts, reason) VALUES (%s, %s, %s, %s)" ,
318+ (collection_id , start_ts , end_ts , reason )
319+ )
255320
256321 conn .commit ()
322+
323+ def get_reason (collection_id , start_ts , end_ts ):
324+ """Get the reason for a specific gap."""
325+ from utils import get_db_connection
326+
327+ with get_db_connection () as conn :
328+ with conn .cursor () as cur :
329+ cur .execute ("""
330+ SELECT reason FROM reasons
331+ WHERE collection_id = %s AND start_ts = %s AND end_ts = %s
332+ """ , (collection_id , start_ts , end_ts ))
333+ result = cur .fetchone ()
334+ return result [0 ] if result else None
335+
336+ def insert_reason (collection_id , start_ts , end_ts , reason ):
337+ """Insert a reason for an existing gap."""
338+ from utils import get_db_connection
339+
340+ with get_db_connection () as conn :
341+ with conn .cursor () as cur :
342+ cur .execute ("""
343+ INSERT INTO reasons (collection_id, start_ts, end_ts, reason)
344+ VALUES (%s, %s, %s, %s)
345+ """ , (collection_id , start_ts , end_ts , reason ))
346+ conn .commit ()
0 commit comments