|
@@ -123,7 +123,8 @@ func main() {
|
|
|
panic(err)
|
|
panic(err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 增:ID 为 0 时由 ORM 自动分配(= 当前行序号 + 1),并回写到结构体
|
|
|
|
|
|
|
+ // 增:ID 为 0 时由 ORM 向引擎要一个只增不减的自增号(从当前最大存活
|
|
|
|
|
+ // pkID 之后继续,删除行不会让号回退),并回写到结构体
|
|
|
_ = emp.Insert(&Employee{Name: "Alice", Age: 25, Score: 90, Dept: "D1"})
|
|
_ = emp.Insert(&Employee{Name: "Alice", Age: 25, Score: 90, Dept: "D1"})
|
|
|
|
|
|
|
|
// 查:主键点查
|
|
// 查:主键点查
|
|
@@ -219,7 +220,7 @@ u.Delete() // 删
|
|
|
| 写法 | 含义 |
|
|
| 写法 | 含义 |
|
|
|
|------|------|
|
|
|------|------|
|
|
|
| `vrtx:"pk"` | 该字段是本表**主键**(列名就是字段名),即**覆盖**所内嵌基类默认的键列(`_id` / `_ts` 不再建列)。写 `vrtx:"_id"` / `vrtx:"_ts"` 等价。注意它**不能替代基类**:只标 `pk` 而不内嵌基类报 `ErrNotModel` |
|
|
| `vrtx:"pk"` | 该字段是本表**主键**(列名就是字段名),即**覆盖**所内嵌基类默认的键列(`_id` / `_ts` 不再建列)。写 `vrtx:"_id"` / `vrtx:"_ts"` 等价。注意它**不能替代基类**:只标 `pk` 而不内嵌基类报 `ErrNotModel` |
|
|
|
-| `vrtx:"maxlen:32"` | 字符串按 32 字节定长存储(≤32/64/128/256 就近取档,>256 转动态列);`N` 的上界是 **16360 字节**,超了 `ParseModel` 直接报错 |
|
|
|
|
|
|
|
+| `vrtx:"maxlen:32"` | 字符串定长存储(16~2048 共 24 档就近**向上**取整,约 1.25 倍递增,如 20→24B、200→224B);`N` 超过 2048 转动态列,上界 **16360 字节**,超了 `ParseModel` 直接报错 |
|
|
|
| `vrtx:"index"` | 建**默认 B+ 二级索引**,索引名自动为 `idx_<表名>_<列名>` |
|
|
| `vrtx:"index"` | 建**默认 B+ 二级索引**,索引名自动为 `idx_<表名>_<列名>` |
|
|
|
| `vrtx:"index:idx_age"` | 建 B+ 二级索引并指定索引名 |
|
|
| `vrtx:"index:idx_age"` | 建 B+ 二级索引并指定索引名 |
|
|
|
| `vrtx:"index:idx_age,type:hash"` | 建 Hash 二级索引 |
|
|
| `vrtx:"index:idx_age,type:hash"` | 建 Hash 二级索引 |
|
|
@@ -356,6 +357,9 @@ func (db *DB) SetCommitPreset(preset string) // "batch" | "online" | "safe
|
|
|
func (db *DB) SetBufferPoolCapacity(capacity int) // BufferPool 容量(页)
|
|
func (db *DB) SetBufferPoolCapacity(capacity int) // BufferPool 容量(页)
|
|
|
func (db *DB) LockWrites() // 后续页面写入阻塞等待
|
|
func (db *DB) LockWrites() // 后续页面写入阻塞等待
|
|
|
func (db *DB) UnlockWrites()
|
|
func (db *DB) UnlockWrites()
|
|
|
|
|
+func (db *DB) VACUUM(opts ...core.VACUUMOptions) (*core.VACUUMResult, error) // 整库重写:真正缩小文件并归还空闲页给 OS(页内墓碑回收走自动 compact,文件大小不变;VACUUM 任何阶段失败则旧库原样保留)
|
|
|
|
|
+func (db *DB) SetAutoVACUUM(interval time.Duration, opts ...core.VACUUMOptions) // 周期自动 VACUUM(0 = 关闭)
|
|
|
|
|
+func (db *DB) LastVACUUM() time.Time // 上次(自动)VACUUM 的时间
|
|
|
|
|
|
|
|
// 事务
|
|
// 事务
|
|
|
func (db *DB) BeginTransaction() *core.Transaction
|
|
func (db *DB) BeginTransaction() *core.Transaction
|
|
@@ -444,7 +448,7 @@ func (table *Table) InsertRow(values []types.FieldValue) (uint64, error)
|
|
|
func (table *Table) SearchByIndex(idxName string, key []byte) ([]types.FieldValue, error)
|
|
func (table *Table) SearchByIndex(idxName string, key []byte) ([]types.FieldValue, error)
|
|
|
func (table *Table) UpdateRow(idxName string, key []byte, values []types.FieldValue) error
|
|
func (table *Table) UpdateRow(idxName string, key []byte, values []types.FieldValue) error
|
|
|
func (table *Table) DeleteRow(idxName string, key []byte) error
|
|
func (table *Table) DeleteRow(idxName string, key []byte) error
|
|
|
-func (table *Table) CreateIndex(idxName string, typ page.IndexType, fields []byte) (*IndexMeta, error)
|
|
|
|
|
|
|
+func (table *Table) CreateIndex(idxName string, typ page.IndexType, fields []byte, unique ...bool) (*IndexMeta, error)
|
|
|
func (table *Table) DropIndex(idx string) error
|
|
func (table *Table) DropIndex(idx string) error
|
|
|
func (table *Table) BuildIndexKey(idxName string, values ...interface{}) ([]byte, error)
|
|
func (table *Table) BuildIndexKey(idxName string, values ...interface{}) ([]byte, error)
|
|
|
func (table *Table) TotalRows() uint64
|
|
func (table *Table) TotalRows() uint64
|
|
@@ -649,7 +653,7 @@ type File struct {
|
|
|
```go
|
|
```go
|
|
|
type HeaderPage struct {
|
|
type HeaderPage struct {
|
|
|
Magic [4]byte // 魔术字 "VRTX"
|
|
Magic [4]byte // 魔术字 "VRTX"
|
|
|
- Version uint32 // 格式版本,当前 = 7(core.Version)
|
|
|
|
|
|
|
+ Version uint32 // 格式版本,当前 = 10(core.Version)
|
|
|
PageSize uint32 // 数据页大小 (16384)
|
|
PageSize uint32 // 数据页大小 (16384)
|
|
|
NextFreePage uint32 // 下一个未分配页(0 头页 / 1 建库预留,故从 2 开始)
|
|
NextFreePage uint32 // 下一个未分配页(0 头页 / 1 建库预留,故从 2 开始)
|
|
|
TablePage uint32 // 表链首页 ID
|
|
TablePage uint32 // 表链首页 ID
|
|
@@ -685,10 +689,13 @@ type HeaderPage struct {
|
|
|
- `v8`:DataPage 引入槽位目录(行号稳定),主键 B+ 树叶条目由 `[key(8)+dpID(4)]` 12 字节扩为
|
|
- `v8`:DataPage 引入槽位目录(行号稳定),主键 B+ 树叶条目由 `[key(8)+dpID(4)]` 12 字节扩为
|
|
|
`[key(8)+dpID(4)+slot(2)]` 14 字节 —— 命中叶即可按 `(dpID, slot)` O(1) 取行。旧库按 12 字节解析会读错
|
|
`[key(8)+dpID(4)+slot(2)]` 14 字节 —— 命中叶即可按 `(dpID, slot)` O(1) 取行。旧库按 12 字节解析会读错
|
|
|
`dpID`,不兼容需重建。
|
|
`dpID`,不兼容需重建。
|
|
|
-- `v9`(当前):字段类型枚举整体重排并改用语义名 —— `1/2/4/8byte` → `Uint8/16/32/64`,定长字符串的
|
|
|
|
|
|
|
+- `v9`:字段类型枚举整体重排并改用语义名 —— `1/2/4/8byte` → `Uint8/16/32/64`,定长字符串的
|
|
|
`16/32/64/128/256byte` 五档合并为单一 `String`(行内宽度全交 Flag 低 5 位的宽度码)。类型值编号与 v8
|
|
`16/32/64/128/256byte` 五档合并为单一 `String`(行内宽度全交 Flag 低 5 位的宽度码)。类型值编号与 v8
|
|
|
完全不同(如 `Uint8` 由 1 变 2、`Int8` 由 12 变 3),旧库按 v8 解析会把类型字节当成另一种类型:行内宽度
|
|
完全不同(如 `Uint8` 由 1 变 2、`Int8` 由 12 变 3),旧库按 v8 解析会把类型字节当成另一种类型:行内宽度
|
|
|
与解码语义同时错位,不兼容需重建。主键列 / 时序键 `_ts` 仍留在 `FieldType_Uint64`(无符号、补码)。
|
|
与解码语义同时错位,不兼容需重建。主键列 / 时序键 `_ts` 仍留在 `FieldType_Uint64`(无符号、补码)。
|
|
|
|
|
+- `v10`(当前):`FieldType_Blob` 行内由 6B 堆引用改为 8B `BlobRef` 直引链页(流式多页链),内容上限
|
|
|
|
|
+ 从单堆页 ~16KB 扩到链页总 ≈4GB;Update / Delete / DropTable 自动释放旧 blob 链。旧库 blob 列的
|
|
|
|
|
+ 6B 引用会被误读成 BlobRef 高 6B(TotalLen 异常),不兼容需重建任何带 Blob 列的表。
|
|
|
|
|
|
|
|
### 页大小与公共页头
|
|
### 页大小与公共页头
|
|
|
|
|
|
|
@@ -972,10 +979,10 @@ TsDB2/
|
|
|
│
|
|
│
|
|
|
├── cmd/vrtxsql/ # 命令行迁移工具(见「跨库迁移」)
|
|
├── cmd/vrtxsql/ # 命令行迁移工具(见「跨库迁移」)
|
|
|
│
|
|
│
|
|
|
-├── demo/ # 20 个可运行示例 / 验证程序(见下节)
|
|
|
|
|
|
|
+├── demo/ # 27 个可运行示例 / 验证程序(见下节)
|
|
|
├── docs/ # 设计与操作文档
|
|
├── docs/ # 设计与操作文档
|
|
|
├── Makefile # build / test / fmt / lint / demo-analyzer
|
|
├── Makefile # build / test / fmt / lint / demo-analyzer
|
|
|
-├── go.mod # module git.ycl.pub/yl32940/TsDB2(go 1.25;引擎零依赖,迁移层带 sqlite/mysql 驱动)
|
|
|
|
|
|
|
+├── go.mod # module git.ycl.pub/yl32940/TsDB2(go 1.24;引擎零依赖,迁移层带 sqlite/mysql 驱动)
|
|
|
└── README.md
|
|
└── README.md
|
|
|
```
|
|
```
|
|
|
|
|
|
|
@@ -992,7 +999,7 @@ TsDB2/
|
|
|
| `go run ./demo/orm_full` | 4 种索引形态 CRUD + 事务 + 持久化 + 索引特性对比 + 头部增删改查 / 自定义主键 / 拒绝无基类模型 | **47 通过 / 0 失败** |
|
|
| `go run ./demo/orm_full` | 4 种索引形态 CRUD + 事务 + 持久化 + 索引特性对比 + 头部增删改查 / 自定义主键 / 拒绝无基类模型 | **47 通过 / 0 失败** |
|
|
|
| `go run ./demo/full_test` | 10 万行端到端:CRUD + 条件 + 排序 + 聚合 + 持久化 + 页回收 | **28 通过 / 0 失败** |
|
|
| `go run ./demo/full_test` | 10 万行端到端:CRUD + 条件 + 排序 + 聚合 + 持久化 + 页回收 | **28 通过 / 0 失败** |
|
|
|
| `go run ./demo/feature_demo` | 特性清单逐项验证(含防抖提交、批量插入、FindAll、表基类约定) | **64 通过 / 0 失败** |
|
|
| `go run ./demo/feature_demo` | 特性清单逐项验证(含防抖提交、批量插入、FindAll、表基类约定) | **64 通过 / 0 失败** |
|
|
|
-| `go run .` | core 层最小示例:建表 + 时序索引 + Inserter/Updater/Deleter | 正常输出 |
|
|
|
|
|
|
|
+| `go run ./demo/manual_test` | core 层最小示例:建表 + 时序索引 + Inserter/Updater/Deleter | 正常输出 |
|
|
|
|
|
|
|
|
### 索引专项
|
|
### 索引专项
|
|
|
|
|
|
|
@@ -1031,6 +1038,7 @@ TsDB2/
|
|
|
| 命令 | 内容 | 实测结果 |
|
|
| 命令 | 内容 | 实测结果 |
|
|
|
|------|------|----------|
|
|
|------|------|----------|
|
|
|
| `go run ./demo/blob_stream` | Blob 流式存储 5 场景:空流 / 8KB 不压缩 / 1MB Snappy / 网络中断回滚 / 100MB 随机流 | **全部通过**(链式 blob,单值上限 ~4GB,可选 Snappy) |
|
|
| `go run ./demo/blob_stream` | Blob 流式存储 5 场景:空流 / 8KB 不压缩 / 1MB Snappy / 网络中断回滚 / 100MB 随机流 | **全部通过**(链式 blob,单值上限 ~4GB,可选 Snappy) |
|
|
|
|
|
+| `go run ./demo/blob_orm` | ORM 层 Blob 6 场景:32MB 流式写 / 流式读 CRC / []byte 对照 / 改名 Update 保链 / 重开持久化 / 删行归还链页 | **全部通过** |
|
|
|
| `go run ./demo/unique_index_test` | 唯一索引正确性 / 跨二级索引冲突 / 重建 / 持久化 | 通过 |
|
|
| `go run ./demo/unique_index_test` | 唯一索引正确性 / 跨二级索引冲突 / 重建 / 持久化 | 通过 |
|
|
|
| `go run ./demo/unique_orm_test` | ORM 层面 `vrtx:"unique"` 与 `index:NAME` 复合唯一键 | 通过 |
|
|
| `go run ./demo/unique_orm_test` | ORM 层面 `vrtx:"unique"` 与 `index:NAME` 复合唯一键 | 通过 |
|
|
|
| `go run ./demo/null_bitmap_test` | NULL 位图:可空列 + 多列组合 + 持久化 + 索引对 NULL 的语义 | 通过 |
|
|
| `go run ./demo/null_bitmap_test` | NULL 位图:可空列 + 多列组合 + 持久化 + 索引对 NULL 的语义 | 通过 |
|
|
@@ -1104,6 +1112,7 @@ DSN 前缀决定库的种类,冒号后面整段就是它的标识(路径 /
|
|
|
| `-auto-pk` | 源表没有整数主键时自动补 `_id` 列(默认报错) |
|
|
| `-auto-pk` | 源表没有整数主键时自动补 `_id` 列(默认报错) |
|
|
|
| `-continue-on-error` | 某张表失败时跳过它、继续迁其余表(默认第一个错误就中止) |
|
|
| `-continue-on-error` | 某张表失败时跳过它、继续迁其余表(默认第一个错误就中止) |
|
|
|
| `-loose` | 值装不进目标列时按引擎语义截断(默认报错,免得静默丢数据) |
|
|
| `-loose` | 值装不进目标列时按引擎语义截断(默认报错,免得静默丢数据) |
|
|
|
|
|
+| `-truncate-oversize` | 行超过引擎整行上限(8171B)时截断而非报错——`INTO VRTX` 导入超大行时用 |
|
|
|
| `-no-meta` | 不写 `__vrtx_meta`,放弃定长档位 / 索引类型的还原 |
|
|
| `-no-meta` | 不写 `__vrtx_meta`,放弃定长档位 / 索引类型的还原 |
|
|
|
| `-v` | 打印每张表的进度 |
|
|
| `-v` | 打印每张表的进度 |
|
|
|
|
|
|
|
@@ -1127,7 +1136,7 @@ fmt.Println(rep.String()) // 每张表的行数 / 列数 / 索
|
|
|
|
|
|
|
|
SQL 表达不了 VRTX 的这些细节,所以导出时会在 SQL 侧额外建一张隐藏表 `__vrtx_meta` 记着:
|
|
SQL 表达不了 VRTX 的这些细节,所以导出时会在 SQL 侧额外建一张隐藏表 `__vrtx_meta` 记着:
|
|
|
|
|
|
|
|
-- 每列的**定长档位**(1/2/4/8/16/32/64/128/256 / Float64 / Int8/16/32/64 / 变长)——否则 `INT` 回来时无法区分 4 字节还是 8 字节,也无从分辨有符号档与无符号档;
|
|
|
|
|
|
|
+- 每列的**定长档位**(`FieldType` 原始字节:覆盖 `Bool`/有符号与无符号各档整数/`Float32`/`Float64`/`Int128`/`UUID`/`Decimal`/`String`(16~2048 宽度码)/变长/Blob 全部语义类型)——否则 `INT` 回来时无法区分 4 字节还是 8 字节,也无从分辨有符号档与无符号档;
|
|
|
- 每列的**可空性**(`NULL` / `NOT NULL`)与**列级唯一声明**——SQL 的列级 `UNIQUE` 在读回时只剩一条自动索引,列上"声明过唯一"这半信息只有这里记得住;
|
|
- 每列的**可空性**(`NULL` / `NOT NULL`)与**列级唯一声明**——SQL 的列级 `UNIQUE` 在读回时只剩一条自动索引,列上"声明过唯一"这半信息只有这里记得住;
|
|
|
- 行存储索引的**类型**(primary / time)与**名字**(`pk_<表名>`);
|
|
- 行存储索引的**类型**(primary / time)与**名字**(`pk_<表名>`);
|
|
|
- 二级索引的**类型**(B+ 树 / Hash / 复合)、分量列顺序与**唯一标记**(`UNIQUE INDEX` ⇄ `IndexFlagUnique`)。
|
|
- 二级索引的**类型**(B+ 树 / Hash / 复合)、分量列顺序与**唯一标记**(`UNIQUE INDEX` ⇄ `IndexFlagUnique`)。
|
|
@@ -1163,7 +1172,7 @@ SQL 表达不了 VRTX 的这些细节,所以导出时会在 SQL 侧额外建
|
|
|
3. **动态列不能建索引**:`[]byte`、`maxlen>2048` 的字符串映射为动态列,行内只存 6B 堆引用,`UseTable` 会拒绝为其建索引。
|
|
3. **动态列不能建索引**:`[]byte`、`maxlen>2048` 的字符串映射为动态列,行内只存 6B 堆引用,`UseTable` 会拒绝为其建索引。
|
|
|
4. **Hash 索引不保序**:`type:hash` 的等值查询 O(1),但范围 / 排序查询退化为全桶扫描;需要范围 / 有序时请用默认 B+ 二级索引。
|
|
4. **Hash 索引不保序**:`type:hash` 的等值查询 O(1),但范围 / 排序查询退化为全桶扫描;需要范围 / 有序时请用默认 B+ 二级索引。
|
|
|
5. **索引分量变更需显式处理**:表已存在时,若同名索引在模型中的分量列表与库中不一致,`UseTable` 会**报错**(避免静默使用少一列的索引),需先删索引再重建。
|
|
5. **索引分量变更需显式处理**:表已存在时,若同名索引在模型中的分量列表与库中不一致,`UseTable` 会**报错**(避免静默使用少一列的索引),需先删索引再重建。
|
|
|
-6. **主键自动分配是「只增不减」的游标**:结构体主键为 0 时向引擎要一个自增号(首次使用时从当前最大存活 pkID 之后开始),**删除不会让编号回退**,不会与存活行撞号;结构体主键非 0 时按该值插入(`_id` 列与引擎 pkID 保持一致),重复报 `ErrDuplicatePK`。自动号不是真实时间戳。**时序表是唯一的例外**:时间键不是行号,所以无论 `_ts` 是否给了值,引擎都会分配一个新行号 —— 给了 `_ts` 就原样使用、不写回、也不当行号(同刻多行因此能共存);完全没给(`_ts == 0`)才会把刚分配的行号回填进 `_ts`,让这行至少落在某个明确时刻上(回填的是编号,不是真实时间戳)。
|
|
|
|
|
|
|
+6. **主键自动分配是「只增不减」的游标**:结构体主键为 0 时向引擎要一个自增号(首次使用时从当前最大存活 pkID 之后开始),**删除不会让编号回退**,不会与存活行撞号;结构体主键非 0 时按该值插入(`_id` 列与引擎 pkID 保持一致),重复报 `ErrDuplicatePK`。自动号不是真实时间戳。**时序表是唯一的例外**:时间键不是行号,所以无论 `_ts` 是否给了值,引擎都会分配一个新行号 —— 给了 `_ts` 就原样使用、不写回、也不当行号(同刻多行因此能共存);完全没给(`_ts == 0`)才会补上**写入这一刻的纳秒时间戳**(与显式传入的时间戳同量纲;同一纳秒内连插多行允许,同刻多行由行号区分)。
|
|
|
7. **`LockWrites` 是「阻塞」而非「拒绝」,锁的是「页落盘」**:它挡住的是 `BufferPool.WritePage`(正在写盘的 goroutine 阻塞等待),`Insert` / `Update` / `Delete` 本身照常改内存中的页并标记脏,**不会返回任何错误** —— 引擎里没有"写入被拒绝"这种错误码。因此锁定期写入既不报错也不丢,只是落盘被推迟到 `UnlockWrites`(或 `Commit` 内部临时加锁结束)之后。`demo/lock_table_test` 按这套语义断言:锁定期写入不报错且一条不丢、写入过程中脏页阈值触发提交不丢行、`BufferPool` 容量满时淘汰干净页而不是死锁、关闭重开后数据仍在。
|
|
7. **`LockWrites` 是「阻塞」而非「拒绝」,锁的是「页落盘」**:它挡住的是 `BufferPool.WritePage`(正在写盘的 goroutine 阻塞等待),`Insert` / `Update` / `Delete` 本身照常改内存中的页并标记脏,**不会返回任何错误** —— 引擎里没有"写入被拒绝"这种错误码。因此锁定期写入既不报错也不丢,只是落盘被推迟到 `UnlockWrites`(或 `Commit` 内部临时加锁结束)之后。`demo/lock_table_test` 按这套语义断言:锁定期写入不报错且一条不丢、写入过程中脏页阈值触发提交不丢行、`BufferPool` 容量满时淘汰干净页而不是死锁、关闭重开后数据仍在。
|
|
|
8. **单进程嵌入式,且是「单写入者」模型**:只面向单进程,**同一时刻只允许一个 goroutine 写** —— `Insert` / `Update` / `Delete` / `Commit` 都应由同一个写入者依次调用;不提供多进程写并发控制与分布式复制。多 goroutine 的正确用法是「一个写 + 只读」,但引擎对并发**读**没有一致性保证:一次写入要分多页依次落到缓存,读可能撞上写入的中间态(读到"新叶子 + 旧父节点"这种半成品)。要做到读取端严格一致,请把读写也串起来(或者写完之后再读)。**两个 goroutine 同时写、或一个 goroutine 写的同时另一个 goroutine 调 `Commit` 会崩**:`CollectDirty` 把脏页移出缓冲池之后、回写磁盘之前,这一页既不在缓存里也还不在盘上,并发的读会从盘上读到全零页(实测稳定 panic:`btPID: slice bounds out of range [-4:]`,页头 `EntryLen` 读到 0)。引擎内部没有写入串行化(`DB/core/pool/buffer_pool.go` 里关于 `capacityWait` 的注释写的就是「单写入者模型」),需要多写入者请在调用方自己加锁。写入与提交的「穿插」是支持的 —— 脏页阈值 / 防抖计时器触发的提交由引擎在写入者的 goroutine 里同步发起,不存在重叠。
|
|
8. **单进程嵌入式,且是「单写入者」模型**:只面向单进程,**同一时刻只允许一个 goroutine 写** —— `Insert` / `Update` / `Delete` / `Commit` 都应由同一个写入者依次调用;不提供多进程写并发控制与分布式复制。多 goroutine 的正确用法是「一个写 + 只读」,但引擎对并发**读**没有一致性保证:一次写入要分多页依次落到缓存,读可能撞上写入的中间态(读到"新叶子 + 旧父节点"这种半成品)。要做到读取端严格一致,请把读写也串起来(或者写完之后再读)。**两个 goroutine 同时写、或一个 goroutine 写的同时另一个 goroutine 调 `Commit` 会崩**:`CollectDirty` 把脏页移出缓冲池之后、回写磁盘之前,这一页既不在缓存里也还不在盘上,并发的读会从盘上读到全零页(实测稳定 panic:`btPID: slice bounds out of range [-4:]`,页头 `EntryLen` 读到 0)。引擎内部没有写入串行化(`DB/core/pool/buffer_pool.go` 里关于 `capacityWait` 的注释写的就是「单写入者模型」),需要多写入者请在调用方自己加锁。写入与提交的「穿插」是支持的 —— 脏页阈值 / 防抖计时器触发的提交由引擎在写入者的 goroutine 里同步发起,不存在重叠。
|
|
|
9. **批量删除开销较高**:`Deleter` 走「重建主键 + 二级索引」路径以保证正确性,峰值内存 / IO 高于单行删除(单行删除走 B+ 树借位 / 合并)。
|
|
9. **批量删除开销较高**:`Deleter` 走「重建主键 + 二级索引」路径以保证正确性,峰值内存 / IO 高于单行删除(单行删除走 B+ 树借位 / 合并)。
|
|
@@ -1174,11 +1183,11 @@ SQL 表达不了 VRTX 的这些细节,所以导出时会在 SQL 侧额外建
|
|
|
- 表名:**≤ 48 字节**(`TableInfo.Name`);`pk_` 前缀算在索引名的 64 里,不再侵占表名额度;
|
|
- 表名:**≤ 48 字节**(`TableInfo.Name`);`pk_` 前缀算在索引名的 64 里,不再侵占表名额度;
|
|
|
- 自动命名的二级索引 `idx_<表名>_<首列名>`:**≤ 64 字节**,即表名 ≤ `59 - len(首列名)`。
|
|
- 自动命名的二级索引 `idx_<表名>_<首列名>`:**≤ 64 字节**,即表名 ≤ `59 - len(首列名)`。
|
|
|
`UseTable` 在建表前先算这笔账(`checkNameBudget`),返回带具体字节数的 `ErrTableNameTooLong` / `ErrIndexNameTooLong`;名字还不够长就用 `vrtx:"index:<名字>"` 显式命名二级索引。core 层 `CreateIndex` 同样会拒绝超长名,而不是 `copy` 静默截断成另一个名字。
|
|
`UseTable` 在建表前先算这笔账(`checkNameBudget`),返回带具体字节数的 `ErrTableNameTooLong` / `ErrIndexNameTooLong`;名字还不够长就用 `vrtx:"index:<名字>"` 显式命名二级索引。core 层 `CreateIndex` 同样会拒绝超长名,而不是 `copy` 静默截断成另一个名字。
|
|
|
-14. **文件格式版本 `v7`**:v6 及更早的旧格式(24 字节表名 / `IndexMeta` 84 字节且索引名与表名共用 48 字节上限 / 有符号整数混用无符号档)不兼容,需要重建数据库文件。
|
|
|
|
|
|
|
+14. **文件格式版本 `v10`**:v9 及更早的旧格式(类型枚举旧编号 / blob 列行内 6B 堆引用 / 主键叶条目 12 字节 / 24 字节表名等,见「存储格式」的版本演进)不兼容,需要重建数据库文件。
|
|
|
15. **没有 `RENAME COLUMN` / `RENAME TABLE` API**:改列名只能 `DROP` 旧列 + `ADD` 新列,**老行原值不会迁移到新列名**(数据仍在行内,只是新列名下不可达),需要业务侧自行回填;改表名请新建表后搬迁。
|
|
15. **没有 `RENAME COLUMN` / `RENAME TABLE` API**:改列名只能 `DROP` 旧列 + `ADD` 新列,**老行原值不会迁移到新列名**(数据仍在行内,只是新列名下不可达),需要业务侧自行回填;改表名请新建表后搬迁。
|
|
|
16. **`DROP COLUMN` 是软删除**:字段类型最高位置 `0x80` 标记删除,**行内数据依然保留**;之后 `ADD` 一个同名同类型的列会「复活」老行里的旧值。因此**删列不能用来清除敏感数据**,需要重建表 / 覆写行才能彻底擦除。
|
|
16. **`DROP COLUMN` 是软删除**:字段类型最高位置 `0x80` 标记删除,**行内数据依然保留**;之后 `ADD` 一个同名同类型的列会「复活」老行里的旧值。因此**删列不能用来清除敏感数据**,需要重建表 / 覆写行才能彻底擦除。
|
|
|
17. **ORM 重开表会按 struct 强制同步 schema**:`UseTable[T]` 会把库里存在、但 struct 里没有的列自动 `DropField`(即上面的软删除)。纯 core API 手工 `AddField` 出来的列,一旦被 ORM `UseTable` 打开就会「消失」(数据仍在行内,把字段加回 struct 即可复活)。因此**同一张表不要混用 ORM 与手工 DDL**;以 ORM 为唯一入口,或把手工列也声明进 struct。
|
|
17. **ORM 重开表会按 struct 强制同步 schema**:`UseTable[T]` 会把库里存在、但 struct 里没有的列自动 `DropField`(即上面的软删除)。纯 core API 手工 `AddField` 出来的列,一旦被 ORM `UseTable` 打开就会「消失」(数据仍在行内,把字段加回 struct 即可复活)。因此**同一张表不要混用 ORM 与手工 DDL**;以 ORM 为唯一入口,或把手工列也声明进 struct。
|
|
|
-18. **迁移会把 VARCHAR 一律当定长**:SQL 侧读到的变长字符串列(`VARCHAR` / `TEXT`)在 VRTX 里按 `VARCHAR(n)` 的 `n` 就近取定长档位;`TEXT` / 无长度上限的类型超出 256 字节则落 `Dynamicbyte`。超过 256 字节的字符串值在 VRTX 里是变长列,**不能再建索引**。
|
|
|
|
|
|
|
+18. **迁移会把 VARCHAR 一律当定长**:SQL 侧读到的变长字符串列(`VARCHAR` / `TEXT`)在 VRTX 里按 `VARCHAR(n)` 的 `n` 就近取定长档位(16~2048);`TEXT` / 无长度上限的类型一律落 `Dynamicbyte`。声明超过 2048 字节的字符串在 VRTX 里是动态列,**不能再建索引**。
|
|
|
19. **迁移不搬 `AUTO_INCREMENT` / 序列**:SQL 的自增属性不还原(VRTX 的主键号由引擎自己的游标分配);`-recreate` 之外的重复迁移是**追加**语义,同一张表迁两次会插两遍 / 撞主键报错,续传请用 `-tables` 只点名没迁完的表。
|
|
19. **迁移不搬 `AUTO_INCREMENT` / 序列**:SQL 的自增属性不还原(VRTX 的主键号由引擎自己的游标分配);`-recreate` 之外的重复迁移是**追加**语义,同一张表迁两次会插两遍 / 撞主键报错,续传请用 `-tables` 只点名没迁完的表。
|
|
|
20. **迁移期间的并发写不做保证**:`migrate.Run` 是"读源 → 写目标"的单向快照,不锁源库、也不是一致性快照;边写边迁只能拿到某一时刻的近似结果。
|
|
20. **迁移期间的并发写不做保证**:`migrate.Run` 是"读源 → 写目标"的单向快照,不锁源库、也不是一致性快照;边写边迁只能拿到某一时刻的近似结果。
|
|
|
21. **时序表的行号不保证一致**:VRTX 时序索引的 `pkID` 由索引内部维护(只用来在库内区分同一时间戳下的多行),跨库没有意义,因此往返只保证 `_ts` 与各列数据一致,不保证行号一致。
|
|
21. **时序表的行号不保证一致**:VRTX 时序索引的 `pkID` 由索引内部维护(只用来在库内区分同一时间戳下的多行),跨库没有意义,因此往返只保证 `_ts` 与各列数据一致,不保证行号一致。
|