|
7 | 7 | """ |
8 | 8 |
|
9 | 9 | import json |
| 10 | +import logging |
10 | 11 | import re |
11 | 12 | import uuid |
12 | 13 | from dataclasses import dataclass |
|
16 | 17 |
|
17 | 18 | from codeframe.core.workspace import Workspace, get_db_connection |
18 | 19 |
|
| 20 | +logger = logging.getLogger(__name__) |
| 21 | + |
19 | 22 |
|
20 | 23 | def _utc_now() -> datetime: |
21 | 24 | """Get current UTC time as timezone-aware datetime.""" |
@@ -322,18 +325,28 @@ def list_chains(workspace: Workspace) -> list[PrdRecord]: |
322 | 325 | conn = get_db_connection(workspace) |
323 | 326 | cursor = conn.cursor() |
324 | 327 |
|
325 | | - # Get the latest version for each unique chain_id |
| 328 | + # Get the latest version for each unique chain. |
| 329 | + # |
| 330 | + # Grouping and joining on COALESCE(chain_id, parent_id, id) rather than |
| 331 | + # chain_id alone (#961): a legacy row can still carry a NULL chain_id, and |
| 332 | + # SQL's `NULL = NULL` is never true — so the INNER JOIN silently dropped |
| 333 | + # those PRDs from the list entirely, with no error. The upgrade path |
| 334 | + # backfills them, but this keeps the read correct for anything it misses |
| 335 | + # (e.g. a child whose parent row is gone). |
326 | 336 | cursor.execute( |
327 | 337 | """ |
328 | 338 | SELECT p.id, p.workspace_id, p.title, p.content, p.metadata, p.created_at, |
329 | 339 | p.version, p.parent_id, p.change_summary, p.chain_id |
330 | 340 | FROM prds p |
331 | 341 | INNER JOIN ( |
332 | | - SELECT chain_id, MAX(version) as max_version |
| 342 | + SELECT COALESCE(chain_id, parent_id, id) AS grp, |
| 343 | + MAX(version) as max_version |
333 | 344 | FROM prds |
334 | 345 | WHERE workspace_id = ? |
335 | | - GROUP BY chain_id |
336 | | - ) latest ON p.chain_id = latest.chain_id AND p.version = latest.max_version |
| 346 | + GROUP BY COALESCE(chain_id, parent_id, id) |
| 347 | + ) latest |
| 348 | + ON COALESCE(p.chain_id, p.parent_id, p.id) = latest.grp |
| 349 | + AND p.version = latest.max_version |
337 | 350 | WHERE p.workspace_id = ? |
338 | 351 | ORDER BY p.created_at DESC |
339 | 352 | """, |
@@ -610,7 +623,19 @@ def create_new_version( |
610 | 623 | chain_id=chain_id, |
611 | 624 | ) |
612 | 625 | except Exception: |
613 | | - cursor.execute("ROLLBACK") |
| 626 | + # The rollback is best-effort cleanup, never the story (#961). A bare |
| 627 | + # cursor.execute("ROLLBACK") raises "cannot rollback - no transaction |
| 628 | + # is active" whenever the failure happened before BEGIN took effect or |
| 629 | + # after the transaction already ended — and that replacement exception |
| 630 | + # propagated instead of the real one, hiding the actual fault. |
| 631 | + try: |
| 632 | + cursor.execute("ROLLBACK") |
| 633 | + except Exception: |
| 634 | + logger.warning( |
| 635 | + "Rollback failed while unwinding create_new_version; the " |
| 636 | + "original error is re-raised.", |
| 637 | + exc_info=True, |
| 638 | + ) |
614 | 639 | raise |
615 | 640 | finally: |
616 | 641 | conn.close() |
|
0 commit comments