@@ -2069,6 +2069,172 @@ int merge_insert (cloudsync_context *data, cloudsync_table_context *table, const
20692069
20702070// MARK: - Block column setup -
20712071
2072+ // Migrate existing tracked rows to block format when block-level LWW is first enabled on a column.
2073+ // Scans the metadata table for alive rows with the plain col_name entry (not yet block entries),
2074+ // reads each row's current value from the base table, splits it into blocks, and inserts
2075+ // the block entries into both the blocks table and the metadata table.
2076+ // Uses INSERT OR IGNORE semantics so the operation is safe to call multiple times.
2077+ static int block_migrate_existing_rows (cloudsync_context * data , cloudsync_table_context * table , int col_idx ) {
2078+ const char * col_name = table -> col_name [col_idx ];
2079+ if (!col_name || !table -> meta_ref || !table -> blocks_ref ) return DBRES_OK ;
2080+
2081+ const char * delim = table -> col_delimiter [col_idx ] ? table -> col_delimiter [col_idx ] : BLOCK_DEFAULT_DELIMITER ;
2082+ int64_t db_version = cloudsync_dbversion_next (data , CLOUDSYNC_VALUE_NOTSET );
2083+
2084+ // Phase 1: collect all existing PKs that have an alive regular col_name entry
2085+ // AND do not yet have any entries in the blocks table for this column.
2086+ // The NOT IN filter makes this idempotent: rows that were already migrated
2087+ // (or had their blocks created via INSERT) are skipped on subsequent calls.
2088+ // We collect PKs before writing so that writes to the metadata table (Phase 2)
2089+ // do not perturb the read cursor on the same table.
2090+ char * like_pattern = block_build_colname (col_name , "%" );
2091+ if (!like_pattern ) return DBRES_NOMEM ;
2092+
2093+ char * scan_sql = cloudsync_memory_mprintf (SQL_META_SCAN_COL_FOR_MIGRATION , table -> meta_ref , table -> blocks_ref );
2094+ if (!scan_sql ) { cloudsync_memory_free (like_pattern ); return DBRES_NOMEM ; }
2095+ dbvm_t * scan_vm = NULL ;
2096+ int rc = databasevm_prepare (data , scan_sql , & scan_vm , 0 );
2097+ cloudsync_memory_free (scan_sql );
2098+ if (rc != DBRES_OK ) { cloudsync_memory_free (like_pattern ); return rc ; }
2099+
2100+ rc = databasevm_bind_text (scan_vm , 1 , col_name , -1 );
2101+ if (rc != DBRES_OK ) { cloudsync_memory_free (like_pattern ); databasevm_finalize (scan_vm ); return rc ; }
2102+ // Bind like_pattern as ?2 and keep it alive until after all scan steps complete,
2103+ // because databasevm_bind_text uses SQLITE_STATIC (no copy).
2104+ rc = databasevm_bind_text (scan_vm , 2 , like_pattern , -1 );
2105+ if (rc != DBRES_OK ) { cloudsync_memory_free (like_pattern ); databasevm_finalize (scan_vm ); return rc ; }
2106+
2107+ // Collect pk blobs into a dynamically-grown array of owned copies
2108+ void * * pks = NULL ;
2109+ size_t * pklens = NULL ;
2110+ int pk_count = 0 ;
2111+ int pk_cap = 0 ;
2112+
2113+ while ((rc = databasevm_step (scan_vm )) == DBRES_ROW ) {
2114+ size_t pklen = 0 ;
2115+ const void * pk = database_column_blob (scan_vm , 0 , & pklen );
2116+ if (!pk || pklen == 0 ) continue ;
2117+
2118+ if (pk_count >= pk_cap ) {
2119+ int new_cap = pk_cap ? pk_cap * 2 : 8 ;
2120+ void * * new_pks = (void * * )cloudsync_memory_realloc (pks , (uint64_t )(new_cap * sizeof (void * )));
2121+ size_t * new_pklens = (size_t * )cloudsync_memory_realloc (pklens , (uint64_t )(new_cap * sizeof (size_t )));
2122+ if (!new_pks || !new_pklens ) {
2123+ cloudsync_memory_free (new_pks ? new_pks : pks );
2124+ cloudsync_memory_free (new_pklens ? new_pklens : pklens );
2125+ databasevm_finalize (scan_vm );
2126+ return DBRES_NOMEM ;
2127+ }
2128+ pks = new_pks ;
2129+ pklens = new_pklens ;
2130+ pk_cap = new_cap ;
2131+ }
2132+
2133+ pks [pk_count ] = cloudsync_memory_alloc ((uint64_t )pklen );
2134+ if (!pks [pk_count ]) { rc = DBRES_NOMEM ; break ; }
2135+ memcpy (pks [pk_count ], pk , pklen );
2136+ pklens [pk_count ] = pklen ;
2137+ pk_count ++ ;
2138+ }
2139+
2140+ databasevm_finalize (scan_vm );
2141+ cloudsync_memory_free (like_pattern ); // safe to free after scan_vm is finalized
2142+ if (rc != DBRES_DONE && rc != DBRES_OK ) {
2143+ for (int i = 0 ; i < pk_count ; i ++ ) cloudsync_memory_free (pks [i ]);
2144+ cloudsync_memory_free (pks );
2145+ cloudsync_memory_free (pklens );
2146+ return rc ;
2147+ }
2148+
2149+ if (pk_count == 0 ) {
2150+ cloudsync_memory_free (pks );
2151+ cloudsync_memory_free (pklens );
2152+ return DBRES_OK ;
2153+ }
2154+
2155+ // Phase 2: for each collected PK, read the column value, split into blocks,
2156+ // and insert into the blocks table + metadata using INSERT OR IGNORE.
2157+
2158+ char * meta_sql = cloudsync_memory_mprintf (SQL_META_INSERT_BLOCK_IGNORE , table -> meta_ref );
2159+ if (!meta_sql ) { rc = DBRES_NOMEM ; goto cleanup_pks ; }
2160+ dbvm_t * meta_vm = NULL ;
2161+ rc = databasevm_prepare (data , meta_sql , & meta_vm , 0 );
2162+ cloudsync_memory_free (meta_sql );
2163+ if (rc != DBRES_OK ) goto cleanup_pks ;
2164+
2165+ char * blocks_sql = cloudsync_memory_mprintf (SQL_BLOCKS_INSERT_IGNORE , table -> blocks_ref );
2166+ if (!blocks_sql ) { databasevm_finalize (meta_vm ); rc = DBRES_NOMEM ; goto cleanup_pks ; }
2167+ dbvm_t * blocks_vm = NULL ;
2168+ rc = databasevm_prepare (data , blocks_sql , & blocks_vm , 0 );
2169+ cloudsync_memory_free (blocks_sql );
2170+ if (rc != DBRES_OK ) { databasevm_finalize (meta_vm ); goto cleanup_pks ; }
2171+
2172+ dbvm_t * val_vm = (dbvm_t * )table_column_lookup (table , col_name , false, NULL );
2173+
2174+ for (int p = 0 ; p < pk_count ; p ++ ) {
2175+ const void * pk = pks [p ];
2176+ size_t pklen = pklens [p ];
2177+
2178+ if (!val_vm ) continue ;
2179+
2180+ // Read current column value from the base table
2181+ int bind_rc = pk_decode_prikey ((char * )pk , pklen , pk_decode_bind_callback , (void * )val_vm );
2182+ if (bind_rc < 0 ) { databasevm_reset (val_vm ); continue ; }
2183+
2184+ int step_rc = databasevm_step (val_vm );
2185+ const char * text = (step_rc == DBRES_ROW ) ? database_column_text (val_vm , 0 ) : NULL ;
2186+ // Make a copy of text before resetting val_vm, as the pointer is only valid until reset
2187+ char * text_copy = text ? cloudsync_string_dup (text ) : NULL ;
2188+ databasevm_reset (val_vm );
2189+
2190+ if (!text_copy ) continue ; // NULL column value: nothing to migrate
2191+
2192+ // Split text into blocks and store each one
2193+ block_list_t * blocks = block_split (text_copy , delim );
2194+ cloudsync_memory_free (text_copy );
2195+ if (!blocks ) continue ;
2196+
2197+ char * * positions = block_initial_positions (blocks -> count );
2198+ if (positions ) {
2199+ for (int b = 0 ; b < blocks -> count ; b ++ ) {
2200+ char * block_cn = block_build_colname (col_name , positions [b ]);
2201+ if (block_cn ) {
2202+ // Metadata entry (skip if this block position already exists)
2203+ databasevm_bind_blob (meta_vm , 1 , pk , (int )pklen );
2204+ databasevm_bind_text (meta_vm , 2 , block_cn , -1 );
2205+ databasevm_bind_int (meta_vm , 3 , 1 ); // col_version = 1 (alive)
2206+ databasevm_bind_int (meta_vm , 4 , db_version );
2207+ databasevm_bind_int (meta_vm , 5 , cloudsync_bumpseq (data ));
2208+ databasevm_step (meta_vm );
2209+ databasevm_reset (meta_vm );
2210+
2211+ // Block value (skip if this block position already exists)
2212+ databasevm_bind_blob (blocks_vm , 1 , pk , (int )pklen );
2213+ databasevm_bind_text (blocks_vm , 2 , block_cn , -1 );
2214+ databasevm_bind_text (blocks_vm , 3 , blocks -> entries [b ].content , -1 );
2215+ databasevm_step (blocks_vm );
2216+ databasevm_reset (blocks_vm );
2217+
2218+ cloudsync_memory_free (block_cn );
2219+ }
2220+ cloudsync_memory_free (positions [b ]);
2221+ }
2222+ cloudsync_memory_free (positions );
2223+ }
2224+ block_list_free (blocks );
2225+ }
2226+
2227+ databasevm_finalize (meta_vm );
2228+ databasevm_finalize (blocks_vm );
2229+ rc = DBRES_OK ;
2230+
2231+ cleanup_pks :
2232+ for (int i = 0 ; i < pk_count ; i ++ ) cloudsync_memory_free (pks [i ]);
2233+ cloudsync_memory_free (pks );
2234+ cloudsync_memory_free (pklens );
2235+ return rc ;
2236+ }
2237+
20722238int cloudsync_setup_block_column (cloudsync_context * data , const char * table_name , const char * col_name , const char * delimiter , bool persist ) {
20732239 cloudsync_table_context * table = table_lookup (data , table_name );
20742240 if (!table ) return cloudsync_set_error (data , "cloudsync_setup_block_column: table not found" , DBRES_ERROR );
@@ -2148,6 +2314,13 @@ int cloudsync_setup_block_column (cloudsync_context *data, const char *table_nam
21482314 rc = dbutils_table_settings_set_key_value (data , table_name , col_name , "delimiter" , delimiter );
21492315 if (rc != DBRES_OK ) return rc ;
21502316 }
2317+
2318+ // Migrate any existing tracked rows: populate the blocks table and metadata with
2319+ // block entries derived from the current column value, so that subsequent UPDATE
2320+ // operations can diff against the real existing state instead of treating everything
2321+ // as new, and so this node participates correctly in LWW conflict resolution.
2322+ rc = block_migrate_existing_rows (data , table , col_idx );
2323+ if (rc != DBRES_OK ) return rc ;
21512324 }
21522325
21532326 return DBRES_OK ;
0 commit comments