database id fixes

This commit is contained in:
Joe Fleming
2026-03-08 14:22:39 -06:00
parent d04af8d6dc
commit c768caa025
2 changed files with 114 additions and 43 deletions

37
app.py
View File

@@ -7,7 +7,7 @@ from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical
from textual.widgets import Header, Footer, Input, Button, Select, ListView, ListItem, DataTable, Label, Static
from textual.screen import ModalScreen
from typing import List, Optional
from typing import List, Optional, cast
from database import Database, Session
from datetime import datetime
import os
@@ -31,7 +31,8 @@ class MoveCopyDialog(ModalScreen):
def compose(self) -> ComposeResult:
# Get currently selected project from tree to exclude from destination list
current_project_id = self.app.filter_project_id if hasattr(self.app, 'filter_project_id') else None
main_app = cast("SessionMoverApp", self.app)
current_project_id = main_app.filter_project_id if hasattr(main_app, 'filter_project_id') else None
project_options = sorted(
[
@@ -68,7 +69,7 @@ class MoveCopyDialog(ModalScreen):
yield Button("Confirm", variant="primary", id="confirm-btn", disabled=True)
def on_mount(self) -> None:
self.query_one("#dialog").border_title = f"{'Copy' if self.is_copy else 'Move'} Sessions"
self.query_one("#dialog", Container).border_title = f"{'Copy' if self.is_copy else 'Move'} Sessions"
def on_select_changed(self, event):
if event.select.id == "project-select":
@@ -77,12 +78,12 @@ class MoveCopyDialog(ModalScreen):
self.target_workspace = event.value if event.value else None
self.update_preview()
confirm_btn = self.query_one("#confirm-btn")
confirm_btn = self.query_one("#confirm-btn", Button)
confirm_btn.disabled = not (self.target_project is not None)
def update_preview(self):
if not self.target_project:
preview = self.query_one("#sql-preview")
preview = self.query_one("#sql-preview", Static)
preview.update("Select a project to see preview")
return
@@ -111,7 +112,7 @@ class MoveCopyDialog(ModalScreen):
if self.is_copy:
lines.append("\nNote: Copy creates new sessions with all related data.")
self.query_one("#sql-preview").update("\n".join(lines))
self.query_one("#sql-preview", Static).update("\n".join(lines))
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "cancel-btn":
@@ -141,13 +142,11 @@ class MoveCopyDialog(ModalScreen):
)
if success:
self.app.notify(f"{'Copied' if self.is_copy else 'Moved'} {len(self.sessions)} session(s) successfully!", severity="information")
self.app.pop_screen()
# Get reference to main app and refresh
app = self.app
if hasattr(app, 'refresh_sessions'):
app.refresh_project_list()
app.refresh_sessions()
main_app = cast("SessionMoverApp", self.app)
main_app.notify(f"{'Copied' if self.is_copy else 'Moved'} {len(self.sessions)} session(s) successfully!", severity="information")
main_app.pop_screen()
main_app.refresh_project_list()
main_app.refresh_sessions()
else:
self.app.notify(f"Operation failed: {sql}", severity="error")
@@ -194,7 +193,7 @@ class DeleteConfirmDialog(ModalScreen):
yield Button("Delete", variant="error", id="confirm-btn")
def on_mount(self) -> None:
self.query_one("#dialog").border_title = "Confirm Delete"
self.query_one("#dialog", Container).border_title = "Confirm Delete"
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "cancel-btn":
@@ -470,8 +469,8 @@ class SessionMoverApp(App):
def refresh_sessions(self) -> None:
"""Update the sessions table."""
table = self.query_one("#sessions-table")
table = self.query_one("#sessions-table", DataTable)
# Save cursor position before clearing
cursor_row = table.cursor_row
cursor_column = table.cursor_column
@@ -509,7 +508,7 @@ class SessionMoverApp(App):
# Update selection count
count = len(self.selected_session_ids)
count_widget = self.query_one("#selection-count")
count_widget = self.query_one("#selection-count", Static)
if count > 0:
count_widget.update(f"{count} session(s) selected")
else:
@@ -542,7 +541,7 @@ class SessionMoverApp(App):
self.filter_project_id = proj_id
self.filter_workspace_id = None
status = self.query_one("#current-selection")
status = self.query_one("#current-selection", Static)
if proj:
status.update(f"📁 {proj.worktree}")
@@ -555,7 +554,7 @@ class SessionMoverApp(App):
def action_toggle_selection(self) -> None:
"""Toggle selection of current row."""
table = self.query_one("#sessions-table")
table = self.query_one("#sessions-table", DataTable)
row_key = table.cursor_row
if row_key is not None:
sessions = self.db.get_sessions(

View File

@@ -217,11 +217,43 @@ class Database:
def get_session(self, session_id: str) -> Optional[Session]:
"""Get a single session by ID."""
sessions = self.get_sessions(include_archived=True)
for s in sessions:
if s.id == session_id:
return s
return None
if self.conn is None:
raise RuntimeError("Database is not connected. Call connect() first.")
cursor = self.conn.cursor()
cursor.execute("SELECT * FROM session WHERE id = ?", (session_id,))
row = cursor.fetchone()
if not row:
return None
sess = Session(
id=row["id"],
project_id=row["project_id"],
workspace_id=row["workspace_id"],
parent_id=row["parent_id"],
slug=row["slug"],
directory=row["directory"],
title=row["title"],
version=row["version"],
time_created=row["time_created"],
time_updated=row["time_updated"],
time_archived=row["time_archived"],
)
# Message count
cursor.execute("SELECT COUNT(*) FROM message WHERE session_id = ?", (session_id,))
sess.message_count = cursor.fetchone()[0]
# Project name
if sess.project_id in self.projects:
proj = self.projects[sess.project_id]
sess.project_name = proj.name or proj.worktree
# Workspace name
if sess.workspace_id and sess.workspace_id in self.workspaces:
ws = self.workspaces[sess.workspace_id]
sess.workspace_name = ws.name or ws.branch or ws.id
return sess
def move_sessions(self, session_ids: List[str], target_project_id: str,
target_workspace_id: Optional[str] = None) -> Tuple[bool, List[str]]:
@@ -297,9 +329,7 @@ class Database:
continue
# Create new session ID
new_id = str(uuid.uuid4()).replace("-", "")
while new_id[:3] != "ses":
new_id = "ses_" + new_id
new_id = "ses_" + uuid.uuid4().hex
# Insert new session
cols = [k for k in row.keys() if k != "id"]
@@ -325,28 +355,70 @@ class Database:
cursor.execute(sql, [new_id] + values)
sql_statements.append(f"INSERT INTO session ... VALUES ({new_id}, ...)")
# Copy related records
for table, foreign_key in [("message", "session_id"), ("part", "session_id"), ("todo", "session_id"), ("session_share", "session_id")]:
try:
cursor.execute(f"SELECT * FROM {table} WHERE {foreign_key} = ?", (sess_id,))
except sqlite3.OperationalError:
continue # Table doesn't exist in this schema version
rows = cursor.fetchall()
for r in rows:
# Include foreign_key in cols so the new session_id is written
# Copy messages first, building old_id -> new_id map so parts can be relinked
msg_id_map: dict = {}
try:
cursor.execute("SELECT * FROM message WHERE session_id = ?", (sess_id,))
except sqlite3.OperationalError:
pass
else:
for r in cursor.fetchall():
new_msg_id = uuid.uuid4().hex
msg_id_map[r["id"]] = new_msg_id
cols = [k for k in r.keys() if k != "id"]
values = [new_id if k == foreign_key else r[k] for k in cols]
values = [new_id if k == "session_id" else r[k] for k in cols]
col_list = ", ".join(cols)
placeholders = ", ".join(["?"] * len(cols))
cursor.execute(
f"INSERT INTO message (id, {col_list}) VALUES (?, {placeholders})",
[new_msg_id] + values,
)
# Generate new ID for tables with id column
# Copy parts, relinking both session_id and message_id
try:
cursor.execute("SELECT * FROM part WHERE session_id = ?", (sess_id,))
except sqlite3.OperationalError:
pass
else:
for r in cursor.fetchall():
new_part_id = uuid.uuid4().hex
cols = [k for k in r.keys() if k != "id"]
values = []
for k in cols:
if k == "session_id":
values.append(new_id)
elif k == "message_id":
values.append(msg_id_map.get(r[k], r[k]))
else:
values.append(r[k])
col_list = ", ".join(cols)
placeholders = ", ".join(["?"] * len(cols))
cursor.execute(
f"INSERT INTO part (id, {col_list}) VALUES (?, {placeholders})",
[new_part_id] + values,
)
# Copy todo and session_share (session_id only, no secondary FKs)
for table in ("todo", "session_share"):
try:
cursor.execute(f"SELECT * FROM {table} WHERE session_id = ?", (sess_id,))
except sqlite3.OperationalError:
continue
for r in cursor.fetchall():
cols = [k for k in r.keys() if k != "id"]
values = [new_id if k == "session_id" else r[k] for k in cols]
col_list = ", ".join(cols)
placeholders = ", ".join(["?"] * len(cols))
if "id" in r.keys():
new_table_id = str(uuid.uuid4()).replace("-", "")
sql = f"INSERT INTO {table} (id, {col_list}) VALUES (?, {placeholders})"
cursor.execute(sql, [new_table_id] + values)
cursor.execute(
f"INSERT INTO {table} (id, {col_list}) VALUES (?, {placeholders})",
[uuid.uuid4().hex] + values,
)
else:
sql = f"INSERT INTO {table} ({col_list}) VALUES ({placeholders})"
cursor.execute(sql, values)
cursor.execute(
f"INSERT INTO {table} ({col_list}) VALUES ({placeholders})",
values,
)
new_session_ids.append(new_id)