table_compare.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. package core
  2. import (
  3. "bytes"
  4. "encoding/binary"
  5. "git.ycl.pub/yl32940/TsDB2/DB/core/types"
  6. )
  7. // maxCompareInlineSize 字段比较时使用的栈缓冲上界 == 最宽的行内定长列。
  8. //
  9. // 取自 types.MaxInlineColumnWidth 而不是写死 256: 字符串列可以用宽度码声明到 2048 字节,
  10. // 缓冲若还停在 256, 宽字符串列的每次比较都会走堆分配。
  11. const maxCompareInlineSize = types.MaxInlineColumnWidth
  12. // ======================== 行内字段比较 ========================
  13. //
  14. // 行级比较统一经由 compareOp → compareFieldBytes 实现, 也就是"把条件值编码成与行内
  15. // 同源的字节, 再按字节比较"。这样做的理由:
  16. // - 行内字节与索引键字节本就同源 (都出自 types.EncodeValue), 于是行级过滤与索引
  17. // 范围扫描必然给出同一个结论, 不会出现"索引选出 5 行、过滤后剩 0 行"的自相矛盾;
  18. // - 新增类型不必再回来改这一个函数 (以前每个类型要在这里各写 3 遍, 漏改一处就是静默漏行)。
  19. //
  20. // 与旧的"解码成 Go 值再比"相比, 唯一的行为差异是 NaN: 按字节比时 NaN == NaN 成立,
  21. // 而 float 的 == 不成立。索引键本来就是按字节比的, 统一到字节序反而消除了
  22. // "全表扫和索引给出不同答案"的隐患。
  23. // compareFieldBytes 比较行内字段与给定值, 返回 (cmp, ok); ok=false 表示无法比较。
  24. //
  25. // ct 必须是【列的实际存储类型 + 列级参数】(调用方经 compareColTypeFor 取, 与索引路径
  26. // 编键同源), 否则编码出来的字节与行内不同源, 比较结果没有意义。
  27. // ok=false 的来源: 字段越界、动态列解引用失败、值无法按该类型编码。
  28. // 前两种按"不匹配"处理是正确的; 第三种是调用方 bug —— 索引路径会显式报错
  29. // (见 encodeIndexKeySafe), 行级过滤按不匹配处理, 至少不会污染数据。
  30. // rowFieldForCompare 定位列并返回【可直接比较】的字节。
  31. //
  32. // 【为什么必须补齐到档位宽度】行内在 v9 之后存的是实际长度, 而条件值与索引键走的仍是
  33. // 定长档位编码 (EncodeValue 的 buf 长度恒为 FieldSize)。两侧若不补齐就不是同一份编码:
  34. // B+ 树按定长拼键比较、行级过滤按变长比较, 同一条件会得出不同结论 (表现为"索引选出
  35. // 5 行, 过滤后剩 0 行")。补齐用零字节, 与 v8 的定长存储逐字节相同, 因此比较结果与 v8
  36. // 完全一致; 索引键那条路径也按同一规则补齐, 两条路径始终同源。
  37. //
  38. // 动态列【不补齐】: 它的内容是 6B 堆引用, 补齐毫无意义, 交由 compareFieldBytes 解引用。
  39. //
  40. // 【值级溢出同理】字符串列一旦在【这一行】被溢出, 行内就是 6B 堆引用而不是内容。
  41. // 是否溢出必须看行内 tag (RowFieldRefTagged), 不能看列类型 —— 值级溢出是逐行的,
  42. // 同一列可能"这行内联、那行溢出"。漏了这步就会把页号/偏移当成字符串内容去比,
  43. // 症状是"溢出发生后, 对未建索引的宽字符串列过滤开始静默漏行"。
  44. func (table *Table) rowFieldForCompare(row []byte, masterFieldIdx int, ct colType) ([]byte, bool) {
  45. start, end, ok := table.getVersionedRange(row, masterFieldIdx)
  46. if !ok {
  47. return nil, false
  48. }
  49. seg := row[start:end]
  50. if table.columnRefTagged(row, masterFieldIdx) {
  51. if types.IsDynamic(ct.typ) {
  52. return seg, true // 动态列: 保持既有契约, 交 compareFieldBytes 解引用
  53. }
  54. // 被值级溢出的字符串列: 解引用拿回内容, 再按档位补齐 → 与索引键同源。
  55. data, ok := table.derefHeapSeg(seg)
  56. if !ok {
  57. data = nil // pageID == 0 或读取失败 → 空内容
  58. }
  59. return types.PadToWidth(data, ct.width()), true
  60. }
  61. if types.IsDynamic(ct.typ) {
  62. return seg, true
  63. }
  64. return types.PadToWidth(seg, ct.width()), true
  65. }
  66. // columnRefTagged 判断【这一行】的 masterFieldIdx 列在行内是不是 6B 堆引用:
  67. // 动态列恒是; 字符串列看 RowFieldRefTagged 的 tag 位 —— 该实例被值级溢出了。
  68. func (table *Table) columnRefTagged(rowData []byte, masterFieldIdx int) bool {
  69. if len(rowData) < 1 {
  70. return false
  71. }
  72. sv := table.getVersion(rowData[0])
  73. if sv == nil {
  74. return false
  75. }
  76. col0Width := table.svCol0Width(sv)
  77. offTableBase := types.RowOffTableBase(col0Width)
  78. for i, idx := range sv.FieldIndices {
  79. if int(idx) != masterFieldIdx || int(idx) >= len(table.Field) {
  80. continue
  81. }
  82. if types.IsDynamic(table.Field[idx].FieldType) {
  83. return true
  84. }
  85. if i == 0 {
  86. return false // 列 0 恒内联 (偏移表基址依赖它, 不参与溢出)
  87. }
  88. if offTableBase+2*i >= len(rowData) {
  89. return false
  90. }
  91. return types.RowFieldRefTagged(rowData, i, col0Width)
  92. }
  93. return false
  94. }
  95. // derefHeapSeg 把行内 6B 堆引用 [PageID 4B][Offset 2B] 解引用成内容。
  96. // pageID == 0 (空引用) 或读取失败返回 ok=false, 调用方按"空内容"处理。
  97. func (table *Table) derefHeapSeg(seg []byte) ([]byte, bool) {
  98. if len(seg) < types.DynamicRefSize {
  99. return nil, false
  100. }
  101. pageID := binary.BigEndian.Uint32(seg[:4])
  102. if pageID == 0 {
  103. return nil, false
  104. }
  105. data, err := table.heapRead(pageID, binary.BigEndian.Uint16(seg[4:6]))
  106. if err != nil {
  107. return nil, false
  108. }
  109. return data, true
  110. }
  111. func (table *Table) compareFieldBytes(seg []byte, ct colType, value interface{}) (int, bool) {
  112. if types.IsDynamic(ct.typ) {
  113. // derefHeapSeg 把 pageID==0 与读失败统一报成 ok=false; 本处的契约与 heapRead
  114. // 同构, 二者必须区分: 短引用 / 读失败 → 不可比; pageID==0 (空引用) 是空内容,
  115. // 照常参与下面的比较 (与 rowFieldForCompare / LikeCondition 的空引用语义一致)。
  116. if len(seg) < types.DynamicRefSize {
  117. return 0, false
  118. }
  119. stored, ok := table.derefHeapSeg(seg)
  120. if !ok && binary.BigEndian.Uint32(seg[:4]) != 0 {
  121. return 0, false // 真读失败 → 不可比
  122. }
  123. switch v := value.(type) {
  124. case []byte:
  125. return bytes.Compare(stored, v), true
  126. case string:
  127. return bytes.Compare(stored, []byte(v)), true
  128. }
  129. return 0, false
  130. }
  131. size := int(ct.width())
  132. if size == 0 || len(seg) != size {
  133. // 长度必须正好等于档位宽度: 调用方 (rowFieldForCompare) 已按档位补齐, 不齐说明列
  134. // 参数非法或行已损坏 —— 这时按"不匹配"处理, 绝不拿错位字节去比。
  135. return 0, false
  136. }
  137. stored := seg
  138. // decimal 的 scale 存在行内, 条件值的 scale 可能与行内不同 (1.5 与 1.50 的系数
  139. // 分别是 15 和 150), 系数直接 memcmp 会得出错误结论 → 走数值比较。
  140. if ct.typ == types.FieldType_Decimal {
  141. return table.compareDecimalBytes(stored, value)
  142. }
  143. // 编码到栈缓冲再比较 (最宽列 2048 字节; 更宽的类型退化为堆分配, 不 panic)
  144. var stack [maxCompareInlineSize]byte
  145. var buf []byte
  146. if size <= len(stack) {
  147. buf = stack[:size]
  148. } else {
  149. buf = make([]byte, size)
  150. }
  151. // param 必须一起传: 字符串列的宽度码决定 EncodeValue 期望的 buf 长度,
  152. // 传 0 会让"按类型默认 256 编"与"按声明 2048 存"对不上 → 该列静默全不匹配。
  153. if err := types.EncodeValue(ct.typ, ct.param, value, buf); err != nil {
  154. return 0, false
  155. }
  156. return bytes.Compare(stored, buf), true
  157. }
  158. // compareDecimalBytes 比较行内 decimal 字节与给定值。
  159. //
  160. // 同 scale (同一列的常态) → 直接字节比较, 与索引键行为一致;
  161. // 不同 scale (条件值写成 1.5 而列是 DECIMAL(18,2)) → 退回数值比较, 保证语义正确。
  162. func (table *Table) compareDecimalBytes(stored []byte, value interface{}) (int, bool) {
  163. d, err := types.DecimalFromValue(value)
  164. if err != nil {
  165. return 0, false
  166. }
  167. if d.Scale() == types.DecimalScaleOfBytes(stored) {
  168. var buf [types.DecimalSize]byte
  169. if err := types.EncodeValue(types.FieldType_Decimal, byte(d.Scale()), d, buf[:]); err != nil {
  170. return 0, false
  171. }
  172. return bytes.Compare(stored, buf[:]), true
  173. }
  174. return types.DecimalFromBytes(stored).Cmp(d), true
  175. }
  176. // colType 一列"编码/比较所需的全部类型信息": 类型值 + 列级参数 (scale / 宽度码)。
  177. //
  178. // 打包成一个值传递, 而不是把 typ 与 param 拆成两个形参: 两者必须一起走 —— 只传类型
  179. // 会出现"按 256 字节编、按 2048 字节存"这类不一致, 而它表现为整列静默不匹配或整行
  180. // 错位, 都不是编译错误。打包后"忘了带参数"这件事在签名上就写不出来。
  181. type colType struct {
  182. typ types.FieldType
  183. param byte
  184. }
  185. // width 该列的行内宽度 (字节); 0 表示参数非法。
  186. func (c colType) width() uint16 { return types.FieldSize(c.typ, c.param) }
  187. // compareColTypeFor 取出行级过滤比较要用的列信息 —— 以【列的实际存储类型与参数】为准。
  188. //
  189. // 为什么不能沿用条件里声明的 Type:
  190. // - 索引路径编键用的是列定义 (见 query.go buildKeyLayout / planIndex:
  191. // q.table.Field[..].FieldType 与 fieldParam), 行级过滤若用声明类型, 两条路径就会
  192. // 分叉: 同一列带索引时"声明错类型"仍能命中 (索引键按列定义编), 不带索引时却按
  193. // 声明类型编出另一种字节 → 静默返回 0 行, 看起来像数据不存在。
  194. // - 声明类型因此降级为"值怎么从 interface{} 取出来"的提示: EncodeValue 按 Go
  195. // 值的 kind 分派, 本身并不依赖它。
  196. //
  197. // 列级参数 (字符串宽度码) 同理必须一起取: 宽度决定了"行内那几字节从哪切、切多长",
  198. // 与是否走索引无关。
  199. //
  200. // 删除标记已迁到 FieldInfo.Flag, FieldType 本身不再混入状态位, 因此这里直接返回
  201. // 即可 —— 旧实现在此清 0x80, 留着标记位会让 IsDynamic 查不到注册项: 动态列会被
  202. // 当成定长字段, 拿 6 字节堆引用去和值做字节比较。
  203. //
  204. // 拿不到字段时返回零值 (未注册类型 + 参数 0), compareFieldBytes 判 width()==0 后按
  205. // 不匹配处理 —— 调用方已先判过 fieldIdx < 0, 这里只是兜底。
  206. func (table *Table) compareColTypeFor(fieldIdx int) colType {
  207. if fieldIdx < 0 || fieldIdx >= len(table.Field) {
  208. return colType{}
  209. }
  210. return colType{table.Field[fieldIdx].FieldType, table.Field[fieldIdx].Param()}
  211. }
  212. // compareOp 按算子 op 比较行内字段与给定值 (seg 由 rowFieldForCompare 定位并补齐)。
  213. // 只做一次 compareFieldBytes 再按符号判断 —— Ge/Le 因此不必像旧实现那样为"等于"分支
  214. // 再比一遍 (动态列每多比一次就要多解一次堆引用、把条件值再编一次码)。
  215. func (table *Table) compareOp(seg []byte, ct colType, value interface{}, op CmpOp) bool {
  216. c, ok := table.compareFieldBytes(seg, ct, value)
  217. if !ok {
  218. return false
  219. }
  220. switch op {
  221. case OpEq:
  222. return c == 0
  223. case OpGt:
  224. return c > 0
  225. case OpGe:
  226. return c >= 0
  227. case OpLt:
  228. return c < 0
  229. case OpLe:
  230. return c <= 0
  231. }
  232. return false
  233. }