package DB import ( "errors" "fmt" "git.ycl.pub/yl32940/TsDB2/DB/core/page" ) // ============================================================================ // 表基类: 会话绑定 + 头部增删改查 // ============================================================================ // // 三张公开面孔: // // BaseModel 主键表基类 = PrimaryModel (别名) 内嵌 → _id 列 + B+ 树主键索引 // TimeSeriesModel 时序表基类 内嵌 → _ts 列 + 时序索引 // // 表**必须**内嵌其中之一: 它既是"这是一张本框架的表"的标记 (ParseModel / UseTable // 对没有基类的结构体直接报 ErrNotModel), 也让对象的"头部"有了增删改查: // // type User struct { // DB.BaseModel // Name string `vrtx:"maxlen:32"` // } // // tbl, _ := DB.UseTable[User](db) // u := tbl.New() // 已绑定 // u.Name = "a" // u.Insert() // 增 (自增主键自动回写 u.ID) // u.Name = "b" // u.Update() // 改 (按 u.ID) // u.Reload() // 查 (按 u.ID 重新读进 u) // u.Delete() // 删 (按 u.ID) // // 为什么要把 sess / self 存在基类里 —— Go 的嵌入不是继承: // // 方法被提升到外层结构体之后, 接收者仍然是那个被嵌入的基类值, 它没有任何办法回头 // 拿到"外层对象"。而增删改查必须拿到外层对象 (要序列化整行、要把自增主键写回字段), // 所以基类只能自己持有它。这两个字段由 Table[T] 在绑定时写入 —— 只有那一层知道 T。 // modelCore 三个公开基类的共同底座, 持有"这个对象属于哪张表"。 // // 单独拆成一个非导出结构体, 而不是直接塞进 BaseModel: // - 时序表基类要用同一套增删改查, 但它不该被塞进一个根本用不上的 _id 字段; // - 增删改查只保留一份实现, 而不是每个基类各写一遍 (也就不会有哪一份忘了改)。 type modelCore struct { sess session // 所属表 (Table[T] 的类型擦除视图); nil = 未绑定 self any // 外层对象的指针 (*T), 由 Table 写入 pkRowID uint64 // 引擎行号 (pkID); 0 = 未知。对用户隐藏, 只用来精确定位"自己那一行" } // session 是 Table[T] 的类型擦除视图: 模型基类只认这一组操作, 不认识 T。 // // 每个方法内部都会把参数断言回 *T。断言失败只可能是"把 A 表的对象交给了 B 表", // 所以返回明确错误, 而不是 panic。 type session interface { insert(obj any) error update(obj any) error pkOf(obj any) (uint64, error) loadInto(pk uint64, obj any) error reload(obj any) error deleteByPK(pk uint64) error deleteByRowID(pkID uint64) error tableName() string } // modelBinder 由内嵌的表基类提供, 让 Table 把"这个对象属于哪张表"写回基类, // 也让它把"这一行在引擎里的行号"带回对象。 // // 走接口断言而不是反射: 绑定是每条被读写的记录都要过一遍的路径, 反射在这里是白付的。 // 由于 UseTable 已经强制要求内嵌基类, 这里的断言必然成功 —— 保留失败分支只是为了让 // Table[T] 的绑定路径不依赖"上游一定校验过"这个假设 (少一处只能靠推理保证的不变量)。 type modelBinder interface { bindTo(s session, self any) rowID() uint64 setRowID(pkID uint64) } // rowID 返回本对象的引擎行号 (pkID); 0 表示未知。 // // 行号对用户完全隐藏 —— 它是引擎给每一行发的自增内部句柄 (类似 SQLite 的 rowid), // 不是表结构里的一列, 也不参与查询语义。它存在的唯一理由是"同一个时间戳下可以有多行": // 时间戳只说得清"落在哪个分段", 只有行号能指认"是分段里的哪一行"。没有它, 同刻的 // 第二行就没法被读 / 改 / 删精确定位 (按时间键只会碰到第一行)。 // // 由 Table[T] 在 Insert / Find / Select 之后写入本对象。 func (c *modelCore) rowID() uint64 { return c.pkRowID } // setRowID 由 Table[T] 写入本对象的引擎行号。 func (c *modelCore) setRowID(pkID uint64) { c.pkRowID = pkID } // rowIDOf / setRowIDOf 是包内工具: 对可能没有内嵌基类的对象安全地读写行号。 func rowIDOf(obj any) uint64 { if b, ok := obj.(modelBinder); ok { return b.rowID() } return 0 } func setRowIDOf(obj any, pkID uint64) { if b, ok := obj.(modelBinder); ok && pkID != 0 { b.setRowID(pkID) } } // bindTo 由 Table 调用: 写入会话与外层对象指针。 func (c *modelCore) bindTo(s session, self any) { c.sess = s c.self = self } // Bound 报告本对象是否已绑定到表。 func (c *modelCore) Bound() bool { return c != nil && c.sess != nil } // ready 检查绑定状态。未绑定时给出"接下来该做什么"的错误, 而不是一句 nil 崩溃。 func (c *modelCore) ready(op string) error { if !c.Bound() { return fmt.Errorf("%w: 不能调用 %s (先用 Table.New 创建, 或 Table.Bind 绑定, "+ "或先经过一次 Insert / Find)", ErrModelNotBound, op) } return nil } // ===== 头部增删改查 ===== // Insert 插入本对象 (增)。主键字段为 0 时向引擎要一个自增号并回写本对象; // 显式给了主键就原样写入 (重复主键报 core.ErrDuplicatePK)。 func (c *modelCore) Insert() error { if err := c.ready("Insert"); err != nil { return err } return c.sess.insert(c.self) } // Update 按本对象自己的主键更新 (改)。 func (c *modelCore) Update() error { if err := c.ready("Update"); err != nil { return err } return c.sess.update(c.self) } // Delete 删除本对象所在的那一行 (删)。 // // 优先按引擎行号删 (对象是从表里读出来的 / 插入过, 行号已知), 这样同一时间戳下 // 有多行时删的确实是"自己那一行"; 行号未知时 (外部手工构造的对象) 才退回按可见 // 主键 (_id / _ts) 删。 func (c *modelCore) Delete() error { if err := c.ready("Delete"); err != nil { return err } if id := c.rowID(); id != 0 { return c.sess.deleteByRowID(id) } pk, err := c.PKErr() if err != nil { return err } if pk == 0 { return fmt.Errorf("%w: Delete 需要一个非 0 主键来定位行", ErrZeroPrimaryKey) } return c.sess.deleteByPK(pk) } // Save 没见过这一行就插入, 见过就更新 —— 不想区分"新对象 / 已有对象"时用它。 // // "见过"以引擎行号为准 (插入过或从表里读出来的对象都有行号); 行号未知时退回首部的 // 老约定: 可见主键为 0 当新对象插入。 func (c *modelCore) Save() error { if err := c.ready("Save"); err != nil { return err } if c.rowID() != 0 { return c.sess.update(c.self) } pk, err := c.PKErr() if err != nil { return err } if pk == 0 { return c.sess.insert(c.self) } return c.sess.update(c.self) } // Load 按主键把整行读进本对象 (查)。 // // 之所以是"填自己"而不是"返回新对象": 头部方法没有类型参数, 拿不到 T, 也就没法 // 返回 *T。要一个新建的、类型安全的对象请用 Table[T].Find。 func (c *modelCore) Load(pk uint64) error { if err := c.ready("Load"); err != nil { return err } return c.sess.loadInto(pk, c.self) } // Reload 重新读一遍本对象那一行 (行被别处改过时用它刷新)。 // // 走的是 Table.reload: 行号已知就按行号读 —— 时序表里"按时间键读"只会读到该时刻的 // 第一行, 拿到同刻多行中的第二行时那样会读串行。 func (c *modelCore) Reload() error { if err := c.ready("Reload"); err != nil { return err } return c.sess.reload(c.self) } // PK 返回本对象当前的主键值; 取不到 (未绑定 / 非法) 时为 0。 func (c *modelCore) PK() uint64 { pk, _ := c.PKErr() return pk } // PKErr 同 PK, 但把"未绑定"和"主键非法 (负数等)"的错误也交出来。 func (c *modelCore) PKErr() (uint64, error) { if err := c.ready("PK"); err != nil { return 0, err } return c.sess.pkOf(c.self) } // TableName 返回本对象所属的表名 (未绑定时为空串)。 func (c *modelCore) TableName() string { if !c.Bound() { return "" } return c.sess.tableName() } // ===== 公开基类 ===== // BaseModel 主键表基类 —— 内嵌后自动获得 _id 主键列 (B+ 树主键索引) 和头部增删改查。 // // type User struct { // DB.BaseModel // Name string `vrtx:"maxlen:32,index:idx_name"` // Age int64 `vrtx:"index:idx_age,type:hash"` // Score int // 自动 int→int64 // } // // 主键可以被 vrtx:"pk" 覆盖成自己的字段 (见 types.go 的 ParseModel 说明): // // type Ticket struct { // DB.BaseModel // No int64 `vrtx:"pk"` // 主键列变成 No, _id 不再建列 // Name string `vrtx:"maxlen:32"` // } type BaseModel struct { modelCore ID uint64 `vrtx:"_id"` } // PrimaryModel 主键表基类, 与 BaseModel 完全等价。 // // 用别名而不是再定义一个同构结构体: 主键表基类本来就只有一种物理形态 // (B+ 树主键索引 + 默认 _id 列), 复制一份结构体只会让 ParseModel 多出一个必须 // 等价处理的分支 —— 多一份必须同步维护的"真相"。别名既给了这个名字, 又不引入第二份定义。 type PrimaryModel = BaseModel // TimeSeriesModel 时序表基类 —— 内嵌后自动获得 _ts 时序键列 (时序索引) 和头部增删改查。 // // type Sensor struct { // DB.TimeSeriesModel // Value float64 // } // // 时序键同样可以用 vrtx:"pk" 换成自己的字段, 但注意两条约束: // - 键列必须是 64 位整数; // - 键列会被排到首列 (时序索引按行内固定偏移读时间键, 见 types.go)。 // // 时序键就是"时间戳"本身, 约定用纳秒级 Unix 时间戳。Insert 时若没给 (==0), ORM 会补上 // "写入这一刻"的纳秒时间戳, 而不是行号 —— 时序键只表达时间, 行号另有其人。 // // type Tick struct { // DB.TimeSeriesModel // Stamp int64 `vrtx:"pk"` // 时序键列变成 Stamp, _ts 不再建列 // Price float64 // } // // 时序键不是引擎行号。同一个时间戳下允许存在多行 (同一时刻的多个测点 / 多次事件) —— // 时间键只决定"落在哪个分段", 行号才能指认"是分段里的哪一行"。行号由引擎自动分配, // 随对象在 Insert / Find / Select 之后带回 (对用户隐藏), 所以同刻多行各自都能被精确 // 地改 / 删。详见 table.go 的 Insert / Update 与 core.Table 的 *ByPK 系列。 type TimeSeriesModel struct { modelCore TS uint64 `vrtx:"_ts"` } // ModelSpec 表基类自报物理形态 —— 相当于其它语言里的虚函数。 // // 之所以是接口而不是"基类里写死判断": ParseModel 只负责递归结构体, 主键列叫什么、 // 一级索引是哪种, 由基类自己回答; 于是再加一种基类只需要实现这三个方法。 // // 方法名非导出: 只允许本包的基类实现, 用户结构体的嵌入不会意外满足它。 type ModelSpec interface { indexType() page.IndexType // 一级 (行存储) 索引类型 pkColumn() string // 默认主键列名 pkGoField() string // 默认主键列对应的 Go 字段名 } func (BaseModel) indexType() page.IndexType { return page.IndexTypePrimary } func (BaseModel) pkColumn() string { return pkColumnName } func (BaseModel) pkGoField() string { return "ID" } func (TimeSeriesModel) indexType() page.IndexType { return page.IndexTypeTime } func (TimeSeriesModel) pkColumn() string { return tsColumnName } func (TimeSeriesModel) pkGoField() string { return "TS" } // ===== 错误 ===== var ( // ErrNotModel 类型没有内嵌 DB.BaseModel / DB.PrimaryModel / DB.TimeSeriesModel。 // 表基类既是"这是一张本框架的表"的标记 (ParseModel / UseTable 会因此拒绝该类型), // 也是对象头部增删改查的来源。 ErrNotModel = errors.New("类型未内嵌表基类") // ErrModelNotBound 在未绑定到表的对象上调用头部方法。 ErrModelNotBound = errors.New("模型未绑定到表") // ErrZeroPrimaryKey 头部方法需要对象自带主键来定位行, 但主键为 0。 ErrZeroPrimaryKey = errors.New("主键为 0") )