|
|
@@ -46,10 +46,10 @@ type SchemaVersion struct {
|
|
|
}
|
|
|
type TableInfo struct {
|
|
|
Name [TableNameSize]byte
|
|
|
- Flag byte // 表的状态是否被删除
|
|
|
- FieldCount byte // 当前字段的总数量(包含已删除,永不减少)byte=255上限
|
|
|
- ActiveFieldCount byte // 实际活跃字段数量(不包含已删除)
|
|
|
- _ byte // 1 显式对齐填充 (TotalRows 需要 8 字节对齐)
|
|
|
+ Flag byte // 表的状态是否被删除
|
|
|
+ FieldCount byte // 当前字段的总数量(包含已删除,永不减少)byte=255上限
|
|
|
+ ActiveFieldCount byte // 实际活跃字段数量(不包含已删除)
|
|
|
+ _ byte // 1 显式对齐填充 (TotalRows 需要 8 字节对齐)
|
|
|
|
|
|
// HeapPageCount 本表堆链 (FirstHeap→LastHeap) 上的页数。
|
|
|
//
|
|
|
@@ -242,7 +242,7 @@ func (table *Table) CreateIndex(idxName string, typ page.IndexType, fields []byt
|
|
|
return nil, fmt.Errorf("%w (索引 %q 的第 %d 个分量)", ErrIndexDynamicField, idxName, i+1)
|
|
|
}
|
|
|
if typ == page.IndexTypeCompose {
|
|
|
- if w := getFieldSize(ft); w != 8 {
|
|
|
+ if w := table.Field[fi].Width(); w != 8 {
|
|
|
return nil, fmt.Errorf("%w (索引 %q 的第 %d 个分量是 %s, 宽度 %d)",
|
|
|
ErrIndexComposeWidth, idxName, i+1, types.TypeNameOf(ft), w)
|
|
|
}
|
|
|
@@ -381,11 +381,11 @@ func (table *Table) BuildIndexKey(idxName string, values ...interface{}) ([]byte
|
|
|
if types.IsDynamic(ft) {
|
|
|
return nil, fmt.Errorf("索引 %q 的第 %d 个分量是动态列, 不能作为索引键", idxName, k+1)
|
|
|
}
|
|
|
- enc, err := encodeIndexKeySafeWithParam(values[k], ft, table.fieldScale(fi))
|
|
|
+ enc, err := encodeIndexKeySafeWithParam(values[k], ft, table.fieldParam(fi))
|
|
|
if err != nil {
|
|
|
return nil, fmt.Errorf("索引 %q 的第 %d 个分量: %w", idxName, k+1, err)
|
|
|
}
|
|
|
- if want := int(getFieldSize(ft)); len(enc) != want {
|
|
|
+ if want := int(table.Field[fi].Width()); len(enc) != want {
|
|
|
return nil, fmt.Errorf("索引 %q 的第 %d 个分量: 编码 %d 字节, 列宽 %d 字节",
|
|
|
idxName, k+1, len(enc), want)
|
|
|
}
|
|
|
@@ -419,13 +419,73 @@ func (table *Table) findFieldIndex(name string) int {
|
|
|
return -1
|
|
|
}
|
|
|
|
|
|
-// getFieldSize 获取字段大小。
|
|
|
+// fieldWidth 取某列的行内宽度 (字节) —— 引擎内所有行偏移计算的唯一入口。
|
|
|
//
|
|
|
-// 不再需要 & 0x7F: 删除标记已迁到 FieldInfo.Flag (见 types.FlagDeleted), FieldType
|
|
|
-// 现在就是纯粹的类型值 —— 旧实现必须在这里清标记位, 漏一处就让已删除列的宽度变成 0,
|
|
|
-// 整行字段静默错位。
|
|
|
-func getFieldSize(ft types.FieldType) uint16 {
|
|
|
- return types.GetSize(ft)
|
|
|
+// 为什么不能直接 types.GetSize(f.FieldType): 字符串列的行内宽度由【列自己声明】
|
|
|
+// (Flag 低 5 位的宽度码, 上限 types.MaxInlineColumnWidth), 只看类型会一律算成 256,
|
|
|
+// 于是该列之后的所有列都从错位偏移解码。这里统一经 FieldInfo.Width()。
|
|
|
+//
|
|
|
+// 返回 0 表示索引越界或列级参数非法 —— 调用方必须当错误处理: 0 宽度会让整行错位。
|
|
|
+func (table *Table) fieldWidth(masterIdx int) uint16 {
|
|
|
+ if masterIdx < 0 || masterIdx >= len(table.Field) {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ return table.Field[masterIdx].Width()
|
|
|
+}
|
|
|
+
|
|
|
+// MaxPhysicalRowData 单行 rowData 的物理上限 (字节) —— 一行必须能装进一个 DataPage。
|
|
|
+//
|
|
|
+// 16384 (页) - 40 (DataPage 头) - 4 (行长度前缀) - 8 (storeRow 里的 pkID) = 16332
|
|
|
+//
|
|
|
+// 它与建列闸门 types.MaxRowDataSize (8171, 一页两行) 是两件事:
|
|
|
+// - MaxRowDataSize 是设计政策, 在 CreateTable / AddField 处执行, 保证一页至少装两行;
|
|
|
+// 它卡的是【整行占用】= 4 + 8 + 这里的 rowData, 所以比这道物理墙紧得多 (8159 / 16332);
|
|
|
+// - MaxPhysicalRowData 是物理墙, 在写入时兜底 —— 超过它的行字节流会写坏 DataPage,
|
|
|
+// 必须有第二道网拦住"建列闸门之前的旧表"与"schema 被外部改坏"这两种情况。
|
|
|
+const MaxPhysicalRowData = int(PageSize) - int(page.DataPageHdrSize) - 4 - 8
|
|
|
+
|
|
|
+// validateFieldsForSchema 校验一组列定义能否构成合法 schema:
|
|
|
+// 列名长度可落盘 + 列级参数自洽 + 行宽不超建列闸门。
|
|
|
+//
|
|
|
+// 校验落在 core 而不是只留在 ORM / migrate: schema 也能从 SQL 或低层 API 直接建出来,
|
|
|
+// 只有拦在真正写 schema 的地方才拦得住。
|
|
|
+func validateFieldsForSchema(fields []types.FieldInfo) error {
|
|
|
+ for _, f := range fields {
|
|
|
+ // 列名长度: schema 里是 [nameLen(1B)][name...] (见 serializeSchema / readSchema)。
|
|
|
+ // 长度为 0 或 >255 都不能落盘 —— 0 会让解析侧把下一个字段的类型字节当名字,
|
|
|
+ // >255 会让 nameLen 截断并让其后所有字段整体错位。都不是"名字有瑕疵", 是整表错乱。
|
|
|
+ if len(f.FieldName) == 0 {
|
|
|
+ return fmt.Errorf("%w: 列名长度为 0 (schema 里列名长度是 1 字节, 空名字无法落盘)",
|
|
|
+ types.ErrValueRange)
|
|
|
+ }
|
|
|
+ if len(f.FieldName) > FieldNameSize {
|
|
|
+ return fmt.Errorf("%w: 列名 %q 有 %d 字节, 上限 %d 字节 "+
|
|
|
+ "(schema 里列名长度是 1 字节, 超长会截断并让后续列整体错位)",
|
|
|
+ types.ErrValueRange, f.FieldName, len(f.FieldName), FieldNameSize)
|
|
|
+ }
|
|
|
+ if err := types.ValidateColumnParam(f.FieldType, f.Param()); err != nil {
|
|
|
+ return fmt.Errorf("列 %q: %w", f.FieldName, err)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return types.CheckRowSize(fields)
|
|
|
+}
|
|
|
+
|
|
|
+// nextSchemaVersion 返回下一个可用的 schema 版本号。
|
|
|
+//
|
|
|
+// schemaVer 是每行行首的 1 字节 (getVersionedOffset / rowDataSizeForVersion 都按它取版本),
|
|
|
+// 版本条数也是 1 字节落盘 (serializeSchema 的 verCount)。到 0xFF 再自增就会回绕成 0:
|
|
|
+// 0 不是任何已知版本, getVersion 返回 nil → 该行按"未知版本"解析, NULL 位图取不到,
|
|
|
+// 所有列静默读成 0 / NULL。宁可在演进这一步拒绝, 让用户重建表。
|
|
|
+//
|
|
|
+// 每次 AddField / DropField 都会新增一版, 因此这道闸门也限定了"一张表的 schema
|
|
|
+// 最多能演进多少次"。
|
|
|
+func (table *Table) nextSchemaVersion() (byte, error) {
|
|
|
+ if table.activeVersion >= 0xFF || len(table.versions) >= 0xFF {
|
|
|
+ return 0, fmt.Errorf("schema 版本数已达上限 %d (activeVer=%d, 已存 %d 版): "+
|
|
|
+ "版本号是行首 1 字节, 再演进会回绕并让旧行解析错位; 请重建该表",
|
|
|
+ 0xFF, table.activeVersion, len(table.versions))
|
|
|
+ }
|
|
|
+ return table.activeVersion + 1, nil
|
|
|
}
|
|
|
|
|
|
// getVersion 获取指定版本的 schema 定义
|
|
|
@@ -475,7 +535,7 @@ func (table *Table) getVersionedOffset(rowData []byte, masterFieldIdx int) (uint
|
|
|
}
|
|
|
return offset, true
|
|
|
}
|
|
|
- offset += getFieldSize(table.Field[idx].FieldType)
|
|
|
+ offset += table.Field[idx].Width()
|
|
|
}
|
|
|
return 0, false
|
|
|
}
|
|
|
@@ -525,7 +585,7 @@ func (table *Table) rowDataSizeForVersion(ver byte) int {
|
|
|
if sv != nil {
|
|
|
size += types.NullBitmapLen(len(sv.FieldIndices)) // NULL 位图 (行尾)
|
|
|
for _, idx := range sv.FieldIndices {
|
|
|
- size += int(getFieldSize(table.Field[idx].FieldType))
|
|
|
+ size += int(table.Field[idx].Width())
|
|
|
}
|
|
|
}
|
|
|
return size
|
|
|
@@ -600,9 +660,11 @@ func (table *Table) pickVersionForUpdate(oldRowData []byte, values []types.Field
|
|
|
return oldVer
|
|
|
}
|
|
|
|
|
|
-// maxCompareInlineSize 字段比较时使用的栈缓冲上界 == 最宽的定长字段 (256byte)。
|
|
|
-// 超过它的类型会退化成堆分配 (不会 panic, 但新增更宽的类型时值得注意)。
|
|
|
-const maxCompareInlineSize = 256
|
|
|
+// maxCompareInlineSize 字段比较时使用的栈缓冲上界 == 最宽的行内定长列。
|
|
|
+//
|
|
|
+// 取自 types.MaxInlineColumnWidth 而不是写死 256: 字符串列可以用宽度码声明到 2048 字节,
|
|
|
+// 缓冲若还停在 256, 宽字符串列的每次比较都会走堆分配。
|
|
|
+const maxCompareInlineSize = types.MaxInlineColumnWidth
|
|
|
|
|
|
// ======================== 行内字段比较 ========================
|
|
|
//
|
|
|
@@ -618,13 +680,13 @@ const maxCompareInlineSize = 256
|
|
|
|
|
|
// compareFieldBytes 比较行内字段与给定值, 返回 (cmp, ok); ok=false 表示无法比较。
|
|
|
//
|
|
|
-// typ 必须是【列的实际存储类型】(调用方经 compareTypeFor 取, 与索引路径编键同源),
|
|
|
-// 否则编码出来的字节与行内不同源, 比较结果没有意义。
|
|
|
+// ct 必须是【列的实际存储类型 + 列级参数】(调用方经 compareColTypeFor 取, 与索引路径
|
|
|
+// 编键同源), 否则编码出来的字节与行内不同源, 比较结果没有意义。
|
|
|
// ok=false 的来源: 字段越界、动态列解引用失败、值无法按该类型编码。
|
|
|
// 前两种按"不匹配"处理是正确的; 第三种是调用方 bug —— 索引路径会显式报错
|
|
|
// (见 encodeIndexKeySafe), 行级过滤按不匹配处理, 至少不会污染数据。
|
|
|
-func (table *Table) compareFieldBytes(row []byte, offset uint16, typ types.FieldType, value interface{}) (int, bool) {
|
|
|
- if types.IsDynamic(typ) {
|
|
|
+func (table *Table) compareFieldBytes(row []byte, offset uint16, ct colType, value interface{}) (int, bool) {
|
|
|
+ if types.IsDynamic(ct.typ) {
|
|
|
if int(offset)+types.DynamicRefSize > len(row) {
|
|
|
return 0, false
|
|
|
}
|
|
|
@@ -643,7 +705,7 @@ func (table *Table) compareFieldBytes(row []byte, offset uint16, typ types.Field
|
|
|
return 0, false
|
|
|
}
|
|
|
|
|
|
- size := int(getFieldSize(typ))
|
|
|
+ size := int(ct.width())
|
|
|
if size == 0 || int(offset)+size > len(row) {
|
|
|
return 0, false
|
|
|
}
|
|
|
@@ -651,11 +713,11 @@ func (table *Table) compareFieldBytes(row []byte, offset uint16, typ types.Field
|
|
|
|
|
|
// decimal 的 scale 存在行内, 条件值的 scale 可能与行内不同 (1.5 与 1.50 的系数
|
|
|
// 分别是 15 和 150), 系数直接 memcmp 会得出错误结论 → 走数值比较。
|
|
|
- if typ == types.FieldType_Decimal {
|
|
|
+ if ct.typ == types.FieldType_Decimal {
|
|
|
return table.compareDecimalBytes(stored, value)
|
|
|
}
|
|
|
|
|
|
- // 编码到栈缓冲再比较 (定长字段最大 256 字节; 超过则退化为堆分配, 不 panic)
|
|
|
+ // 编码到栈缓冲再比较 (最宽列 2048 字节; 更宽的类型退化为堆分配, 不 panic)
|
|
|
var stack [maxCompareInlineSize]byte
|
|
|
var buf []byte
|
|
|
if size <= len(stack) {
|
|
|
@@ -663,7 +725,9 @@ func (table *Table) compareFieldBytes(row []byte, offset uint16, typ types.Field
|
|
|
} else {
|
|
|
buf = make([]byte, size)
|
|
|
}
|
|
|
- if err := types.EncodeValue(typ, 0, value, buf); err != nil {
|
|
|
+ // 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
|
|
|
@@ -688,55 +752,71 @@ func (table *Table) compareDecimalBytes(stored []byte, value interface{}) (int,
|
|
|
return types.DecimalFromBytes(stored).Cmp(d), true
|
|
|
}
|
|
|
|
|
|
-// compareTypeFor 决定行级过滤比较时到底用哪个类型 —— 以【列的实际存储类型】为准。
|
|
|
+// 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), 行级过滤若用声明类型, 两条路径就会分叉:
|
|
|
-// 同一列带索引时"声明错类型"仍能命中 (索引键按列类型编), 不带索引时却按
|
|
|
+// - 索引路径编键用的是列定义 (见 query.go buildKeyLayout / planIndex:
|
|
|
+// q.table.Field[..].FieldType 与 fieldParam), 行级过滤若用声明类型, 两条路径就会
|
|
|
+// 分叉: 同一列带索引时"声明错类型"仍能命中 (索引键按列定义编), 不带索引时却按
|
|
|
// 声明类型编出另一种字节 → 静默返回 0 行, 看起来像数据不存在。
|
|
|
// - 声明类型因此降级为"值怎么从 interface{} 取出来"的提示: EncodeValue 按 Go
|
|
|
// 值的 kind 分派, 本身并不依赖它。
|
|
|
//
|
|
|
+// 列级参数 (字符串宽度码) 同理必须一起取: 宽度决定了"行内那几字节从哪切、切多长",
|
|
|
+// 与是否走索引无关。
|
|
|
+//
|
|
|
// 删除标记已迁到 FieldInfo.Flag, FieldType 本身不再混入状态位, 因此这里直接返回
|
|
|
// 即可 —— 旧实现在此清 0x80, 留着标记位会让 IsDynamic 查不到注册项: 动态列会被
|
|
|
// 当成定长字段, 拿 6 字节堆引用去和值做字节比较。
|
|
|
//
|
|
|
-// 拿不到字段时返回 0 (未注册类型), compareFieldBytes 判 size==0 后按不匹配处理 ——
|
|
|
-// 调用方已先判过 fieldIdx < 0, 这里只是兜底。
|
|
|
-func (table *Table) compareTypeFor(fieldIdx int) types.FieldType {
|
|
|
+// 拿不到字段时返回零值 (未注册类型 + 参数 0), compareFieldBytes 判 width()==0 后按
|
|
|
+// 不匹配处理 —— 调用方已先判过 fieldIdx < 0, 这里只是兜底。
|
|
|
+func (table *Table) compareColTypeFor(fieldIdx int) colType {
|
|
|
if fieldIdx < 0 || fieldIdx >= len(table.Field) {
|
|
|
- return 0
|
|
|
+ return colType{}
|
|
|
}
|
|
|
- return table.Field[fieldIdx].FieldType
|
|
|
+ return colType{table.Field[fieldIdx].FieldType, table.Field[fieldIdx].Param()}
|
|
|
}
|
|
|
|
|
|
// compareValue 比较字段值是否相等
|
|
|
-func (table *Table) compareValue(row []byte, offset uint16, typ types.FieldType, value interface{}) bool {
|
|
|
- c, ok := table.compareFieldBytes(row, offset, typ, value)
|
|
|
+func (table *Table) compareValue(row []byte, offset uint16, ct colType, value interface{}) bool {
|
|
|
+ c, ok := table.compareFieldBytes(row, offset, ct, value)
|
|
|
return ok && c == 0
|
|
|
}
|
|
|
|
|
|
// compareGreater 比较字段值是否大于给定值
|
|
|
-func (table *Table) compareGreater(row []byte, offset uint16, typ types.FieldType, value interface{}) bool {
|
|
|
- c, ok := table.compareFieldBytes(row, offset, typ, value)
|
|
|
+func (table *Table) compareGreater(row []byte, offset uint16, ct colType, value interface{}) bool {
|
|
|
+ c, ok := table.compareFieldBytes(row, offset, ct, value)
|
|
|
return ok && c > 0
|
|
|
}
|
|
|
|
|
|
// compareLess 比较字段值是否小于给定值
|
|
|
-func (table *Table) compareLess(row []byte, offset uint16, typ types.FieldType, value interface{}) bool {
|
|
|
- c, ok := table.compareFieldBytes(row, offset, typ, value)
|
|
|
+func (table *Table) compareLess(row []byte, offset uint16, ct colType, value interface{}) bool {
|
|
|
+ c, ok := table.compareFieldBytes(row, offset, ct, value)
|
|
|
return ok && c < 0
|
|
|
}
|
|
|
|
|
|
// compareGreaterOrEqual 比较字段值是否大于等于给定值
|
|
|
-func (table *Table) compareGreaterOrEqual(row []byte, offset uint16, typ types.FieldType, value interface{}) bool {
|
|
|
- return table.compareGreater(row, offset, typ, value) || table.compareValue(row, offset, typ, value)
|
|
|
+func (table *Table) compareGreaterOrEqual(row []byte, offset uint16, ct colType, value interface{}) bool {
|
|
|
+ return table.compareGreater(row, offset, ct, value) || table.compareValue(row, offset, ct, value)
|
|
|
}
|
|
|
|
|
|
// compareLessOrEqual 比较字段值是否小于等于给定值
|
|
|
-func (table *Table) compareLessOrEqual(row []byte, offset uint16, typ types.FieldType, value interface{}) bool {
|
|
|
- return table.compareLess(row, offset, typ, value) || table.compareValue(row, offset, typ, value)
|
|
|
+func (table *Table) compareLessOrEqual(row []byte, offset uint16, ct colType, value interface{}) bool {
|
|
|
+ return table.compareLess(row, offset, ct, value) || table.compareValue(row, offset, ct, value)
|
|
|
}
|
|
|
|
|
|
func (table *Table) GetFieldCount() byte { return table.info.FieldCount }
|
|
|
@@ -831,11 +911,11 @@ func (table *Table) serializeSchema() []byte {
|
|
|
// writeSchema 整条重写本表的 schema 定义
|
|
|
//
|
|
|
// 布局:
|
|
|
-// 1) 内联块 —— 表页内, 紧跟 TableInfo, 固定 FieldAllocateSize 字节, **无跳转槽**。
|
|
|
-// 建表时按 len(serializeSchema()) 精确分配 (上限 MaxInlineSchemaSize)。
|
|
|
-// 2) 续块 —— schema 超出内联块的部分, 写入「全库共享」的 schema 扩展页,
|
|
|
-// 每块格式 [SchemaFragHdr 8B][payload], 多块由 hdr.NextPage/NextOff 串成链。
|
|
|
-// 本表首个续块的位置记录在 TableInfo.ExtPage/ExtOff。
|
|
|
+// 1. 内联块 —— 表页内, 紧跟 TableInfo, 固定 FieldAllocateSize 字节, **无跳转槽**。
|
|
|
+// 建表时按 len(serializeSchema()) 精确分配 (上限 MaxInlineSchemaSize)。
|
|
|
+// 2. 续块 —— schema 超出内联块的部分, 写入「全库共享」的 schema 扩展页,
|
|
|
+// 每块格式 [SchemaFragHdr 8B][payload], 多块由 hdr.NextPage/NextOff 串成链。
|
|
|
+// 本表首个续块的位置记录在 TableInfo.ExtPage/ExtOff。
|
|
|
//
|
|
|
// 续块只追加、不单独释放: 每次都是整条重写, 旧续块立刻成为垃圾, 由
|
|
|
// SchemaExtAllocator 在尾页放不下时做「满页压缩重排」统一回收。因此这里
|
|
|
@@ -955,16 +1035,75 @@ func (table *Table) writeFrags(frags []schemaFrag, rest []byte) error {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
+// checkProspectiveRowSize 预演"把 newField 加进当前活跃字段集合之后"的行宽。
|
|
|
+//
|
|
|
+// 加列只会让行变宽, 而超宽的行写不进 DataPage (见 serializeRowInternal 的写行闸门),
|
|
|
+// 所以必须在建列这一步就拒绝: 否则这张表从加列成功的那一刻起, 一条也写不进去 ——
|
|
|
+// 而且是插数据时才暴露 (而且加列本身是"成功"的, 没有任何回滚线索)。
|
|
|
+func (table *Table) checkProspectiveRowSize(newField types.FieldInfo) error {
|
|
|
+ sv := table.getVersion(table.activeVersion)
|
|
|
+ probe := make([]types.FieldInfo, 0, len(table.Field)+1)
|
|
|
+ if sv != nil {
|
|
|
+ for _, idx := range sv.FieldIndices {
|
|
|
+ if int(idx) < len(table.Field) {
|
|
|
+ probe = append(probe, table.Field[idx])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ probe = append(probe, newField)
|
|
|
+ if err := types.CheckRowSize(probe); err != nil {
|
|
|
+ return fmt.Errorf("新增列 %q 后行宽超限: %w", newField.FieldName, err)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
func (table *Table) AddField(newField types.FieldInfo) error {
|
|
|
+ // 列级参数必须先自洽: 宽度码/scale 越界会让该列宽度算成 0, 而 0 宽度会少算字节,
|
|
|
+ // 让其后每一列都从错位偏移解码 (见 types.FieldSize)。
|
|
|
+ if err := validateFieldsForSchema([]types.FieldInfo{newField}); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // 列数闸门与建表同一条 (见 checkFieldCount): master registry 只增不减
|
|
|
+ // (DropField 是软删除, 列仍留在 registry 里), 所以按"加完之后"的列数判断。
|
|
|
+ if err := checkFieldCount(len(table.Field) + 1); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // 版本号闸门必须在【任何改动之前】: 两个分支都会在最后新增一版, 版本耗尽时
|
|
|
+ // 若已经把 in-memory 状态改了一半再返回错误, 表就停在"改了一半"的不一致状态。
|
|
|
+ nextVer, err := table.nextSchemaVersion()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
existing := table.findFieldByName(newField.FieldName)
|
|
|
if existing != nil {
|
|
|
// 字段存在且活跃 → 报错
|
|
|
if !existing.IsDropped() {
|
|
|
return ErrFieldExists
|
|
|
}
|
|
|
- // 字段已删除 → 复活它
|
|
|
- existing.FieldType = newField.FieldType // 恢复类型
|
|
|
- existing.Flag = newField.Flag // 列级标志 (scale / 可空) 一并恢复
|
|
|
+ // 复活必须沿用原类型与列级参数。
|
|
|
+ //
|
|
|
+ // master registry 的类型与列级参数是【所有历史版本共用】的行偏移依据
|
|
|
+ // (见 getVersionedOffset / rowDataSizeForVersion)。旧实现是整字节覆盖
|
|
|
+ // (existing.FieldType = newField.FieldType), 看着像"恢复", 实际是"改掉":
|
|
|
+ // varchar(50) 的列复活成 varchar(500) 之后, 已经落盘的旧行仍按 224 字节排布,
|
|
|
+ // 却会被按 2048 字节解析 —— 该列之后的每一个字段都静默错位。
|
|
|
+ // 想改宽度只能换列名新建, 或者重建表 (DropField 本就只标记删除、保留原类型)。
|
|
|
+ if existing.FieldType != newField.FieldType || existing.Param() != newField.Param() {
|
|
|
+ return fmt.Errorf("%w: 列 %q 已被删除, 复活时不允许改变类型/宽度 "+
|
|
|
+ "(原 %s/参数 %d, 新 %s/参数 %d); 请改用新列名, 或重建该表",
|
|
|
+ ErrFieldExists, newField.FieldName,
|
|
|
+ types.TypeNameOf(existing.FieldType), existing.Param(),
|
|
|
+ types.TypeNameOf(newField.FieldType), newField.Param())
|
|
|
+ }
|
|
|
+ // 复活同样会让行变宽, 先做行宽预演
|
|
|
+ if err := table.checkProspectiveRowSize(newField); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ // 只恢复"存活 + 可空/唯一"这些标志位, 低 5 位的列级参数保持原值
|
|
|
+ existing.Flag = (existing.Flag & types.FlagScaleMask) | (newField.Flag &^ types.FlagScaleMask)
|
|
|
table.info.ActiveFieldCount++
|
|
|
|
|
|
// 找到该字段在 master registry 中的索引
|
|
|
@@ -984,9 +1123,9 @@ func (table *Table) AddField(newField types.FieldInfo) error {
|
|
|
}
|
|
|
newIndices = append(newIndices, byte(revivedIdx))
|
|
|
|
|
|
- table.activeVersion++
|
|
|
+ table.activeVersion = nextVer
|
|
|
table.versions = append(table.versions, SchemaVersion{
|
|
|
- Version: table.activeVersion,
|
|
|
+ Version: nextVer,
|
|
|
FieldIndices: newIndices,
|
|
|
})
|
|
|
|
|
|
@@ -998,7 +1137,12 @@ func (table *Table) AddField(newField types.FieldInfo) error {
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
- // 全新字段: 追加到 master registry
|
|
|
+ // 全新字段: 同样先做行宽预演 (加列只会让行更宽)
|
|
|
+ if err := table.checkProspectiveRowSize(newField); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // 追加到 master registry
|
|
|
table.Field = append(table.Field, newField)
|
|
|
table.info.FieldCount++
|
|
|
table.info.ActiveFieldCount++
|
|
|
@@ -1011,9 +1155,9 @@ func (table *Table) AddField(newField types.FieldInfo) error {
|
|
|
}
|
|
|
newIndices = append(newIndices, byte(len(table.Field)-1))
|
|
|
|
|
|
- table.activeVersion++
|
|
|
+ table.activeVersion = nextVer
|
|
|
table.versions = append(table.versions, SchemaVersion{
|
|
|
- Version: table.activeVersion,
|
|
|
+ Version: nextVer,
|
|
|
FieldIndices: newIndices,
|
|
|
})
|
|
|
|
|
|
@@ -1064,6 +1208,12 @@ func (table *Table) DropField(fieldName string) error {
|
|
|
return ErrFieldNotFound
|
|
|
}
|
|
|
|
|
|
+ // 版本号闸门必须在改动之前 (理由同 AddField): 删列也要新增一版 schema。
|
|
|
+ nextVer, err := table.nextSchemaVersion()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
// 标记删除 (FieldType 本身不动: 它是"这一列是什么", 与存活状态无关)
|
|
|
table.Field[masterIdx].Flag |= types.FlagDeleted
|
|
|
table.info.ActiveFieldCount--
|
|
|
@@ -1079,9 +1229,9 @@ func (table *Table) DropField(fieldName string) error {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- table.activeVersion++
|
|
|
+ table.activeVersion = nextVer
|
|
|
table.versions = append(table.versions, SchemaVersion{
|
|
|
- Version: table.activeVersion,
|
|
|
+ Version: nextVer,
|
|
|
FieldIndices: newIndices,
|
|
|
})
|
|
|
|
|
|
@@ -1095,8 +1245,8 @@ func (table *Table) DropField(fieldName string) error {
|
|
|
|
|
|
// ======================== 动态字段堆页存储 ========================
|
|
|
//
|
|
|
-// 动态字段 (FieldType_Dynamicbyte: maxlen>256 的字符串 / []byte) 的行内只保存
|
|
|
-// 6 字节引用: [PageID uint32 BE(4B)][Offset uint16 BE(2B)]
|
|
|
+// 动态字段 (FieldType_Dynamicbyte: 声明宽度超过 MaxInlineColumnWidth(2048) 字节的
|
|
|
+// 字符串 / []byte) 的行内只保存 6 字节引用: [PageID uint32 BE(4B)][Offset uint16 BE(2B)]
|
|
|
// - PageID == 0 → 空值 (NULL / 空串 / 空二进制)
|
|
|
// - 否则指向堆页 PageID 的物理偏移 Offset 处的数据块
|
|
|
// 堆页数据块格式: [Len uint32 BE][Payload...]
|
|
|
@@ -1117,8 +1267,27 @@ const dynamicBlockHdr = 4 // 堆数据块的长度前缀大小
|
|
|
// 为什么单独列出来: Blob 是"存文件/大对象"的类型, 使用者必须能在写入前就知道
|
|
|
// 这条线, 而不是写进去才撞上 heapAllocBlock 里那句与"值太大"看不出因果的
|
|
|
// ErrHeapPageTooLarge。写入侧的检查见 writeFieldValueAt。
|
|
|
+//
|
|
|
+// 它同时是【声明侧】的容量上限 (声明超限的字符串列也落动态列, 容量同样是这个数):
|
|
|
+// types.MaxStringSize 就是它, 由下面那行编译期断言钉住。
|
|
|
const MaxBlobSize = PageSize - page.PageHdrSize - dynamicBlockHdr
|
|
|
|
|
|
+// 编译期断言: types 里那组【页面布局镜像】推出的两个上限, 必须与这里的物理布局一致
|
|
|
+// (行宽那条因为要整除, 只钉到 ±1 字节 —— "它恰好是一页能放下的最大值"这个精确性质由
|
|
|
+// core 的 TestMaxRowDataSizeFitsTwoRows 用真实的 PageSize / DataPageHdrSize 复核)。
|
|
|
+// 两处一旦漂移, 后果不是报错而是"承诺了做不到的事":
|
|
|
+// - MaxStringSize 对不上 MaxBlobSize → 允许声明一个写不进堆页的容量, 建表时通过、
|
|
|
+// 某天写入大值时才撞 ErrHeapPageTooLarge;
|
|
|
+// - MaxRowDataSize 对不上"一页两行" → 放进来一张两行装不进一页的表, 页内顺序扫描、
|
|
|
+// 按 pkID 的有序插入、更新时的页内重建全部退化, 正是那条政策要避免的情形。
|
|
|
+//
|
|
|
+// 用"下标必须为 0"表达相等: 常量下标越界直接编译失败, 且表达式不占任何空间。改页大小 /
|
|
|
+// 页头 / 块长度前缀时这里会先报错, 提醒同步 types 里那组镜像。
|
|
|
+var (
|
|
|
+ _ = [1]struct{}{}[int(MaxBlobSize)-types.MaxStringSize]
|
|
|
+ _ = [1]struct{}{}[int(types.MaxRowDataSize)-int((PageSize-page.DataPageHdrSize-1)/2)]
|
|
|
+)
|
|
|
+
|
|
|
// maxHeapRefCount 单个堆页的引用数防御上限 (RefCount 为 uint16, 容器上限 65535)。
|
|
|
//
|
|
|
// 注意: 这是纯防御性常量, 实际永远不会触发 —— 堆页块数的物理上限只有 3272
|
|
|
@@ -1387,7 +1556,7 @@ func (table *Table) heapReleaseRow(rowData []byte) {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- offset += getFieldSize(f.FieldType)
|
|
|
+ offset += f.Width()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1455,10 +1624,15 @@ func lookupFieldValue(values []types.FieldValue, name []byte) (interface{}, bool
|
|
|
|
|
|
// writeFieldValueAt 写入单个字段; 动态字段写入行内 6B 堆引用
|
|
|
//
|
|
|
+// 形参是 FieldInfo 而不是 FieldType: 宽度与列级参数都必须取自【同一个列定义】——
|
|
|
+// 拆成两个参数传 (类型来自这里、宽度码来自那里) 就会出现"按 256 字节编、按 2048 字节存"
|
|
|
+// 这类不一致, 而它表现为整行错位而不是报错。
|
|
|
+//
|
|
|
// checkOnly 为真时动态列直接跳过 (行内引用保持全零), 见 serializeRowForCheck: 试算行
|
|
|
// 只用于算索引键, 绝不能有分配/覆写/释放堆块的副作用 —— 它的结果可能被整丢弃。
|
|
|
func (table *Table) writeFieldValueAt(rowData []byte, offset uint16, masterIdx int,
|
|
|
- ft types.FieldType, value interface{}, oldRowData []byte, checkOnly bool) error {
|
|
|
+ f types.FieldInfo, value interface{}, oldRowData []byte, checkOnly bool) error {
|
|
|
+ ft := f.FieldType
|
|
|
if types.IsDynamic(ft) {
|
|
|
if checkOnly {
|
|
|
return nil
|
|
|
@@ -1475,35 +1649,50 @@ func (table *Table) writeFieldValueAt(rowData []byte, offset uint16, masterIdx i
|
|
|
}
|
|
|
data = b
|
|
|
}
|
|
|
- // Blob 的单块尺寸闸门: 超限在这里就报出列名与上限, 而不是等 heapAllocBlock
|
|
|
+ // 动态列的单块尺寸闸门: 超限在这里就报出列名与上限, 而不是等 heapAllocBlock
|
|
|
// 抛出与"值太大"因果关系不明的 ErrHeapPageTooLarge。
|
|
|
- if ft == types.FieldType_Blob && uint32(len(data)) > MaxBlobSize {
|
|
|
- return fmt.Errorf("%w: 字段 %q (blob) 的值 %d 字节超过单块上限 %d 字节, "+
|
|
|
- "更大的对象需要分块存储 (见 types.FieldType_Blob)",
|
|
|
- types.ErrValueRange, table.Field[masterIdx].FieldName, len(data), MaxBlobSize)
|
|
|
+ //
|
|
|
+ // 必须覆盖【所有】动态列, 不能只拦 Blob: Dynamicbyte 与 Blob 的盘上布局相同
|
|
|
+ // (行内 6B 引用, 内容整块落进一个堆页), 单值上限也一样。只拦 Blob 的话,
|
|
|
+ // 往 Dynamicbyte 写一个超限值只会得到不带列名、不带字节数的
|
|
|
+ // "申请空间超过单页最大容量", 用户看不出是哪一列、超了多少。
|
|
|
+ if uint32(len(data)) > MaxBlobSize {
|
|
|
+ return fmt.Errorf("%w: 字段 %q (%s) 的值 %d 字节超过单块上限 %d 字节, "+
|
|
|
+ "更大的内容需要分块存储 (见 types.FieldType_Blob)",
|
|
|
+ types.ErrValueRange, table.Field[masterIdx].FieldName,
|
|
|
+ types.TypeNameOf(ft), len(data), MaxBlobSize)
|
|
|
}
|
|
|
return table.writeDynamicField(rowData, offset, masterIdx, data, oldRowData)
|
|
|
}
|
|
|
- size := getFieldSize(ft)
|
|
|
+ size := f.Width()
|
|
|
+ if size == 0 {
|
|
|
+ return fmt.Errorf("%w: 字段 %q (%s) 的列级参数 %d 非法, 宽度不可计算",
|
|
|
+ types.ErrValueRange, f.FieldName, types.TypeNameOf(ft), f.Param())
|
|
|
+ }
|
|
|
if int(offset)+int(size) > len(rowData) {
|
|
|
return fmt.Errorf("%w: 字段 %d 写入越界 (offset=%d, 列宽=%d, 行长=%d)",
|
|
|
types.ErrEncodeBufSize, masterIdx, offset, size, len(rowData))
|
|
|
}
|
|
|
// 编码统一走 types.EncodeValue —— 与索引键字节同源 (见 types/codec.go)。
|
|
|
// param 取列级类型参数: decimal 必须在这里校验"值的 scale == 列声明的 scale",
|
|
|
- // 否则同一列出现两种 scale 会让"字节序 == 数值序"失效, 索引范围扫描静默漏行。
|
|
|
- if err := types.EncodeValue(ft, table.fieldScale(masterIdx), value, rowData[offset:int(offset)+int(size)]); err != nil {
|
|
|
+ // 否则同一列出现两种 scale 会让"字节序 == 数值序"失效, 索引范围扫描静默漏行;
|
|
|
+ // 字符串列则由它决定列宽闸门 (超宽在 EncodeValue 里直接报错, 不再截断)。
|
|
|
+ if err := types.EncodeValue(ft, f.Param(), value, rowData[offset:int(offset)+int(size)]); err != nil {
|
|
|
return fmt.Errorf("字段 %q 写入失败: %w", table.Field[masterIdx].FieldName, err)
|
|
|
}
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
-// fieldScale 取列级 scale (FieldType_Decimal 的小数位数, 其余类型为 0)。
|
|
|
-func (table *Table) fieldScale(masterIdx int) byte {
|
|
|
+// fieldParam 取列级类型参数 (Flag 低 5 位): decimal 是 scale, 字符串家族是行内宽度码,
|
|
|
+// 其余类型为 0 (由 types.ValidateColumnParam 保证)。
|
|
|
+//
|
|
|
+// 以前这里叫 fieldScale 且只服务 decimal; 字符串列也用上同一格之后, "取参数"与
|
|
|
+// "算宽度"必须成对 —— 只传 scale 会漏掉宽度码, 编出来的字节宽度与行内不一致。
|
|
|
+func (table *Table) fieldParam(masterIdx int) byte {
|
|
|
if masterIdx < 0 || masterIdx >= len(table.Field) {
|
|
|
return 0
|
|
|
}
|
|
|
- return table.Field[masterIdx].Flag & types.FlagScaleMask
|
|
|
+ return table.Field[masterIdx].Param()
|
|
|
}
|
|
|
|
|
|
// serializeRow 将 FieldValue 切片序列化为行数据字节 (不含 pkID 前缀)
|
|
|
@@ -1534,7 +1723,24 @@ func (table *Table) serializeRowForCheck(values []types.FieldValue, oldRowData [
|
|
|
|
|
|
func (table *Table) serializeRowInternal(values []types.FieldValue, oldRowData []byte, checkOnly bool) ([]byte, error) {
|
|
|
ver := table.pickVersionForUpdate(oldRowData, values)
|
|
|
- rowData := make([]byte, table.rowDataSizeForVersion(ver))
|
|
|
+
|
|
|
+ // 写行闸门 (第三处行宽校验)。
|
|
|
+ //
|
|
|
+ // 前两处在建列侧 (TablePage.Create / Table.AddField), 管的是"新表不许超宽";
|
|
|
+ // 这一处管的是"已经存在的老表": 表是在限制生效前建的 (或 schema 被外部改坏) 时,
|
|
|
+ // 行字节数会超过一个 DataPage 能装下的量。放任它走下去的后果不是"写失败"这么轻——
|
|
|
+ // 行长度会在 DataPage 里溢出到相邻区域, 把整页的行解析全部错位 (页内行是顺序排布、
|
|
|
+ // 靠每行 4B 长度前缀自描述的, 一行错位等于整页报废)。
|
|
|
+ //
|
|
|
+ // 放在这里而不是"逐字段校验"是因为行总长只有这里算得出来 (行宽由 schema 版本决定)。
|
|
|
+ // 代价是每行一次整数比较。
|
|
|
+ rowSize := table.rowDataSizeForVersion(ver)
|
|
|
+ if rowSize > MaxPhysicalRowData {
|
|
|
+ return nil, fmt.Errorf("%w: 表 %q 的 schema 版本 %d 行宽 %d 字节超过单页可容纳上限 %d 字节, 拒绝写入; "+
|
|
|
+ "请重建该表并把大列改为动态列 (FieldType_Dynamicbyte / Blob)",
|
|
|
+ types.ErrValueRange, util.BytesToString(table.info.Name[:]), ver, rowSize, MaxPhysicalRowData)
|
|
|
+ }
|
|
|
+ rowData := make([]byte, rowSize)
|
|
|
rowData[0] = ver
|
|
|
|
|
|
sv := table.getVersion(ver)
|
|
|
@@ -1556,7 +1762,6 @@ func (table *Table) serializeRowInternal(values []types.FieldValue, oldRowData [
|
|
|
offset := uint16(1)
|
|
|
for i, masterIdx := range sv.FieldIndices {
|
|
|
f := table.Field[masterIdx]
|
|
|
- ft := f.FieldType
|
|
|
name := f.FieldName
|
|
|
// 取值优先级: 本次传入 > 旧行原值 (未提供的字段沿用旧值, 否则"只改一列"会清空其它列)
|
|
|
value, provided := lookupFieldValue(values, name)
|
|
|
@@ -1564,25 +1769,34 @@ func (table *Table) serializeRowInternal(values []types.FieldValue, oldRowData [
|
|
|
value, provided = lookupFieldValue(base, name)
|
|
|
}
|
|
|
if provided && value == nil {
|
|
|
- if f.IsNullable() {
|
|
|
- // NULL: 位图置位, 字段区留零 (行长固定; 动态列因此不会写入堆引用)。
|
|
|
- // 试算模式也要置位 —— extractIndexKey 正是靠位图判定"该行不进本索引"。
|
|
|
- types.SetNullBit(bitmap, i)
|
|
|
- // 旧行若在该列持有堆块, 新行不再引用它 —— 不在这里释放就是每更新一次漏一块。
|
|
|
- if !checkOnly {
|
|
|
- table.releaseOldDynamic(oldRowData, int(masterIdx))
|
|
|
- }
|
|
|
- } else if err := table.writeFieldValueAt(rowData, offset, int(masterIdx), ft, value, oldRowData, checkOnly); err != nil {
|
|
|
- // 未声明 nullable 的列: 保持既有"nil 按零值写"的行为 (收紧成报错会让
|
|
|
- // 所有"传 nil 当零值"的既有调用当场失败), 由声明侧决定是否可空。
|
|
|
- return nil, err
|
|
|
+ if !f.IsNullable() {
|
|
|
+ // 非空列写 NULL: 拒绝并点名列。
|
|
|
+ //
|
|
|
+ // 这里不是"收紧", 而是把已有的不一致统一掉: 定长列写 nil 本来就走不通
|
|
|
+ // (EncodeValue 对 nil 报 ErrValueType), 唯独动态列曾把 nil 静默当成
|
|
|
+ // "空内容"存下去 (行内引用全 0, NULL 位图也不置位) —— 同一句"写 nil"
|
|
|
+ // 在定长列报错、在动态列静默成功。既然声明了非空, 就该两边都拒。
|
|
|
+ //
|
|
|
+ // 注意 base (旧行原值) 不会贡献 nil: deserializeRow 只 append 非 nil 的值
|
|
|
+ // (见那里的 `if val != nil`), 所以这条只拦"本次显式传入的 NULL",
|
|
|
+ // 不会让"只改其它列"的更新半路失败。
|
|
|
+ return nil, fmt.Errorf("%w: 字段 %q (%s) 未声明可空 (nullable), 不接受 NULL; "+
|
|
|
+ "要允许写入 NULL, 请在列声明里加 `nullable` / `null`",
|
|
|
+ types.ErrValueRange, name, types.TypeNameOf(f.FieldType))
|
|
|
+ }
|
|
|
+ // NULL: 位图置位, 字段区留零 (行长固定; 动态列因此不会写入堆引用)。
|
|
|
+ // 试算模式也要置位 —— extractIndexKey 正是靠位图判定"该行不进本索引"。
|
|
|
+ types.SetNullBit(bitmap, i)
|
|
|
+ // 旧行若在该列持有堆块, 新行不再引用它 —— 不在这里释放就是每更新一次漏一块。
|
|
|
+ if !checkOnly {
|
|
|
+ table.releaseOldDynamic(oldRowData, int(masterIdx))
|
|
|
}
|
|
|
} else if provided {
|
|
|
- if err := table.writeFieldValueAt(rowData, offset, int(masterIdx), ft, value, oldRowData, checkOnly); err != nil {
|
|
|
+ if err := table.writeFieldValueAt(rowData, offset, int(masterIdx), f, value, oldRowData, checkOnly); err != nil {
|
|
|
return nil, err
|
|
|
}
|
|
|
}
|
|
|
- offset += getFieldSize(ft)
|
|
|
+ offset += f.Width()
|
|
|
}
|
|
|
return rowData, nil
|
|
|
}
|
|
|
@@ -1647,7 +1861,7 @@ func (table *Table) deserializeRow(rowData []byte) []types.FieldValue {
|
|
|
// NULL 不出现在结果里 —— 与"零值不 append"是同一约定: 读不到值就是没有值;
|
|
|
// 更新路径靠它区分"沿用旧值"与"显式写 NULL"(见 serializeRow 的取值优先级)。
|
|
|
if !f.IsDropped() && !types.NullBitSet(bitmap, i) {
|
|
|
- size := getFieldSize(f.FieldType)
|
|
|
+ size := f.Width()
|
|
|
if int(offset)+int(size) <= len(rowData) {
|
|
|
var val interface{}
|
|
|
if types.IsDynamic(f.FieldType) {
|
|
|
@@ -1665,7 +1879,7 @@ func (table *Table) deserializeRow(rowData []byte) []types.FieldValue {
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- offset += getFieldSize(table.Field[idx].FieldType)
|
|
|
+ offset += table.Field[idx].Width()
|
|
|
}
|
|
|
return result
|
|
|
}
|