- 只翻注释/文档字符串:820 增 / 820 删 严格 1:1;剥掉整行注释与行尾注释尾巴后, 逐文件与 HEAD 比对「代码部分差异 = 0」;裁决/design 编号计数逐项与 HEAD 一致 - 保留可执行 SQL 样例、SQLite 报错原文、pgvector/HNSW/ANN/BLOB 等技术词原文 - 复核:go build 绿 + memory/projectindex/vector 三包测试绿;gofmt 仅剩 2 个既有未格式化文件(改动前即如此,未顺手修)
136 lines
4.7 KiB
Go
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
|
|
}
|