1111
1212_logger = logging .getLogger (__name__ )
1313
14+ from models import Bubble , ParseWarningCollector , SchemaError
1415from utils .path_helpers import get_workspace_folder_paths
1516from utils .workspace_descriptor import read_json_file
1617
@@ -34,30 +35,63 @@ def safe_fetchall(
3435 return []
3536
3637
37- def load_bubble_map (global_db : sqlite3 .Connection ) -> dict [str , dict [str , Any ]]:
38- """Load all ``bubbleId:*`` KV entries into ``{bubble_id: bubble_dict}``.
38+ def _parse_bubble_kv_row (
39+ row_key : str ,
40+ row_value : str | bytes ,
41+ * ,
42+ parse_warnings : ParseWarningCollector | None = None ,
43+ ) -> tuple [str , Bubble ] | None :
44+ """Parse one ``bubbleId:…`` row; return ``(bubble_id, Bubble)`` or skip."""
45+ parts = row_key .split (":" )
46+ if len (parts ) < 3 :
47+ return None
48+ bid = parts [2 ]
49+ try :
50+ parsed = json .loads (row_value )
51+ bubble = Bubble .from_dict (parsed , bubble_id = bid )
52+ return bid , bubble
53+ except SchemaError as exc :
54+ _logger .warning (
55+ "Schema drift in bubble %s: %s (%s)" , bid , exc , type (exc ).__name__
56+ )
57+ if parse_warnings is not None :
58+ parse_warnings .record_bubble_skipped ()
59+ except (json .JSONDecodeError , TypeError , ValueError ) as exc :
60+ if parse_warnings is not None :
61+ _logger .warning (
62+ "Failed to decode Bubble from %s: %s" , row_key , exc
63+ )
64+ parse_warnings .record_bubble_skipped ()
65+ else :
66+ _logger .debug ("Skipping malformed bubbleId row %s: %s" , row_key , exc )
67+ return None
3968
40- Skips rows whose JSON value is not a dict; JSON parse errors are logged at
41- DEBUG level so a single malformed row cannot block the rest.
69+
70+ def load_bubble_map (
71+ global_db : sqlite3 .Connection ,
72+ * ,
73+ parse_warnings : ParseWarningCollector | None = None ,
74+ ) -> dict [str , Bubble ]:
75+ """Load all ``bubbleId:*`` KV entries into ``{bubble_id: Bubble}``.
76+
77+ Uses the same :meth:`Bubble.from_dict` validation as search and tabs.
78+ When *parse_warnings* is set, skipped rows are recorded for the API.
4279 """
43- bubble_map : dict [str , dict [ str , Any ] ] = {}
80+ bubble_map : dict [str , Bubble ] = {}
4481 try :
4582 rows = global_db .execute (
46- "SELECT key, value FROM cursorDiskKV WHERE key LIKE 'bubbleId:%'"
83+ "SELECT key, value FROM cursorDiskKV"
84+ " WHERE key LIKE 'bubbleId:%' AND value IS NOT NULL"
4785 ).fetchall ()
4886 except sqlite3 .Error :
4987 return bubble_map
5088 for row in rows :
51- parts = row ["key" ].split (":" )
52- if len (parts ) < 3 :
53- continue
54- bid = parts [2 ]
55- try :
56- b = json .loads (row ["value" ])
57- if isinstance (b , dict ):
58- bubble_map [bid ] = b
59- except (json .JSONDecodeError , ValueError , KeyError , TypeError ) as e :
60- _logger .debug ("Skipping malformed bubbleId row %s: %s" , row ["key" ], e )
89+ parsed = _parse_bubble_kv_row (
90+ row ["key" ], row ["value" ], parse_warnings = parse_warnings
91+ )
92+ if parsed is not None :
93+ bid , bubble = parsed
94+ bubble_map [bid ] = bubble
6195 return bubble_map
6296
6397
@@ -163,14 +197,17 @@ def load_code_block_diff_map(global_db: sqlite3.Connection) -> dict[str, list[di
163197
164198
165199def load_bubbles_for_composer (
166- global_db : sqlite3 .Connection , composer_id : str ,
167- ) -> dict [str , dict [str , Any ]]:
168- """Load ``bubbleId:{composer_id}:*`` KV entries into ``{bubble_id: bubble_dict}``.
200+ global_db : sqlite3 .Connection ,
201+ composer_id : str ,
202+ * ,
203+ parse_warnings : ParseWarningCollector | None = None ,
204+ ) -> dict [str , Bubble ]:
205+ """Load ``bubbleId:{composer_id}:*`` KV entries into ``{bubble_id: Bubble}``.
169206
170207 Scoped alternative to :func:`load_bubble_map` for single-conversation assembly;
171208 avoids a full global ``bubbleId:%`` scan.
172209 """
173- bubble_map : dict [str , dict [ str , Any ] ] = {}
210+ bubble_map : dict [str , Bubble ] = {}
174211 try :
175212 rows = global_db .execute (
176213 "SELECT key, value FROM cursorDiskKV WHERE key LIKE ?" ,
@@ -179,16 +216,12 @@ def load_bubbles_for_composer(
179216 except sqlite3 .Error :
180217 return bubble_map
181218 for row in rows :
182- parts = row ["key" ].split (":" )
183- if len (parts ) < 3 :
184- continue
185- bid = parts [2 ]
186- try :
187- b = json .loads (row ["value" ])
188- if isinstance (b , dict ):
189- bubble_map [bid ] = b
190- except (json .JSONDecodeError , ValueError , KeyError , TypeError ) as e :
191- _logger .debug ("Skipping malformed bubbleId row %s: %s" , row ["key" ], e )
219+ parsed = _parse_bubble_kv_row (
220+ row ["key" ], row ["value" ], parse_warnings = parse_warnings
221+ )
222+ if parsed is not None :
223+ bid , bubble = parsed
224+ bubble_map [bid ] = bubble
192225 return bubble_map
193226
194227
0 commit comments