| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- package core
- import (
- "bytes"
- "encoding/binary"
- "git.ycl.pub/yl32940/TsDB2/DB/core/types"
- )
- // maxCompareInlineSize 字段比较时使用的栈缓冲上界 == 最宽的行内定长列。
- //
- // 取自 types.MaxInlineColumnWidth 而不是写死 256: 字符串列可以用宽度码声明到 2048 字节,
- // 缓冲若还停在 256, 宽字符串列的每次比较都会走堆分配。
- const maxCompareInlineSize = types.MaxInlineColumnWidth
- // ======================== 行内字段比较 ========================
- //
- // 行级比较统一经由 compareOp → compareFieldBytes 实现, 也就是"把条件值编码成与行内
- // 同源的字节, 再按字节比较"。这样做的理由:
- // - 行内字节与索引键字节本就同源 (都出自 types.EncodeValue), 于是行级过滤与索引
- // 范围扫描必然给出同一个结论, 不会出现"索引选出 5 行、过滤后剩 0 行"的自相矛盾;
- // - 新增类型不必再回来改这一个函数 (以前每个类型要在这里各写 3 遍, 漏改一处就是静默漏行)。
- //
- // 与旧的"解码成 Go 值再比"相比, 唯一的行为差异是 NaN: 按字节比时 NaN == NaN 成立,
- // 而 float 的 == 不成立。索引键本来就是按字节比的, 统一到字节序反而消除了
- // "全表扫和索引给出不同答案"的隐患。
- // compareFieldBytes 比较行内字段与给定值, 返回 (cmp, ok); ok=false 表示无法比较。
- //
- // ct 必须是【列的实际存储类型 + 列级参数】(调用方经 compareColTypeFor 取, 与索引路径
- // 编键同源), 否则编码出来的字节与行内不同源, 比较结果没有意义。
- // ok=false 的来源: 字段越界、动态列解引用失败、值无法按该类型编码。
- // 前两种按"不匹配"处理是正确的; 第三种是调用方 bug —— 索引路径会显式报错
- // (见 encodeIndexKeySafe), 行级过滤按不匹配处理, 至少不会污染数据。
- // rowFieldForCompare 定位列并返回【可直接比较】的字节。
- //
- // 【为什么必须补齐到档位宽度】行内在 v9 之后存的是实际长度, 而条件值与索引键走的仍是
- // 定长档位编码 (EncodeValue 的 buf 长度恒为 FieldSize)。两侧若不补齐就不是同一份编码:
- // B+ 树按定长拼键比较、行级过滤按变长比较, 同一条件会得出不同结论 (表现为"索引选出
- // 5 行, 过滤后剩 0 行")。补齐用零字节, 与 v8 的定长存储逐字节相同, 因此比较结果与 v8
- // 完全一致; 索引键那条路径也按同一规则补齐, 两条路径始终同源。
- //
- // 动态列【不补齐】: 它的内容是 6B 堆引用, 补齐毫无意义, 交由 compareFieldBytes 解引用。
- //
- // 【值级溢出同理】字符串列一旦在【这一行】被溢出, 行内就是 6B 堆引用而不是内容。
- // 是否溢出必须看行内 tag (RowFieldRefTagged), 不能看列类型 —— 值级溢出是逐行的,
- // 同一列可能"这行内联、那行溢出"。漏了这步就会把页号/偏移当成字符串内容去比,
- // 症状是"溢出发生后, 对未建索引的宽字符串列过滤开始静默漏行"。
- func (table *Table) rowFieldForCompare(row []byte, masterFieldIdx int, ct colType) ([]byte, bool) {
- start, end, ok := table.getVersionedRange(row, masterFieldIdx)
- if !ok {
- return nil, false
- }
- seg := row[start:end]
- if table.columnRefTagged(row, masterFieldIdx) {
- if types.IsDynamic(ct.typ) {
- return seg, true // 动态列: 保持既有契约, 交 compareFieldBytes 解引用
- }
- // 被值级溢出的字符串列: 解引用拿回内容, 再按档位补齐 → 与索引键同源。
- data, ok := table.derefHeapSeg(seg)
- if !ok {
- data = nil // pageID == 0 或读取失败 → 空内容
- }
- return types.PadToWidth(data, ct.width()), true
- }
- if types.IsDynamic(ct.typ) {
- return seg, true
- }
- return types.PadToWidth(seg, ct.width()), true
- }
- // columnRefTagged 判断【这一行】的 masterFieldIdx 列在行内是不是 6B 堆引用:
- // 动态列恒是; 字符串列看 RowFieldRefTagged 的 tag 位 —— 该实例被值级溢出了。
- func (table *Table) columnRefTagged(rowData []byte, masterFieldIdx int) bool {
- if len(rowData) < 1 {
- return false
- }
- sv := table.getVersion(rowData[0])
- if sv == nil {
- return false
- }
- col0Width := table.svCol0Width(sv)
- offTableBase := types.RowOffTableBase(col0Width)
- for i, idx := range sv.FieldIndices {
- if int(idx) != masterFieldIdx || int(idx) >= len(table.Field) {
- continue
- }
- if types.IsDynamic(table.Field[idx].FieldType) {
- return true
- }
- if i == 0 {
- return false // 列 0 恒内联 (偏移表基址依赖它, 不参与溢出)
- }
- if offTableBase+2*i >= len(rowData) {
- return false
- }
- return types.RowFieldRefTagged(rowData, i, col0Width)
- }
- return false
- }
- // derefHeapSeg 把行内 6B 堆引用 [PageID 4B][Offset 2B] 解引用成内容。
- // pageID == 0 (空引用) 或读取失败返回 ok=false, 调用方按"空内容"处理。
- func (table *Table) derefHeapSeg(seg []byte) ([]byte, bool) {
- if len(seg) < types.DynamicRefSize {
- return nil, false
- }
- pageID := binary.BigEndian.Uint32(seg[:4])
- if pageID == 0 {
- return nil, false
- }
- data, err := table.heapRead(pageID, binary.BigEndian.Uint16(seg[4:6]))
- if err != nil {
- return nil, false
- }
- return data, true
- }
- func (table *Table) compareFieldBytes(seg []byte, ct colType, value interface{}) (int, bool) {
- if types.IsDynamic(ct.typ) {
- // derefHeapSeg 把 pageID==0 与读失败统一报成 ok=false; 本处的契约与 heapRead
- // 同构, 二者必须区分: 短引用 / 读失败 → 不可比; pageID==0 (空引用) 是空内容,
- // 照常参与下面的比较 (与 rowFieldForCompare / LikeCondition 的空引用语义一致)。
- if len(seg) < types.DynamicRefSize {
- return 0, false
- }
- stored, ok := table.derefHeapSeg(seg)
- if !ok && binary.BigEndian.Uint32(seg[:4]) != 0 {
- return 0, false // 真读失败 → 不可比
- }
- switch v := value.(type) {
- case []byte:
- return bytes.Compare(stored, v), true
- case string:
- return bytes.Compare(stored, []byte(v)), true
- }
- return 0, false
- }
- size := int(ct.width())
- if size == 0 || len(seg) != size {
- // 长度必须正好等于档位宽度: 调用方 (rowFieldForCompare) 已按档位补齐, 不齐说明列
- // 参数非法或行已损坏 —— 这时按"不匹配"处理, 绝不拿错位字节去比。
- return 0, false
- }
- stored := seg
- // decimal 的 scale 存在行内, 条件值的 scale 可能与行内不同 (1.5 与 1.50 的系数
- // 分别是 15 和 150), 系数直接 memcmp 会得出错误结论 → 走数值比较。
- if ct.typ == types.FieldType_Decimal {
- return table.compareDecimalBytes(stored, value)
- }
- // 编码到栈缓冲再比较 (最宽列 2048 字节; 更宽的类型退化为堆分配, 不 panic)
- var stack [maxCompareInlineSize]byte
- var buf []byte
- if size <= len(stack) {
- buf = stack[:size]
- } else {
- buf = make([]byte, size)
- }
- // param 必须一起传: 字符串列的宽度码决定 EncodeValue 期望的 buf 长度,
- // 传 0 会让"按类型默认 256 编"与"按声明 2048 存"对不上 → 该列静默全不匹配。
- if err := types.EncodeValue(ct.typ, ct.param, value, buf); err != nil {
- return 0, false
- }
- return bytes.Compare(stored, buf), true
- }
- // compareDecimalBytes 比较行内 decimal 字节与给定值。
- //
- // 同 scale (同一列的常态) → 直接字节比较, 与索引键行为一致;
- // 不同 scale (条件值写成 1.5 而列是 DECIMAL(18,2)) → 退回数值比较, 保证语义正确。
- func (table *Table) compareDecimalBytes(stored []byte, value interface{}) (int, bool) {
- d, err := types.DecimalFromValue(value)
- if err != nil {
- return 0, false
- }
- if d.Scale() == types.DecimalScaleOfBytes(stored) {
- var buf [types.DecimalSize]byte
- if err := types.EncodeValue(types.FieldType_Decimal, byte(d.Scale()), d, buf[:]); err != nil {
- return 0, false
- }
- return bytes.Compare(stored, buf[:]), true
- }
- return types.DecimalFromBytes(stored).Cmp(d), true
- }
- // colType 一列"编码/比较所需的全部类型信息": 类型值 + 列级参数 (scale / 宽度码)。
- //
- // 打包成一个值传递, 而不是把 typ 与 param 拆成两个形参: 两者必须一起走 —— 只传类型
- // 会出现"按 256 字节编、按 2048 字节存"这类不一致, 而它表现为整列静默不匹配或整行
- // 错位, 都不是编译错误。打包后"忘了带参数"这件事在签名上就写不出来。
- type colType struct {
- typ types.FieldType
- param byte
- }
- // width 该列的行内宽度 (字节); 0 表示参数非法。
- func (c colType) width() uint16 { return types.FieldSize(c.typ, c.param) }
- // compareColTypeFor 取出行级过滤比较要用的列信息 —— 以【列的实际存储类型与参数】为准。
- //
- // 为什么不能沿用条件里声明的 Type:
- // - 索引路径编键用的是列定义 (见 query.go buildKeyLayout / planIndex:
- // q.table.Field[..].FieldType 与 fieldParam), 行级过滤若用声明类型, 两条路径就会
- // 分叉: 同一列带索引时"声明错类型"仍能命中 (索引键按列定义编), 不带索引时却按
- // 声明类型编出另一种字节 → 静默返回 0 行, 看起来像数据不存在。
- // - 声明类型因此降级为"值怎么从 interface{} 取出来"的提示: EncodeValue 按 Go
- // 值的 kind 分派, 本身并不依赖它。
- //
- // 列级参数 (字符串宽度码) 同理必须一起取: 宽度决定了"行内那几字节从哪切、切多长",
- // 与是否走索引无关。
- //
- // 删除标记已迁到 FieldInfo.Flag, FieldType 本身不再混入状态位, 因此这里直接返回
- // 即可 —— 旧实现在此清 0x80, 留着标记位会让 IsDynamic 查不到注册项: 动态列会被
- // 当成定长字段, 拿 6 字节堆引用去和值做字节比较。
- //
- // 拿不到字段时返回零值 (未注册类型 + 参数 0), compareFieldBytes 判 width()==0 后按
- // 不匹配处理 —— 调用方已先判过 fieldIdx < 0, 这里只是兜底。
- func (table *Table) compareColTypeFor(fieldIdx int) colType {
- if fieldIdx < 0 || fieldIdx >= len(table.Field) {
- return colType{}
- }
- return colType{table.Field[fieldIdx].FieldType, table.Field[fieldIdx].Param()}
- }
- // compareOp 按算子 op 比较行内字段与给定值 (seg 由 rowFieldForCompare 定位并补齐)。
- // 只做一次 compareFieldBytes 再按符号判断 —— Ge/Le 因此不必像旧实现那样为"等于"分支
- // 再比一遍 (动态列每多比一次就要多解一次堆引用、把条件值再编一次码)。
- func (table *Table) compareOp(seg []byte, ct colType, value interface{}, op CmpOp) bool {
- c, ok := table.compareFieldBytes(seg, ct, value)
- if !ok {
- return false
- }
- switch op {
- case OpEq:
- return c == 0
- case OpGt:
- return c > 0
- case OpGe:
- return c >= 0
- case OpLt:
- return c < 0
- case OpLe:
- return c <= 0
- }
- return false
- }
|