Fix bugs in snapshot stripping: auto-note, mass update, DB-off, fallback
- _auto_change_note now loads previous snapshot from DB instead of reading stripped in-memory node (was producing wrong commit messages) - _render_mass_update now strips snapshots after save (was leaking RAM) - Only strip snapshots when DB is enabled (preview/restore still works without DB via in-memory or disk fallback) - _render_data_preview adds disk fallback matching _restore_node Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+18
-9
@@ -86,14 +86,19 @@ def find_insert_position(batch_list, parent_index, parent_seq_num):
|
|||||||
|
|
||||||
# --- Auto change note ---
|
# --- Auto change note ---
|
||||||
|
|
||||||
def _auto_change_note(htree, batch_list):
|
def _auto_change_note(htree, batch_list, state=None, file_path=None):
|
||||||
"""Compare current batch_list against last snapshot and describe changes."""
|
"""Compare current batch_list against last snapshot and describe changes."""
|
||||||
# Get previous batch data from the current head
|
# Get previous batch data from the current head
|
||||||
if not htree.head_id or htree.head_id not in htree.nodes:
|
if not htree.head_id or htree.head_id not in htree.nodes:
|
||||||
return f'Initial save ({len(batch_list)} sequences)'
|
return f'Initial save ({len(batch_list)} sequences)'
|
||||||
|
|
||||||
prev_data = htree.nodes[htree.head_id].get('data', {})
|
# Load previous snapshot from DB (nodes no longer hold data in memory)
|
||||||
prev_batch = prev_data.get(KEY_BATCH_DATA, [])
|
prev_data = htree.nodes[htree.head_id].get('data')
|
||||||
|
if not prev_data and state and state.db_enabled and state.db and state.current_project and file_path:
|
||||||
|
df = state.db.get_data_file_by_names(state.current_project, file_path.stem)
|
||||||
|
if df:
|
||||||
|
prev_data = state.db.get_node_snapshot(df['id'], htree.head_id)
|
||||||
|
prev_batch = (prev_data or {}).get(KEY_BATCH_DATA, [])
|
||||||
|
|
||||||
prev_by_seq = {int(s.get(KEY_SEQUENCE_NUMBER, 0)): s for s in prev_batch}
|
prev_by_seq = {int(s.get(KEY_SEQUENCE_NUMBER, 0)): s for s in prev_batch}
|
||||||
curr_by_seq = {int(s.get(KEY_SEQUENCE_NUMBER, 0)): s for s in batch_list}
|
curr_by_seq = {int(s.get(KEY_SEQUENCE_NUMBER, 0)): s for s in batch_list}
|
||||||
@@ -359,7 +364,7 @@ def render_batch_processor(state: AppState):
|
|||||||
data[KEY_BATCH_DATA] = batch_list
|
data[KEY_BATCH_DATA] = batch_list
|
||||||
tree_data = data.get(KEY_HISTORY_TREE, {})
|
tree_data = data.get(KEY_HISTORY_TREE, {})
|
||||||
htree = HistoryTree(tree_data)
|
htree = HistoryTree(tree_data)
|
||||||
note = commit_input.value if commit_input.value else _auto_change_note(htree, batch_list)
|
note = commit_input.value if commit_input.value else _auto_change_note(htree, batch_list, state=state, file_path=file_path)
|
||||||
# Single serialization: json roundtrip gives us an isolated snapshot
|
# Single serialization: json roundtrip gives us an isolated snapshot
|
||||||
# without the expensive deepcopy
|
# without the expensive deepcopy
|
||||||
t1 = time.perf_counter()
|
t1 = time.perf_counter()
|
||||||
@@ -381,9 +386,9 @@ def render_batch_processor(state: AppState):
|
|||||||
t1 = time.perf_counter()
|
t1 = time.perf_counter()
|
||||||
await asyncio.to_thread(sync_to_db, state.db, state.current_project, file_path, save_snapshot)
|
await asyncio.to_thread(sync_to_db, state.db, state.current_project, file_path, save_snapshot)
|
||||||
logger.info("save_and_snap sync_to_db %.3fs", time.perf_counter() - t1)
|
logger.info("save_and_snap sync_to_db %.3fs", time.perf_counter() - t1)
|
||||||
# Free snapshot data from memory — it's persisted in DB now
|
# Free snapshot data from memory — it's persisted in DB now
|
||||||
htree.strip_snapshots()
|
htree.strip_snapshots()
|
||||||
data[KEY_HISTORY_TREE] = htree.to_dict()
|
data[KEY_HISTORY_TREE] = htree.to_dict()
|
||||||
state.restored_indicator = None
|
state.restored_indicator = None
|
||||||
commit_input.set_value('')
|
commit_input.set_value('')
|
||||||
logger.info("save_and_snap END (%.3fs)", time.perf_counter() - t_ss)
|
logger.info("save_and_snap END (%.3fs)", time.perf_counter() - t_ss)
|
||||||
@@ -843,8 +848,9 @@ def _render_mass_update(batch_list, data, file_path, state: AppState, refresh_li
|
|||||||
|
|
||||||
data[KEY_BATCH_DATA] = batch_list
|
data[KEY_BATCH_DATA] = batch_list
|
||||||
htree = HistoryTree(data.get(KEY_HISTORY_TREE, {}))
|
htree = HistoryTree(data.get(KEY_HISTORY_TREE, {}))
|
||||||
snapshot = {k: copy.deepcopy(v) for k, v in data.items()
|
snapshot_json = json.dumps({k: v for k, v in data.items()
|
||||||
if k != KEY_HISTORY_TREE}
|
if k != KEY_HISTORY_TREE})
|
||||||
|
snapshot = json.loads(snapshot_json)
|
||||||
try:
|
try:
|
||||||
htree.commit(snapshot, f"Mass update: {', '.join(selected_keys)}")
|
htree.commit(snapshot, f"Mass update: {', '.join(selected_keys)}")
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@@ -855,6 +861,9 @@ def _render_mass_update(batch_list, data, file_path, state: AppState, refresh_li
|
|||||||
await asyncio.to_thread(save_json, file_path, save_snapshot)
|
await asyncio.to_thread(save_json, file_path, save_snapshot)
|
||||||
if state.db_enabled and state.current_project and state.db:
|
if state.db_enabled and state.current_project and state.db:
|
||||||
await asyncio.to_thread(sync_to_db, state.db, state.current_project, file_path, save_snapshot)
|
await asyncio.to_thread(sync_to_db, state.db, state.current_project, file_path, save_snapshot)
|
||||||
|
# Free snapshot data from memory after persisting
|
||||||
|
htree.strip_snapshots()
|
||||||
|
data[KEY_HISTORY_TREE] = htree.to_dict()
|
||||||
ui.notify(f'Updated {len(targets)} sequences', type='positive')
|
ui.notify(f'Updated {len(targets)} sequences', type='positive')
|
||||||
if refresh_list:
|
if refresh_list:
|
||||||
refresh_list.refresh()
|
refresh_list.refresh()
|
||||||
|
|||||||
@@ -619,6 +619,14 @@ def _render_data_preview(nid, htree, state: AppState = None, file_path=None):
|
|||||||
df = state.db.get_data_file_by_names(state.current_project, file_path.stem)
|
df = state.db.get_data_file_by_names(state.current_project, file_path.stem)
|
||||||
if df:
|
if df:
|
||||||
node_data = state.db.get_node_snapshot(df['id'], nid)
|
node_data = state.db.get_node_snapshot(df['id'], nid)
|
||||||
|
if not node_data and file_path:
|
||||||
|
# Disk fallback: read snapshot from JSON file
|
||||||
|
try:
|
||||||
|
raw_data, _ = load_json(file_path)
|
||||||
|
tree_on_disk = raw_data.get(KEY_HISTORY_TREE, {})
|
||||||
|
node_data = tree_on_disk.get('nodes', {}).get(nid, {}).get('data')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
if not node_data:
|
if not node_data:
|
||||||
ui.label('Snapshot data not available.').classes('text-caption text-warning')
|
ui.label('Snapshot data not available.').classes('text-caption text-warning')
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user