- chat_sessions 增 read_at(毫秒,与 chat_messages.created_at 同单位);迁移 0010 并把既有会话回填为各自最新消息时间(升级当天不误报)
- ListSessions 单查询派生 unreadCount(无 N+1,复用 idx_chat_msgs_session_created 覆盖索引)
- MarkSessionRead 只前进 MAX(read_at, ?);未知会话 ErrSessionNotFound;游标方法挂在网关消费侧窄接口(沿用 GetMessage/messageLookup 先例,避免污染 runtime 接口与其 fake)
- POST /api/v1/sessions/{id}/read:writeAuth + 400/404/501,成功广播 session_read、失败路径不广播
- Docs/api-client v0.37(§1 unreadCount / 新行 8a / §3.9 session_read / §4.2 用法)
- 含迁移回填测试、store/handler 测试与变异验证;顺带把 migrate_pinned_test 的「回退一步=0009」硬编码改为对迁移数量免疫
62 lines
2.1 KiB
Go
62 lines
2.1 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"testing"
|
|
)
|
|
|
|
// columnExists reports whether table has a column named column.
|
|
func columnExists(t *testing.T, ctx context.Context, conn *sql.DB, table, column string) bool {
|
|
t.Helper()
|
|
var n int
|
|
if err := conn.QueryRowContext(ctx,
|
|
"SELECT count(*) FROM pragma_table_info(?) WHERE name = ?", table, column).Scan(&n); err != nil {
|
|
t.Fatalf("pragma_table_info(%s): %v", table, err)
|
|
}
|
|
return n == 1
|
|
}
|
|
|
|
// TestMigrate0009PinnedColumnIdempotent verifies the pinned migration (Android
|
|
// collaboration batch ③a / D5) is reversible and re-appliable: up adds the column,
|
|
// a second up is a no-op, down drops it, and a re-up restores it.
|
|
func TestMigrate0009PinnedColumnIdempotent(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
conn := openMemory(t)
|
|
|
|
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
|
t.Fatalf("MigrateUp: %v", err)
|
|
}
|
|
if !columnExists(t, ctx, conn, "chat_sessions", "pinned") {
|
|
t.Fatal("chat_sessions.pinned should exist after MigrateUp")
|
|
}
|
|
|
|
// Up twice: the recorded migration is skipped, so the column stays and no error.
|
|
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
|
t.Fatalf("MigrateUp second call: %v", err)
|
|
}
|
|
if !columnExists(t, ctx, conn, "chat_sessions", "pinned") {
|
|
t.Fatal("chat_sessions.pinned should still exist after idempotent re-run")
|
|
}
|
|
|
|
// Roll back every migration newer than 0008 (0009 itself plus any migration added
|
|
// after it, currently 0010), so this test keeps validating the 0009 down no matter
|
|
// how many later migrations the repo has grown.
|
|
steps := len(upMigrationIDs(t)) - 8
|
|
if err := MigrateDown(ctx, conn, steps, testLogger()); err != nil {
|
|
t.Fatalf("MigrateDown(%d): %v", steps, err)
|
|
}
|
|
if columnExists(t, ctx, conn, "chat_sessions", "pinned") {
|
|
t.Fatal("chat_sessions.pinned should be dropped by the 0009 down")
|
|
}
|
|
|
|
// Re-applying restores it end to end.
|
|
if err := MigrateUp(ctx, conn, testLogger()); err != nil {
|
|
t.Fatalf("re-MigrateUp: %v", err)
|
|
}
|
|
if !columnExists(t, ctx, conn, "chat_sessions", "pinned") {
|
|
t.Fatal("chat_sessions.pinned should exist again after re-MigrateUp")
|
|
}
|
|
}
|