feat(todos): 修改 todos 表结构,迁移至复合主键并添加索引
This commit is contained in:
parent
66e40fc714
commit
edc1a50d1c
@ -1493,7 +1493,7 @@ impl SessionStore {
|
|||||||
// Insert new todos
|
// Insert new todos
|
||||||
for item in items {
|
for item in items {
|
||||||
conn.execute(
|
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)",
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||||
params![
|
params![
|
||||||
item.id,
|
item.id,
|
||||||
@ -1904,7 +1904,7 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
|
|||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
"
|
"
|
||||||
CREATE TABLE IF NOT EXISTS todos (
|
CREATE TABLE IF NOT EXISTS todos (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT NOT NULL,
|
||||||
scope_key TEXT NOT NULL,
|
scope_key TEXT NOT NULL,
|
||||||
session_id TEXT NOT NULL,
|
session_id TEXT NOT NULL,
|
||||||
topic_id TEXT,
|
topic_id TEXT,
|
||||||
@ -1912,7 +1912,8 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
|
|||||||
status TEXT NOT NULL DEFAULT 'pending',
|
status TEXT NOT NULL DEFAULT 'pending',
|
||||||
priority TEXT NOT NULL DEFAULT 'medium',
|
priority TEXT NOT NULL DEFAULT 'medium',
|
||||||
created_at INTEGER NOT NULL,
|
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
|
CREATE INDEX IF NOT EXISTS idx_todos_scope
|
||||||
@ -1922,6 +1923,55 @@ fn ensure_todos_schema(conn: &Connection) -> Result<(), StorageError> {
|
|||||||
ON todos(session_id);
|
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(())
|
Ok(())
|
||||||
|
|||||||
@ -165,6 +165,11 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) {
|
|||||||
const [toast, setToast] = useState('')
|
const [toast, setToast] = useState('')
|
||||||
const [dirty, setDirty] = useState(false)
|
const [dirty, setDirty] = useState(false)
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
if (dirty && !confirm('有未保存的更改,确定要关闭吗?')) return
|
||||||
|
onClose()
|
||||||
|
}, [dirty, onClose])
|
||||||
|
|
||||||
// Load config
|
// Load config
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/config').then(r => r.json()).then(data => {
|
fetch('/api/config').then(r => r.json()).then(data => {
|
||||||
@ -175,10 +180,10 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) {
|
|||||||
|
|
||||||
// ESC to close
|
// ESC to close
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
|
const h = (e: KeyboardEvent) => { if (e.key === 'Escape') handleClose() }
|
||||||
document.addEventListener('keydown', h)
|
document.addEventListener('keydown', h)
|
||||||
return () => document.removeEventListener('keydown', h)
|
return () => document.removeEventListener('keydown', h)
|
||||||
}, [onClose])
|
}, [handleClose])
|
||||||
|
|
||||||
const update = useCallback(<K extends keyof AppConfig>(key: K, value: AppConfig[K]) => {
|
const update = useCallback(<K extends keyof AppConfig>(key: K, value: AppConfig[K]) => {
|
||||||
setConfig(prev => prev ? { ...prev, [key]: value } : prev)
|
setConfig(prev => prev ? { ...prev, [key]: value } : prev)
|
||||||
@ -589,7 +594,7 @@ export function ConfigPage({ onClose, onSaveConnection }: ConfigPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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
|
<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]"
|
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()}
|
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>
|
<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>}
|
{dirty && <span className="text-xs text-amber-400 bg-amber-500/10 px-2 py-0.5 rounded-full">未保存</span>}
|
||||||
<div className="flex-1" />
|
<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" />
|
<X className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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">
|
<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>}
|
{error && <span className="text-sm text-red-400 truncate max-w-xs">{error}</span>}
|
||||||
<div className="flex-1" />
|
<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>
|
||||||
<button
|
<button
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user