@@ -206,3 +206,100 @@ def test_logical_replication(self):
206206 })
207207 self .assertEqual (messages [4 ]['type' ], 'STATE' )
208208 self .assertDictEqual (state , messages [4 ]['value' ])
209+
210+
211+ class TestUnselectedTableSlotAdvancement (unittest .TestCase ):
212+ """Test that WAL slot LSN advances even when only non-selected tables have activity.
213+
214+ This verifies the include-transaction: true fix — B/C markers from transactions
215+ on unselected tables cause the slot to advance, preventing unbounded slot growth.
216+ """
217+ selected_table = None
218+ unselected_table = None
219+ maxDiff = None
220+
221+ @classmethod
222+ def setUpClass (cls ) -> None :
223+ cls .selected_table = 'selected_table'
224+ cls .unselected_table = 'unselected_table'
225+
226+ selected_spec = {
227+ "columns" : [
228+ {"name" : "id" , "type" : "serial" , "primary_key" : True },
229+ {"name" : "val" , "type" : "character varying" },
230+ ],
231+ "name" : cls .selected_table ,
232+ }
233+ unselected_spec = {
234+ "columns" : [
235+ {"name" : "id" , "type" : "serial" , "primary_key" : True },
236+ {"name" : "val" , "type" : "character varying" },
237+ ],
238+ "name" : cls .unselected_table ,
239+ }
240+
241+ ensure_test_table (selected_spec )
242+ ensure_test_table (unselected_spec )
243+ create_replication_slot ()
244+
245+ cls .config = get_test_connection_config ()
246+ tap_postgres .dump_catalog = lambda catalog : True
247+
248+ @classmethod
249+ def tearDownClass (cls ) -> None :
250+ drop_replication_slot ()
251+ drop_table (cls .selected_table )
252+ drop_table (cls .unselected_table )
253+
254+ def test_slot_advances_with_only_unselected_table_activity (self ):
255+ """Slot LSN must advance when only the unselected table receives writes."""
256+
257+ # Discover streams, select only `selected_table` for LOG_BASED
258+ streams = tap_postgres .do_discovery (self .config )
259+ selected_stream = [s for s in streams if s ['tap_stream_id' ] == f'public-{ self .selected_table } ' ][0 ]
260+ selected_stream = set_replication_method_for_stream (selected_stream , 'LOG_BASED' )
261+
262+ # Insert a row into the selected table so initial sync has something to process
263+ conn = get_test_connection ()
264+ try :
265+ with conn .cursor () as cur :
266+ insert_record (cur , self .selected_table , {'val' : 'seed' })
267+ finally :
268+ conn .close ()
269+
270+ # Initial sync to establish bookmarks
271+ state = {}
272+ my_stdout = io .StringIO ()
273+ with contextlib .redirect_stdout (my_stdout ):
274+ state = tap_postgres .do_sync (self .config , {'streams' : [selected_stream ]}, 'LOG_BASED' , state , None )
275+
276+ # Capture LSN after initial sync
277+ initial_lsn = state ['bookmarks' ][f'public-{ self .selected_table } ' ]['lsn' ]
278+ self .assertIsNotNone (initial_lsn , "Initial sync should set an LSN bookmark" )
279+
280+ # Now insert rows ONLY into the unselected table
281+ conn = get_test_connection ()
282+ try :
283+ with conn .cursor () as cur :
284+ for i in range (5 ):
285+ insert_record (cur , self .unselected_table , {'val' : f'noise_{ i } ' })
286+ finally :
287+ conn .close ()
288+
289+ # Run sync again — no rows from selected table, but B/C markers should advance the slot
290+ my_stdout .seek (0 )
291+ my_stdout .truncate ()
292+ with contextlib .redirect_stdout (my_stdout ):
293+ state = tap_postgres .do_sync (self .config , {'streams' : [selected_stream ]}, 'LOG_BASED' , state , None )
294+
295+ # Assert that the LSN bookmark has advanced past the unselected-table activity
296+ new_lsn = state ['bookmarks' ][f'public-{ self .selected_table } ' ]['lsn' ]
297+ self .assertGreater (new_lsn , initial_lsn ,
298+ "LSN bookmark should advance when unselected tables have WAL activity "
299+ "(B/C markers from include-transaction: true)" )
300+
301+ # Verify no RECORD messages were emitted (only the unselected table had activity)
302+ messages = [json .loads (msg ) for msg in my_stdout .getvalue ().splitlines ()]
303+ record_messages = [m for m in messages if m ['type' ] == 'RECORD' ]
304+ self .assertEqual (len (record_messages ), 0 ,
305+ "No RECORD messages should be emitted for unselected table activity" )
0 commit comments