Files
AskaEth 61cf80d5ee
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
docs(comments): 注释中文化 · internal/runtime/memory(31 文件,纯替换)
- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后,
  逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致
- 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文
- 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
2026-10-04 13:32:54 +08:00

136 lines
4.7 KiB
Go

package memory
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
)
// timeLayout 是每个已存 created_at/updated_at 值使用的定宽
// 时间戳布局。与 time.RFC3339Nano 不同,它从不丢弃末尾
// 小数零,因此每个值恰好 storedTimeLen 字节宽。SQLite
// 把这些 TEXT 列按普通字符串比较,而等宽正是让
// 该比较与时间顺序一致的原因。UTC 值把时区渲染为
// 单个 "Z"。
const timeLayout = "2006-01-02T15:04:05.000000000Z07:00"
// storedTimeLen 是用 timeLayout 格式化的 UTC 值的字节长度:
// "2006-01-02T15:04:05.000000000Z" —— Z07:00 时区在 UTC 下坍缩为 "Z"。
const storedTimeLen = 30
// memoryItemsTable 是存储打开时需要规范化时间戳的表。
// 它必须与 store.go 中的 DDL 一致。
const memoryItemsTable = "memory_items"
// formatTime 用定宽 timeLayout 把 t 编码为 UTC。始终写
// UTC 使排序与往返在各进程间保持稳定,而定宽
// 正是 ORDER BY created_at 能按时间排序这些 TEXT 列的原因。
func formatTime(t time.Time) string {
return t.UTC().Format(timeLayout)
}
// parseTime 解码一个已存时间戳。time.RFC3339Nano 同时接受
// 本包写入的定宽值与旧的变宽值,因此同一个解析器既服务
// 读取、也服务打开时的规范化。空输入
// 得到零值时间。
func parseTime(s string) (time.Time, error) {
if strings.TrimSpace(s) == "" {
return time.Time{}, nil
}
t, err := time.Parse(time.RFC3339Nano, s)
if err != nil {
return time.Time{}, fmt.Errorf("memory: parse time %q: %w", s, err)
}
return t, nil
}
// normalizeMemoryTimestamps 把每个非 storedTimeLen 字节宽的 memory_items
// 时间戳重写为定宽 timeLayout。布局存在之前写入的行
// 携带变宽 RFC3339Nano 文本,其字符串顺序
// 并非时间顺序;这把它们拉齐。
//
// 它是幂等的:只选取列长度与 storedTimeLen 不同的行,
// 因此在已规范化的表上再跑一次是空操作。值解析不了的行
// 原样保留 —— 一个坏的遗留值不该
// 妨碍存储打开。
func normalizeMemoryTimestamps(ctx context.Context, db *sql.DB) error {
if err := normalizeTimeColumn(ctx, db, memoryItemsTable, "created_at"); err != nil {
return err
}
return normalizeTimeColumn(ctx, db, memoryItemsTable, "updated_at")
}
// normalizeTimeColumn 重写 table.col 中每个非 storedTimeLen
// 字节宽的值。table 与 col 是编译期常量,绝非用户输入,因此
// 把它们拼进语句是安全的;长度比较与每个
// 写入值都参数化。所有重写都在任何 UPDATE
// 执行前收集完毕,因此 SELECT 游标仍打开时不会发生写入。
func normalizeTimeColumn(ctx context.Context, db *sql.DB, table, col string) error {
fixes, err := collectLegacyTimestamps(ctx, db, table, col)
if err != nil {
return err
}
for _, f := range fixes {
if _, err := db.ExecContext(ctx,
"UPDATE "+f.table+" SET "+f.col+" = ? WHERE id = ?", f.value, f.id); err != nil {
return fmt.Errorf("memory: normalize %s.%s: %w", f.table, f.col, err)
}
}
return nil
}
// timeFix 是一条待执行的时间戳重写:哪张表哪一列、哪一行
// (按主键 id),以及要写入的定宽值。
type timeFix struct {
table string
col string
id string
value string
}
// collectLegacyTimestamps 读取 table.col 中值非 storedTimeLen
// 字节宽的行,把可解析的作为待重写项返回。
// 不可解析的值跳过,因此规范化绝不会让某行失败:
// 唯一的错误是真正的读取失败。
func collectLegacyTimestamps(ctx context.Context, db *sql.DB, table, col string) ([]timeFix, error) {
rows, err := db.QueryContext(ctx,
"SELECT id, "+col+" FROM "+table+" WHERE length("+col+") != ?", storedTimeLen)
if err != nil {
return nil, fmt.Errorf("memory: query legacy %s.%s: %w", table, col, err)
}
defer func() { _ = rows.Close() }()
fixes := make([]timeFix, 0)
for rows.Next() {
var id, raw string
if err := rows.Scan(&id, &raw); err != nil {
return nil, fmt.Errorf("memory: scan legacy %s.%s: %w", table, col, err)
}
fixed, ok := reformatTime(raw)
if !ok {
continue
}
fixes = append(fixes, timeFix{table: table, col: col, id: id, value: fixed})
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("memory: iterate legacy %s.%s: %w", table, col, err)
}
return fixes, nil
}
// reformatTime 用 timeLayout 重新编码 raw。raw 已经
// storedTimeLen 宽、或无法解析时 bool 为 false,此时调用方
// 保持存储值不动。
func reformatTime(raw string) (string, bool) {
if len(raw) == storedTimeLen {
return "", false
}
t, err := parseTime(raw)
if err != nil || t.IsZero() {
return "", false
}
return formatTime(t), true
}