@@ -1177,8 +1177,7 @@ byte[] SerializeFullRow()
11771177 {
11781178 var rowData = SerializeFullRow ( ) ;
11791179
1180- // Columnar: Append new version (old ref becomes stale)
1181- // Get old position from primary key index
1180+ // Get old position from primary key index.
11821181 long oldPosition = - 1 ;
11831182 if ( this . PrimaryKeyIndex >= 0 )
11841183 {
@@ -1190,28 +1189,65 @@ byte[] SerializeFullRow()
11901189 }
11911190 }
11921191
1193- // Insert new version
1194- long newPosition = engine . Insert ( Name , rowData ) ;
1195-
1196- // Update indexes to point to new position
1197- if ( this . PrimaryKeyIndex >= 0 )
1192+ // Issue #6: in-place UPDATE — overwrite the record in its existing slot when
1193+ // the new record fits (fixed-width rows, or variable-width rows whose stored
1194+ // length is unchanged). No new version is appended, the storage reference and
1195+ // the PK index stay valid, and no stale version is left for compaction.
1196+ if ( oldPosition >= 0 && engine . TryUpdateInPlace ( Name , oldPosition , rowData ) )
11981197 {
1199- var pkVal = row [ this . Columns [ this . PrimaryKeyIndex ] ] ? . ToString ( ) ?? string . Empty ;
1200- this . Index . Insert ( pkVal , newPosition ) ;
1201- }
1198+ // Position unchanged: move hash entries in place (values may have changed).
1199+ foreach ( var kvp in this . hashIndexes )
1200+ {
1201+ if ( oldHashKeys != null && oldHashKeys . TryGetValue ( kvp . Key , out var oldKey ) )
1202+ {
1203+ kvp . Value . Remove ( oldKey , oldPosition ) ;
1204+ }
12021205
1203- // Update hash indexes (key-only removal of the old value)
1204- foreach ( var kvp in this . hashIndexes )
1205- {
1206- if ( oldPosition >= 0 && oldHashKeys != null && oldHashKeys . TryGetValue ( kvp . Key , out var oldKey ) )
1206+ kvp . Value . Add ( row , oldPosition ) ;
1207+ }
1208+
1209+ // Re-point the PK index only when the PK value itself changed.
1210+ if ( this . PrimaryKeyIndex >= 0 )
12071211 {
1208- kvp . Value . Remove ( oldKey , oldPosition ) ; // Remove old ref
1212+ var newPkVal = row [ this . Columns [ this . PrimaryKeyIndex ] ] ? . ToString ( ) ?? string . Empty ;
1213+ if ( ! string . Equals ( newPkVal , oldPkValue , StringComparison . Ordinal ) )
1214+ {
1215+ if ( ! string . IsNullOrEmpty ( oldPkValue ) )
1216+ {
1217+ this . Index . Delete ( oldPkValue ) ;
1218+ }
1219+
1220+ if ( ! string . IsNullOrEmpty ( newPkVal ) )
1221+ {
1222+ this . Index . Insert ( newPkVal , oldPosition ) ;
1223+ }
1224+ }
12091225 }
1210- kvp . Value . Add ( row , newPosition ) ; // Add new ref
12111226 }
1227+ else
1228+ {
1229+ // Columnar fallback: append new version (old ref becomes stale) + re-point indexes.
1230+ long newPosition = engine . Insert ( Name , rowData ) ;
12121231
1213- // ✅ NEW: Track updates for compaction
1214- Interlocked . Increment ( ref _updatedRowCount ) ;
1232+ if ( this . PrimaryKeyIndex >= 0 )
1233+ {
1234+ var pkVal = row [ this . Columns [ this . PrimaryKeyIndex ] ] ? . ToString ( ) ?? string . Empty ;
1235+ this . Index . Insert ( pkVal , newPosition ) ;
1236+ }
1237+
1238+ foreach ( var kvp in this . hashIndexes )
1239+ {
1240+ if ( oldPosition >= 0 && oldHashKeys != null && oldHashKeys . TryGetValue ( kvp . Key , out var oldKey ) )
1241+ {
1242+ kvp . Value . Remove ( oldKey , oldPosition ) ; // Remove old ref
1243+ }
1244+
1245+ kvp . Value . Add ( row , newPosition ) ; // Add new ref
1246+ }
1247+
1248+ // ✅ Track updates for compaction (only the append path creates stale versions).
1249+ Interlocked . Increment ( ref _updatedRowCount ) ;
1250+ }
12151251 }
12161252 else // PageBased
12171253 {
@@ -1436,27 +1472,62 @@ internal void UpdateMultiple(List<(string where, Dictionary<string, object> upda
14361472 oldPosition = searchResult . Value ;
14371473 }
14381474
1439- long newPosition = engine . Insert ( Name , rowData ) ;
1440-
1441- if ( this . PrimaryKeyIndex >= 0 )
1475+ // Issue #6: in-place UPDATE — overwrite the record in its existing slot
1476+ // when the new record fits; the storage reference and PK index stay valid.
1477+ if ( oldPosition >= 0 && engine . TryUpdateInPlace ( Name , oldPosition , rowData ) )
14421478 {
1443- var pkVal = row [ this . Columns [ this . PrimaryKeyIndex ] ] ? . ToString ( ) ?? string . Empty ;
1444- this . Index . Delete ( pkVal ) ;
1445- this . Index . Insert ( pkVal , newPosition ) ;
1446- }
1479+ // Position unchanged: move hash entries in place (values may have changed).
1480+ foreach ( var hashIndex in this . hashIndexes )
1481+ {
1482+ if ( oldPosition >= 0 &&
1483+ oldHashValues is not null &&
1484+ oldHashValues . TryGetValue ( hashIndex . Key , out var oldKey ) &&
1485+ oldKey is not null )
1486+ {
1487+ hashIndex . Value . Remove ( oldKey , oldPosition ) ;
1488+ }
1489+
1490+ if ( row . TryGetValue ( hashIndex . Key , out var newKey ) && newKey is not null )
1491+ hashIndex . Value . Add ( newKey , oldPosition ) ;
1492+ }
14471493
1448- foreach ( var hashIndex in this . hashIndexes )
1494+ // Re-point the PK index only when the PK value itself changed.
1495+ if ( this . PrimaryKeyIndex >= 0 )
1496+ {
1497+ var newPkVal = row [ this . Columns [ this . PrimaryKeyIndex ] ] ? . ToString ( ) ?? string . Empty ;
1498+ if ( ! string . Equals ( newPkVal , oldPkValue ? . ToString ( ) , StringComparison . Ordinal ) )
1499+ {
1500+ if ( ! string . IsNullOrEmpty ( oldPkValue ? . ToString ( ) ) )
1501+ this . Index . Delete ( oldPkValue ! . ToString ( ) ! ) ;
1502+ if ( ! string . IsNullOrEmpty ( newPkVal ) )
1503+ this . Index . Insert ( newPkVal , oldPosition ) ;
1504+ }
1505+ }
1506+ }
1507+ else
14491508 {
1450- if ( oldPosition >= 0 &&
1451- oldHashValues is not null &&
1452- oldHashValues . TryGetValue ( hashIndex . Key , out var oldKey ) &&
1453- oldKey is not null )
1509+ long newPosition = engine . Insert ( Name , rowData ) ;
1510+
1511+ if ( this . PrimaryKeyIndex >= 0 )
14541512 {
1455- hashIndex . Value . Remove ( oldKey , oldPosition ) ;
1513+ var pkVal = row [ this . Columns [ this . PrimaryKeyIndex ] ] ? . ToString ( ) ?? string . Empty ;
1514+ this . Index . Delete ( pkVal ) ;
1515+ this . Index . Insert ( pkVal , newPosition ) ;
14561516 }
14571517
1458- if ( row . TryGetValue ( hashIndex . Key , out var newKey ) && newKey is not null )
1459- hashIndex . Value . Add ( newKey , newPosition ) ;
1518+ foreach ( var hashIndex in this . hashIndexes )
1519+ {
1520+ if ( oldPosition >= 0 &&
1521+ oldHashValues is not null &&
1522+ oldHashValues . TryGetValue ( hashIndex . Key , out var oldKey ) &&
1523+ oldKey is not null )
1524+ {
1525+ hashIndex . Value . Remove ( oldKey , oldPosition ) ;
1526+ }
1527+
1528+ if ( row . TryGetValue ( hashIndex . Key , out var newKey ) && newKey is not null )
1529+ hashIndex . Value . Add ( newKey , newPosition ) ;
1530+ }
14601531 }
14611532
14621533 updatedInBatch ++ ;
0 commit comments