feat(todos): 修改 todos 表结构,迁移至复合主键并添加索引
This commit is contained in:
parent
66e40fc714
commit
edc1a50d1c
@ -1493,7 +1493,7 @@ impl SessionStore {
|
||||
// Insert new todos
|
||||
for item in items {
|
||||
conn.execute(
|
||||
"INSERT INTO todos (id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at)
|
||||
"INSERT OR REPLACE INTO todos (id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![
|
||||
item.id,
|
||||
@ -1904,7 +1904,7 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
|
||||
conn.execute_batch(
|
||||
"
|
||||
CREATE TABLE IF NOT EXISTS todos (
|
||||
id TEXT PRIMARY KEY,
|
||||
id TEXT NOT NULL,
|
||||
scope_key TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
topic_id TEXT,
|
||||
@ -1912,7 +1912,8 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
priority TEXT NOT NULL DEFAULT 'medium',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (id, scope_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_todos_scope
|
||||
@ -1922,6 +1923,55 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
|
||||
ON todos(session_id);
|
||||
",
|
||||
)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Migration: check if old schema has single-column PRIMARY KEY on `id`
|
||||
// If so, migrate to composite PRIMARY KEY (id, scope_key)
|
||||
let sql: String = conn
|
||||
.query_row(
|
||||
"SELECT sql FROM sqlite_master WHERE type='table' AND name='todos'",
|
||||
[],
|
||||
|row| row.get::<_, String>(0),
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
let needs_migration = sql.contains("id TEXT PRIMARY KEY")
|
||||
|| (sql.contains("PRIMARY KEY") && !sql.contains("PRIMARY KEY (id, scope_key)"));
|
||||
|
||||
if needs_migration {
|
||||
tracing::info!("Migrating todos table to composite PRIMARY KEY (id, scope_key)");
|
||||
conn.execute_batch(
|
||||
"
|
||||
CREATE TABLE todos_new (
|
||||
id TEXT NOT NULL,
|
||||
scope_key TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL,
|
||||
topic_id TEXT,
|
||||
content TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
priority TEXT NOT NULL DEFAULT 'medium',
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (id, scope_key)
|
||||
);
|
||||
|
||||
INSERT OR IGNORE INTO todos_new
|
||||
SELECT id, scope_key, session_id, topic_id, content, status, priority, created_at, updated_at
|
||||
FROM todos;
|
||||
|
||||
DROP TABLE todos;
|
||||
|
||||
ALTER TABLE todos_new RENAME TO todos;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_todos_scope
|
||||
ON todos(scope_key, created_at ASC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_todos_session
|
||||
ON todos(session_id);
|
||||
",
|
||||
)?;
|
||||
tracing::info!("Todos table migration complete");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -165,6 +165,11 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) {
|
||||
const [toast, setToast] = useState('')
|
||||
const [dirty, setDirty] = useState(false)
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
if (dirty && !confirm('有未保存的更改,确定要关闭吗?')) return
|
||||
onClose()
|
||||
}, [dirty, onClose])
|
||||
|
||||
// Load config
|
||||
useEffect(() => {
|
||||
fetch('/api/config').then(r => r.json()).then(data => {
|
||||
@ -175,10 +180,10 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) {
|
||||
|
||||
// ESC to close
|
||||
useEffect(() => {
|
||||
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
||||
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') handleClose() }
|
||||
document.addEventListener('keydown', h)
|
||||
return () => document.removeEventListener('keydown', h)
|
||||
}, [onClose])
|
||||
}, [handleClose])
|
||||
|
||||
const update = useCallback(<K extends keyof AppConfig>(key: K, value: AppConfig[K]) => {
|
||||
setConfig(prev => prev ? { ...prev, [key]: value } : prev)
|
||||
@ -589,7 +594,7 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-[fadeIn_0.15s_ease-out]" onClick={onClose}>
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm animate-[fadeIn_0.15s_ease-out]" onClick={handleClose}>
|
||||
<div
|
||||
className="relative flex flex-col w-[92vw] max-w-4xl h-[85vh] rounded-2xl border border-[var(--border-color)] bg-[var(--bg-primary)] shadow-2xl overflow-hidden animate-[scaleIn_0.2s_ease-out]"
|
||||
onClick={e => e.stopPropagation()}
|
||||
@ -600,7 +605,7 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) {
|
||||
<span className="text-lg font-semibold text-[var(--text-primary)]">系统配置</span>
|
||||
{dirty && <span className="text-xs text-amber-400 bg-amber-500/10 px-2 py-0.5 rounded-full">未保存</span>}
|
||||
<div className="flex-1" />
|
||||
<button onClick={onClose} className="p-2 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors" title="关闭 (Esc)">
|
||||
<button onClick={handleClose} className="p-2 rounded-lg text-[var(--text-muted)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors" title="关闭 (Esc)">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
@ -640,7 +645,7 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) {
|
||||
<div className="shrink-0 px-6 py-3 border-t border-[var(--border-color)] bg-[var(--bg-secondary)]/80 backdrop-blur-md flex items-center gap-3">
|
||||
{error && <span className="text-sm text-red-400 truncate max-w-xs">{error}</span>}
|
||||
<div className="flex-1" />
|
||||
<button onClick={onClose} className="px-4 py-2 rounded-lg text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors">
|
||||
<button onClick={handleClose} className="px-4 py-2 rounded-lg text-sm text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--overlay-hover)] transition-colors">
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user