package memory import ( "context" "database/sql" "fmt" "strings" ) // 本文件是「时间性元数据位」轴(迁移 0027)的 schema 前滚:给**既有**的 // memory_items 补 `temporal` 列。 // // 与 retention_schema.go 同一手法、同一原因:`createMemoryItemsTable` 是 // `CREATE TABLE IF NOT EXISTS`,只对**新库**生效 —— 既有部署的表早就建好了, // 新列不会自己长出来。记忆存储必须自包含、能在任意 *sql.DB 上使用,因此这条 // 前滚不能只靠 `internal/db/migrations`。 // // 🔴 与迁移的分工(两份真源,必须同形):权威机制是 // `internal/db/migrations/0027_memory_temporal`;这里是「任意 *sql.DB」的前滚。 // 启动顺序是「先 MigrateUp、后 NewStore」(cmd/cyrene/wire.go),因此生产库里 // 0027 先落地、这里走到 `duplicate column name` 是**预期路径**。 // // 本列**不建索引**:目前没有任何按时间性位的读路径(它是写入侧的判定输入 + // 事后巡检用的留痕列),提前建索引只是给每笔写入加一份没人用的维护成本。 // 将来整理侧真要按它筛(例如「把 one_off 的行捞出来复核」)再加索引不迟, // 那时两条路径(迁移 + 本文件)一起加。 // ensureTemporalSchema 幂等地给 memory_items 补上时间性位列(列缺失时 ALTER, // 已存在时忽略 SQLite 的 "duplicate column name" —— 那是预期路径,不是错误)。 // // 默认值 = 空串 = 尚未分型:既有行因此**行为逐字不变**(resolveRetention 对 // 未分型的处理与 0027 之前完全一样,只跑 N1~N7)。本前滚刻意**不做**任何回填 // 判定:历史行没有对应的提取器输出,拿正文反推「它当时是一次性事件吗」正是 // 本片要消灭的那种猜测。 func ensureTemporalSchema(ctx context.Context, db *sql.DB) error { _, err := db.ExecContext(ctx, `ALTER TABLE memory_items ADD COLUMN temporal TEXT NOT NULL DEFAULT ''`) if err == nil { return nil } if strings.Contains(err.Error(), "duplicate column name") { return nil } return fmt.Errorf("memory: roll forward temporal column: %w", err) }