@@ -521,6 +521,65 @@ function dropCrrTriggers(db: DatabaseSyncType, tableName: string, logger?: Logge
521521 return triggers . length ;
522522}
523523
524+ /** Strip CRR triggers/metadata so row deletes stay local (no replicated tombstones). */
525+ function deleteAllRowsWithoutCrrReplication (
526+ db : DatabaseSyncType ,
527+ tableName : string ,
528+ logger ?: Logger ,
529+ ) : void {
530+ if ( ! rawHasTable ( db , tableName ) ) return ;
531+
532+ const clockTableName = `${ tableName } __crsql_clock` ;
533+ const pksTableName = `${ tableName } __crsql_pks` ;
534+ if ( rawHasTable ( db , clockTableName ) || rawHasTable ( db , pksTableName ) || listCrrTriggers ( db , tableName ) . length > 0 ) {
535+ if ( rawHasTable ( db , "crsql_master" ) && rawHasColumn ( db , "crsql_master" , "tbl_name" ) ) {
536+ runStatement ( db , "delete from crsql_master where tbl_name = ?" , [ tableName ] ) ;
537+ }
538+ if ( rawHasTable ( db , "crsql_changes" ) && rawHasColumn ( db , "crsql_changes" , "table" ) ) {
539+ runStatement ( db , "delete from crsql_changes where [table] = ?" , [ tableName ] ) ;
540+ }
541+ try {
542+ getRow ( db , "select crsql_as_table(?) as ok" , [ tableName ] ) ;
543+ } catch {
544+ // Table may not be registered enough for crsql_as_table; shadow cleanup below still applies.
545+ }
546+ dropCrrTriggers ( db , tableName , logger ) ;
547+ runStatement ( db , `drop table if exists ${ quoteIdentifier ( clockTableName ) } ` ) ;
548+ runStatement ( db , `drop table if exists ${ quoteIdentifier ( pksTableName ) } ` ) ;
549+ }
550+
551+ runStatement ( db , `delete from ${ quoteIdentifier ( tableName ) } ` ) ;
552+ }
553+
554+ const QUEUE_OVERHAUL_WIPE_MARKER = "queue_landing_state.wiped_for_stacked_overhaul.v1" ;
555+
556+ /**
557+ * One-shot local wipe of legacy queue_landing_state on upgrade to the stacked-PR
558+ * queue overhaul. Must run after migrations and before ensureCrrTables so deletes
559+ * do not replicate to peers that have not upgraded yet.
560+ */
561+ function wipeQueueLandingStateForStackedOverhaulIfNeeded ( db : DatabaseSyncType , logger ?: Logger ) : void {
562+ try {
563+ const row = getRow < { value : string } > (
564+ db ,
565+ "select value from kv where key = ?" ,
566+ [ QUEUE_OVERHAUL_WIPE_MARKER ] ,
567+ ) ;
568+ if ( row ) return ;
569+
570+ deleteAllRowsWithoutCrrReplication ( db , "queue_landing_state" , logger ) ;
571+ runStatement (
572+ db ,
573+ "insert into kv (key, value) values (?, ?) on conflict(key) do update set value = excluded.value" ,
574+ [ QUEUE_OVERHAUL_WIPE_MARKER , new Date ( ) . toISOString ( ) ] ,
575+ ) ;
576+ } catch {
577+ // Table may not exist on a brand-new DB; initialization will create both
578+ // tables and the next startup will record the marker. Skipping the wipe
579+ // on a fresh DB is correct (nothing to wipe).
580+ }
581+ }
582+
524583function removeExcludedCrrMetadata ( db : DatabaseSyncType , logger ?: Logger ) : void {
525584 for ( const tableName of LOCAL_ONLY_CRR_EXCLUDED_TABLES ) {
526585 const clockTableName = `${ tableName } __crsql_clock` ;
@@ -1821,32 +1880,6 @@ function migrate(db: MigrationDb) {
18211880 try { db . run ( "alter table queue_landing_state add column wait_reason text" ) ; } catch { }
18221881 try { db . run ( "alter table queue_landing_state add column updated_at text" ) ; } catch { }
18231882
1824- // One-shot wipe of legacy queue_landing_state on upgrade to the stacked-PR
1825- // queue overhaul. The new queue creates PRs with chain bases (PR_N's base =
1826- // previous lane's branch) instead of all-into-main, so any in-flight queue
1827- // from the old code path would be misinterpreted by the new landing loop.
1828- // Wiping rather than migrating is a deliberate choice — the user accepts
1829- // losing in-flight queues in exchange for not maintaining a translation
1830- // layer for every legacy field shape.
1831- const QUEUE_OVERHAUL_WIPE_MARKER = "queue_landing_state.wiped_for_stacked_overhaul.v1" ;
1832- try {
1833- const row = db . get < { value : string } > (
1834- "select value from kv where key = ?" ,
1835- [ QUEUE_OVERHAUL_WIPE_MARKER ] ,
1836- ) ;
1837- if ( ! row ) {
1838- db . run ( "delete from queue_landing_state" ) ;
1839- db . run (
1840- "insert into kv (key, value) values (?, ?) on conflict(key) do update set value = excluded.value" ,
1841- [ QUEUE_OVERHAUL_WIPE_MARKER , new Date ( ) . toISOString ( ) ] ,
1842- ) ;
1843- }
1844- } catch {
1845- // Table may not exist on a brand-new DB; initialization will create both
1846- // tables and the next startup will record the marker. Skipping the wipe
1847- // on a fresh DB is correct (nothing to wipe).
1848- }
1849-
18501883 // Rebase dismiss/defer persistence
18511884 db . run ( `
18521885 create table if not exists rebase_dismissed (
@@ -2849,6 +2882,8 @@ export async function openKvDb(dbPath: string, logger: Logger): Promise<AdeDb> {
28492882 removeExcludedCrrMetadata ( db , logger ) ;
28502883 }
28512884
2885+ wipeQueueLandingStateForStackedOverhaulIfNeeded ( db , logger ) ;
2886+
28522887 if ( crsqliteLoaded ) {
28532888 loadCrsqliteIfAvailable ( ) ;
28542889 ensureCrrTables ( db , logger ) ;
0 commit comments