Compare commits

..

No commits in common. "main" and "v1.1.6" have entirely different histories.
main ... v1.1.6

9 changed files with 291 additions and 393 deletions

View File

@ -1,25 +1,5 @@
# CHANGELOG # CHANGELOG
## v1.1.8 (2026-05-16)
- **文档增强**:
- **Hook 事件指南**: 在 `README.md` 中新增了 Hook 事件的详细声明与用法说明,涵盖从表结构变更到数据 CRUD 的全生命周期钩子。
## v1.1.7 (2026-05-15)
- **底层架构重构与标准字段**:
- **强制标准字段**: 所有数据表自动携带并维护 5 个标准字段(`id`, `creator`, `createTime`, `updater`, `updateTime`),极大地规范了业务记录生命周期。
- **默认倒序排列**: `List``Query` 现默认按 `createTime DESC` 返回数据,以符合现代业务开发直觉。
- **严格的 SELECT 安全**: `buildQuery` 彻底摒弃了 `SELECT *`,基于内存缓存中的合法字段列表动态拼接 SQL彻底杜绝已删除残余字段的泄露。
- **Hook 系统重构**:
- **增强上下文注入**: 全面重构了 `Hooks` 接口签名,现在 `OnUpdatedRows``OnRemovedRows` 等事件会完整返回 `rows/ids` 以及关联的 `TableSchema``FieldSchema`
- **RAG 友好型设计**: 增强后的 Hook 系统能够让外部模块(如 RAG 索引器)在零额外查询的情况下,直接获取所需的所有元数据与数据正文。
- **基础设施对齐与 ID 自动化**:
- **移除手动 ID 生成**: 去除了 `tableDB` 内部的 `record["id"] = id.MakeID(10)` 逻辑,完全下推至 `go/db` 基础设施处理。
- **自动随机 ID 识别**: 配合底层 `db` 升级,现在只要字段名为 `id`、类型为 `char(8~16)` 且在插入时为空,系统将自动产生全局随机 ID。
- **分布式 ID 支持**: `GetDB` 增加 `redis` 参数,用于配置底层 `db` 的 Redis 实例,确保在分布式环境下生成的 ID 具备全局唯一性。
- **架构重构稳定性修复**:
- **修复 Schema 重建 bug**: 修正了 `reconstructAndSyncSchema` 在生成 DSL 时丢失 `PK` (Primary Key) 标记的问题,确保动态创建的表始终具备正确的索引结构。
- **对齐 GetDB 签名**: 同步更新所有测试用例与示例文档,对齐最新的多参数初始化模型。
## v1.1.6 (2026-05-15) ## v1.1.6 (2026-05-15)
- **零开销 ABAC 鉴权引擎**: - **零开销 ABAC 鉴权引擎**:
- **内存展平技术**: 引入 `SchemaCache` 策略展平算法,支持无限深度的用户权限继承(如 `Manager -> Boss`),并将复杂权限图谱在内存中转换为 O(1) 的平滑映射,彻底消除运行时递归与 N+1 数据库查询。 - **内存展平技术**: 引入 `SchemaCache` 策略展平算法,支持无限深度的用户权限继承(如 `Manager -> Boss`),并将复杂权限图谱在内存中转换为 O(1) 的平滑映射,彻底消除运行时递归与 N+1 数据库查询。

View File

@ -1,11 +1,10 @@
# go/tableDB v1.1.8 # go/tableDB v1.1.6
`tableDB` 是基于 `@go/db` 的高级数据库抽象引擎,支持动态 Schema 管理、身份驱动的行级隔离 (RLS) 以及 ABAC 权限策略。 `tableDB` 是基于 `@go/db` 的高级数据库抽象引擎,支持动态 Schema 管理、身份驱动的行级隔离 (RLS) 以及 ABAC 权限策略。
## 🎯 核心设计理念 ## 🎯 核心设计理念
- **动态 Schema**: 像操作数据一样操作表和字段,无需手动 SQL 迁移。 - **动态 Schema**: 像操作数据一样操作表和字段,无需手动 SQL 迁移。
- **内置标准字段**: 所有数据表强制且自动携带 5 个标准字段:`id` (全局唯一)、`creator``createTime``updater``updateTime`。系统会自动在插入和更新时维护它们。 - **身份驱动**: 所有操作强制绑定 `userID`,系统自动处理 `creator``createTime`
- **身份驱动**: 所有操作强制绑定 `userID`,系统基于此自动拦截并填充创建者、更新者。
- **ABAC 权限模型**: 支持 SQL 条件过滤、属性权限及关系继承,替代传统的 Role 体系。 - **ABAC 权限模型**: 支持 SQL 条件过滤、属性权限及关系继承,替代传统的 Role 体系。
- **极速性能**: 通过内存展平策略实现零 RTT 鉴权,支持 O(1) 字段验证。 - **极速性能**: 通过内存展平策略实现零 RTT 鉴权,支持 O(1) 字段验证。
@ -18,8 +17,8 @@ go get apigo.cc/go/tableDB
### 1. 初始化与授权 ### 1. 初始化与授权
```go ```go
// 获取未授权实例 (无法直接操作数据),可传入 redis 地址用于分布式 ID 生成 // 获取未授权实例 (无法直接操作数据)
unauth := tableDB.GetDB("sqlite://data.db", logger, "") unauth := tableDB.GetDB("sqlite://data.db", logger)
// 必须绑定身份后才能使用 // 必须绑定身份后才能使用
app := unauth.Auth("user_123") app := unauth.Auth("user_123")
@ -101,44 +100,3 @@ manager.SetPolicy(tableDB.PolicySchema{
2. **零开销拦截**: 鉴权逻辑已下推至 SQL WHERE 层,消除 N+1 查询隐患。 2. **零开销拦截**: 鉴权逻辑已下推至 SQL WHERE 层,消除 N+1 查询隐患。
3. **特权保护**: 系统表 `_Table`, `_Field`, `_Policy` 禁止普通用户通过 Table API 直接修改,必须通过专属 API 进行管理。 3. **特权保护**: 系统表 `_Table`, `_Field`, `_Policy` 禁止普通用户通过 Table API 直接修改,必须通过专属 API 进行管理。
4. **命名规范**: 全局强制驼峰命名,禁止使用 `json` tag。 4. **命名规范**: 全局强制驼峰命名,禁止使用 `json` tag。
## 🔗 Hook 事件 (Hooks)
`tableDB` 提供了一系列 Hook允许开发者在表结构变更或数据更新时注入自定义逻辑缓存同步、消息推送、业务校验等
### 1. 注册 Hook
Hook 需要在 `Auth` 之前,在 `TableDBUnauthorized` 实例上注册。
```go
unauth := tableDB.GetDB("sqlite://data.db", logger, "")
// 监听数据更新 (用于缓存同步)
unauth.Hooks.OnUpdatedRows = func(rows []map[string]any, table *tableDB.TableSchema, fields []tableDB.FieldSchema) {
for _, row := range rows {
fmt.Printf("表 %s 的数据已更新: %v\n", table.Name, row["id"])
}
}
// 数据写入前校验 (返回 error 将中断操作)
unauth.Hooks.OnUpdatingRow = func(row map[string]any, table *tableDB.TableSchema, fields []tableDB.FieldSchema) error {
if table.Name == "orders" && cast.Int(row["amount"]) > 1000000 {
return errors.New("金额过大,需要人工审核")
}
return nil
}
app := unauth.Auth("user_123")
```
### 2. 支持的事件列表
| 事件名 | 触发时机 | 参数说明 |
| :--- | :--- | :--- |
| `OnCreatedTable` | 新表创建后 | `table`: 表架构信息 |
| `OnRemovedTable` | 表被删除后 | `table`: 表架构信息 |
| `OnUpdatedField` | 字段新增或修改后 | `table`: 表架构,`field`: 字段架构 |
| `OnRemovedField` | 字段删除后 | `table`: 表架构,`fieldName`: 字段名 |
| `OnUpdatingRow` | **数据写入前** | `row`: 待写入数据,`table`: 表架构。**返回 error 可拦截写入**。 |
| `OnUpdatedRows` | 数据写入/更新后 | `rows`: 已更新的数据列表(包含完整字段) |
| `OnRemovedRows` | 数据删除后 | `ids`: 已删除的主键列表 |

View File

@ -9,7 +9,6 @@ import (
type SchemaCache struct { type SchemaCache struct {
Tables map[string]map[string]any // name -> table record Tables map[string]map[string]any // name -> table record
TableIDMap map[string]string // id -> name
Fields map[string][]FieldSchema // tableId -> fields Fields map[string][]FieldSchema // tableId -> fields
ValidFieldsMap map[string]map[string]bool // tableName -> fieldName -> true ValidFieldsMap map[string]map[string]bool // tableName -> fieldName -> true
FlatPolicies map[string]map[string]map[string][]FlatPolicy // userID -> tableName -> action -> []FlatPolicy FlatPolicies map[string]map[string]map[string][]FlatPolicy // userID -> tableName -> action -> []FlatPolicy
@ -19,7 +18,6 @@ type SchemaCache struct {
var GlobalCache = &SchemaCache{ var GlobalCache = &SchemaCache{
Tables: make(map[string]map[string]any), Tables: make(map[string]map[string]any),
TableIDMap: make(map[string]string),
Fields: make(map[string][]FieldSchema), Fields: make(map[string][]FieldSchema),
ValidFieldsMap: make(map[string]map[string]bool), ValidFieldsMap: make(map[string]map[string]bool),
FlatPolicies: make(map[string]map[string]map[string][]FlatPolicy), FlatPolicies: make(map[string]map[string]map[string][]FlatPolicy),
@ -36,47 +34,42 @@ func (c *SchemaCache) Load(tDB *TableDB) error {
return nil // System tables not yet created return nil // System tables not yet created
} }
tablesRes := dbInst.Query("SELECT * FROM `_Table`") sysApp := tDB.Auth(SystemUserID)
if tablesRes.Error != nil {
return tablesRes.Error
}
tables := tablesRes.MapResults()
fieldsRes := dbInst.Query("SELECT * FROM `_Field`") tables, err := sysApp.Table("_Table").List(nil)
if fieldsRes.Error != nil { if err != nil {
return fieldsRes.Error return err
} }
fields := fieldsRes.MapResults()
policiesRes := dbInst.Query("SELECT * FROM `_Policy`") fields, err := sysApp.Table("_Field").List(nil)
if policiesRes.Error != nil { if err != nil {
return policiesRes.Error return err
}
policies, err := sysApp.Table("_Policy").List(nil)
if err != nil {
return err
} }
policies := policiesRes.MapResults()
newTables := make(map[string]map[string]any) newTables := make(map[string]map[string]any)
newTableIDMap := make(map[string]string)
for _, t := range tables { for _, t := range tables {
name := cast.String(t["name"]) newTables[cast.String(t["name"])] = t
newTables[name] = t
newTableIDMap[cast.String(t["id"])] = name
} }
newFields := make(map[string][]FieldSchema) newFields := make(map[string][]FieldSchema)
newValidFieldsMap := make(map[string]map[string]bool) newValidFieldsMap := make(map[string]map[string]bool)
tableIdToName := make(map[string]string)
for _, t := range tables {
tableIdToName[cast.String(t["id"])] = cast.String(t["name"])
}
for _, f := range fields { for _, f := range fields {
var fs FieldSchema var fs FieldSchema
cast.Convert(&fs, f) cast.Convert(&fs, f)
// If fs.TableID is empty, try to manually get it just in case
if fs.TableID == "" {
fs.TableID = cast.String(f["tableId"])
}
tid := fs.TableID tid := fs.TableID
newFields[tid] = append(newFields[tid], fs) newFields[tid] = append(newFields[tid], fs)
tableName := newTableIDMap[tid] tableName := tableIdToName[tid]
if tableName != "" { if tableName != "" {
if newValidFieldsMap[tableName] == nil { if newValidFieldsMap[tableName] == nil {
newValidFieldsMap[tableName] = make(map[string]bool) newValidFieldsMap[tableName] = make(map[string]bool)
@ -169,7 +162,6 @@ func (c *SchemaCache) Load(tDB *TableDB) error {
defer c.lock.Unlock() defer c.lock.Unlock()
c.Tables = newTables c.Tables = newTables
c.TableIDMap = newTableIDMap
c.Fields = newFields c.Fields = newFields
c.ValidFieldsMap = newValidFieldsMap c.ValidFieldsMap = newValidFieldsMap
c.FlatPolicies = newFlatPolicies c.FlatPolicies = newFlatPolicies
@ -180,9 +172,6 @@ func (c *SchemaCache) Load(tDB *TableDB) error {
func (c *SchemaCache) IsValidField(tableName, fieldName string) bool { func (c *SchemaCache) IsValidField(tableName, fieldName string) bool {
c.lock.RLock() c.lock.RLock()
defer c.lock.RUnlock() defer c.lock.RUnlock()
if fieldName == "id" || fieldName == "createTime" || fieldName == "creator" || fieldName == "updateTime" || fieldName == "updater" {
return true
}
if c.ValidFieldsMap[tableName] == nil { if c.ValidFieldsMap[tableName] == nil {
return false return false
} }
@ -227,19 +216,8 @@ func (c *SchemaCache) GetValidFields(tableName string) []string {
tid := cast.String(table["id"]) tid := cast.String(table["id"])
fields := c.Fields[tid] fields := c.Fields[tid]
var names []string var names []string
standardFields := map[string]bool{
"id": true,
"createTime": true,
"creator": true,
"updateTime": true,
"updater": true,
}
for _, f := range fields { for _, f := range fields {
names = append(names, f.Name) names = append(names, f.Name)
delete(standardFields, f.Name)
}
for f := range standardFields {
names = append(names, f)
} }
return names return names
} }

225
db.go
View File

@ -3,6 +3,7 @@ package tableDB
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"apigo.cc/go/cast" "apigo.cc/go/cast"
"apigo.cc/go/db" "apigo.cc/go/db"
@ -19,11 +20,8 @@ _Table SD // 核心表:存储所有表的元数据
memo t // 备注 memo t // 备注
enableRLS b // 是否开启行级安全 enableRLS b // 是否开启行级安全
settings o // 设置 settings o // 设置
isSecret b // 是否敏感表(不被索引) createTime bi // 创建时间
createTime bi I // 创建时间
creator v64 // 创建者 creator v64 // 创建者
updateTime bi I // 更新时间
updater v64 // 更新者
_Field SD // 核心表:存储所有字段的元数据 _Field SD // 核心表:存储所有字段的元数据
id c10 PK id c10 PK
@ -33,10 +31,8 @@ _Field SD // 核心表:存储所有字段的元数据
isIndex b // 是否索引 isIndex b // 是否索引
memo t // 备注 memo t // 备注
settings o // 设置 settings o // 设置
createTime bi I // 创建时间 createTime bi // 创建时间
creator v64 // 创建者 creator v64 // 创建者
updateTime bi I // 更新时间
updater v64 // 更新者
_Policy SD // 核心表:访问策略 _Policy SD // 核心表:访问策略
id c10 PK id c10 PK
@ -46,20 +42,18 @@ _Policy SD // 核心表:访问策略
action v16 // 动作 (read, write, full) action v16 // 动作 (read, write, full)
condition v1024 // SQL WHERE 条件片段 condition v1024 // SQL WHERE 条件片段
conditionArgs o // 条件对应的参数数组 conditionArgs o // 条件对应的参数数组
createTime bi I // 创建时间 createTime bi // 创建时间
creator v64 // 创建者 creator v64 // 创建者
updateTime bi I // 更新时间
updater v64 // 更新者
` `
type Hooks struct { type Hooks struct {
OnCreatedTable func(table *TableSchema) OnCreatedTable func(tableName string, record map[string]any)
OnRemovedTable func(table *TableSchema) OnRemovedTable func(tableName string)
OnUpdatedField func(table *TableSchema, field *FieldSchema) OnUpdatedField func(tableId, fieldName string, record map[string]any)
OnRemovedField func(table *TableSchema, fieldName string) OnRemovedField func(tableId, fieldName string)
OnUpdatingRow func(row map[string]any, table *TableSchema, fields []FieldSchema) error OnUpdatingRow func(tableName string, row map[string]any) error
OnUpdatedRows func(rows []map[string]any, table *TableSchema, fields []FieldSchema) OnUpdatedRows func(tableName string, count int)
OnRemovedRows func(ids []string, table *TableSchema, fields []FieldSchema) OnRemovedRows func(tableName string, ids []string)
} }
type TableDBUnauthorized struct { type TableDBUnauthorized struct {
@ -75,76 +69,14 @@ type TableDB struct {
} }
// GetDB retrieves a configured database instance. Must call Auth() before use. // GetDB retrieves a configured database instance. Must call Auth() before use.
func GetDB(name string, logger *log.Logger, redis string) *TableDBUnauthorized { func GetDB(name string, logger *log.Logger) *TableDBUnauthorized {
baseDB := db.GetDB(name, logger) baseDB := db.GetDB(name, logger)
if redis != "" {
baseDB.Config.Redis = redis
}
return &TableDBUnauthorized{ return &TableDBUnauthorized{
base: baseDB, base: baseDB,
Hooks: &Hooks{}, Hooks: &Hooks{},
} }
} }
// Bootstrap initializes the system tables metadata required for tableDB to function.
func (d *TableDBUnauthorized) Bootstrap() error {
err := d.base.Sync(SystemSchema)
if err != nil {
return err
}
sys := d.Auth(SystemUserID)
groups := db.ParseSchema(SystemSchema)
for _, group := range groups {
for _, table := range group.Tables {
tRecord := map[string]any{
"name": table.Name,
"memo": table.Comment,
}
res := sys.base.Query("SELECT id FROM `_Table` WHERE name = ?", table.Name)
var tid string
rows := res.MapResults()
if len(rows) > 0 {
tid = cast.String(rows[0]["id"])
tRecord["id"] = tid
}
_ = sys.Table("_Table").Set(tRecord)
if tid == "" {
res = sys.base.Query("SELECT id FROM `_Table` WHERE name = ?", table.Name)
rows = res.MapResults()
if len(rows) > 0 {
tid = cast.String(rows[0]["id"])
}
}
if tid != "" {
for _, field := range table.Fields {
fName := field.Name
if fName == "id" || fName == "createTime" || fName == "creator" || fName == "updateTime" || fName == "updater" {
continue
}
fRecord := map[string]any{
"tableId": tid,
"name": fName,
"type": field.Type,
"isIndex": cast.If(field.Index != "", 1, 0),
"memo": field.Comment,
}
fRes := sys.base.Query("SELECT id FROM `_Field` WHERE tableId = ? AND name = ?", tid, fName)
fRows := fRes.MapResults()
if len(fRows) > 0 {
fRecord["id"] = fRows[0]["id"]
}
_ = sys.Table("_Field").Set(fRecord)
}
}
}
}
return GlobalCache.Load(sys)
}
// Auth creates a new instance with the specified userID context. // Auth creates a new instance with the specified userID context.
func (d *TableDBUnauthorized) Auth(userID string) *TableDB { func (d *TableDBUnauthorized) Auth(userID string) *TableDB {
return &TableDB{ return &TableDB{
@ -249,6 +181,74 @@ func (d *TableDB) ListPolicy(filter map[string]any) ([]PolicySchema, error) {
return policies, nil return policies, nil
} }
// syncSchema automatically applies the DSL schema to the underlying database.
func (d *TableDB) syncSchema(schemaDSL string) error {
finalDSL := schemaDSL
if !strings.Contains(schemaDSL, "_Table") {
finalDSL = SystemSchema + "\n" + schemaDSL
}
err := d.base.Sync(finalDSL)
if err != nil {
return err
}
// 2. Update _Table and _Field metadata
res := d.base.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='_Table'")
if d.base.Config.Type == "mysql" {
res = d.base.Query("SELECT TABLE_NAME name FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME='_Table'", d.base.Config.DB)
}
if res.Error == nil && res.MapOnR1()["name"] != nil {
groups := db.ParseSchema(finalDSL)
for _, group := range groups {
for _, table := range group.Tables {
// Upsert _Table
tRecord := map[string]any{
"name": table.Name,
"memo": table.Comment,
"createTime": time.Now().UnixMilli(),
}
existingTable, _ := d.Table("_Table").List(map[string]any{"name": table.Name})
var tid string
if len(existingTable) > 0 {
tid = cast.String(existingTable[0]["id"])
tRecord["id"] = tid
}
_ = d.Table("_Table").Set(tRecord)
if tid == "" {
newTable, _ := d.Table("_Table").List(map[string]any{"name": table.Name})
if len(newTable) > 0 {
tid = cast.String(newTable[0]["id"])
}
}
if tid != "" {
// Update _Field
for _, field := range table.Fields {
fRecord := map[string]any{
"tableId": tid,
"name": field.Name,
"type": field.Type,
"isIndex": cast.If(field.Index != "", 1, 0),
"memo": field.Comment,
"createTime": time.Now().UnixMilli(),
}
existingField, _ := d.Table("_Field").List(map[string]any{"tableId": tid, "name": field.Name})
if len(existingField) > 0 {
fRecord["id"] = existingField[0]["id"]
}
_ = d.Table("_Field").Set(fRecord)
}
}
}
}
}
// 3. Reload cache
return GlobalCache.Load(d)
}
// Table returns an AI-friendly interface for multi-dimensional operations on a specific table. // Table returns an AI-friendly interface for multi-dimensional operations on a specific table.
func (d *TableDB) Table(name string) *Table { func (d *TableDB) Table(name string) *Table {
return NewTable(name, d) return NewTable(name, d)
@ -263,28 +263,25 @@ func (d *TableDB) GetRawDB() (*db.DB, error) {
} }
// buildQuery constructs a SQL query from a QueryRequest with strict identifier validation and auth filtering. // buildQuery constructs a SQL query from a QueryRequest with strict identifier validation and auth filtering.
func (d *TableDB) buildQuery(t *Table, req QueryRequest) (string, []any, error) { func (d *TableDB) buildQuery(tableName string, req QueryRequest) (string, []any, error) {
tableName := t.Name
if GlobalCache.GetTable(tableName) == nil { if GlobalCache.GetTable(tableName) == nil {
return "", nil, fmt.Errorf("invalid table: %s", tableName) return "", nil, fmt.Errorf("invalid table: %s", tableName)
} }
fieldList := req.Select fields := "*"
if len(fieldList) == 0 { if len(req.Select) > 0 {
fieldList = GlobalCache.GetValidFields(tableName) var validatedSelect []string
} for _, s := range req.Select {
if !GlobalCache.IsValidField(tableName, s) {
var validatedSelect []string return "", nil, fmt.Errorf("invalid field %s in table %s", s, tableName)
for _, s := range fieldList { }
if !GlobalCache.IsValidField(tableName, s) { validatedSelect = append(validatedSelect, "`"+s+"`")
return "", nil, fmt.Errorf("invalid field %s in table %s", s, tableName)
} }
validatedSelect = append(validatedSelect, "`"+s+"`") fields = strings.Join(validatedSelect, ", ")
} }
fieldsStr := strings.Join(validatedSelect, ", ")
var sql strings.Builder var sql strings.Builder
fmt.Fprintf(&sql, "SELECT %s FROM `%s` ", fieldsStr, tableName) fmt.Fprintf(&sql, "SELECT %s FROM `%s` ", fields, tableName)
for _, join := range req.Joins { for _, join := range req.Joins {
if GlobalCache.GetTable(join.Table) == nil { if GlobalCache.GetTable(join.Table) == nil {
@ -305,8 +302,9 @@ func (d *TableDB) buildQuery(t *Table, req QueryRequest) (string, []any, error)
whereStr := req.Where whereStr := req.Where
// Apply auth filtering for the main table // Apply auth filtering for the main table
dummyTable := &Table{Name: tableName, userID: d.userID, db: d.base}
var err error var err error
whereStr, args, err = t.appendAuthAndConstraint(whereStr, args) whereStr, args, err = dummyTable.appendAuthAndConstraint(whereStr, args)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@ -316,28 +314,25 @@ func (d *TableDB) buildQuery(t *Table, req QueryRequest) (string, []any, error)
sql.WriteString(whereStr) sql.WriteString(whereStr)
} }
orderBy := req.OrderBy if req.OrderBy != "" {
if orderBy == "" { parts := strings.Fields(req.OrderBy)
orderBy = "createTime DESC" if len(parts) > 0 {
} fieldName := parts[0]
if !GlobalCache.IsValidField(tableName, fieldName) {
parts := strings.Fields(orderBy) return "", nil, fmt.Errorf("invalid order by field: %s", fieldName)
if len(parts) > 0 {
fieldName := parts[0]
if !GlobalCache.IsValidField(tableName, fieldName) {
return "", nil, fmt.Errorf("invalid order by field: %s", fieldName)
}
direction := ""
if len(parts) > 1 {
dir := strings.ToUpper(parts[1])
if dir == "ASC" || dir == "DESC" {
direction = " " + dir
} else {
return "", nil, fmt.Errorf("invalid order by direction: %s", parts[1])
} }
direction := ""
if len(parts) > 1 {
dir := strings.ToUpper(parts[1])
if dir == "ASC" || dir == "DESC" {
direction = " " + dir
} else {
return "", nil, fmt.Errorf("invalid order by direction: %s", parts[1])
}
}
fmt.Fprintf(&sql, " ORDER BY `%s` %s", fieldName, direction)
} }
fmt.Fprintf(&sql, " ORDER BY `%s` %s", fieldName, direction)
} }
if req.Limit > 0 { if req.Limit > 0 {

24
go.mod
View File

@ -3,21 +3,21 @@ module apigo.cc/go/tableDB
go 1.25.0 go 1.25.0
require ( require (
apigo.cc/go/cast v1.3.3 apigo.cc/go/cast v1.3.2
apigo.cc/go/db v1.3.4 apigo.cc/go/db v1.3.1
apigo.cc/go/log v1.3.4 apigo.cc/go/id v1.3.0
apigo.cc/go/log v1.3.2
) )
require ( require (
apigo.cc/go/config v1.3.1 // indirect apigo.cc/go/config v1.3.0 // indirect
apigo.cc/go/crypto v1.3.1 // indirect apigo.cc/go/crypto v1.3.0 // indirect
apigo.cc/go/encoding v1.3.1 // indirect apigo.cc/go/encoding v1.3.0 // indirect
apigo.cc/go/file v1.3.2 // indirect apigo.cc/go/file v1.3.0 // indirect
apigo.cc/go/id v1.3.1 // indirect apigo.cc/go/rand v1.3.0 // indirect
apigo.cc/go/rand v1.3.1 // indirect apigo.cc/go/redis v1.3.0 // indirect
apigo.cc/go/redis v1.3.2 // indirect apigo.cc/go/safe v1.3.0 // indirect
apigo.cc/go/safe v1.3.1 // indirect apigo.cc/go/shell v1.3.0 // indirect
apigo.cc/go/shell v1.3.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect

48
go.sum
View File

@ -1,27 +1,27 @@
apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34= apigo.cc/go/cast v1.3.2 h1:hh9MWDSwh3T/kQdCHjFpjDwHrh2A05Q4wt1AAWs8NBI=
apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= apigo.cc/go/cast v1.3.2/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE= apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg=
apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U= apigo.cc/go/config v1.3.0/go.mod h1:88lqKEBXlIExFKt1geLONVLYyM+QhRVpBe0ok3OEvjI=
apigo.cc/go/crypto v1.3.1 h1:ulQ2zX9bUWirk0sEacx1Srsjs2Jow7HlZq7ED7msNcg= apigo.cc/go/crypto v1.3.0 h1:rGRrrb5O+4M50X5hVUmJQbXx3l87zzlcgzGtUvZrZL8=
apigo.cc/go/crypto v1.3.1/go.mod h1:SwHlBFDPddttWgFFtzsEMla8CM/rcFy9nvdsJjW4CIs= apigo.cc/go/crypto v1.3.0/go.mod h1:uSCcmbcFoiltUPMQTSuqmU9nfKEH/lRs7nQ7aa3Z4Mc=
apigo.cc/go/db v1.3.4 h1:SDTK9+anU3vc30i5JT4y5NCkUByFxg+tX5Ju+Ye/jq4= apigo.cc/go/db v1.3.1 h1:X+iHMZ71OdWW8R91TFtE+aZr/CZPOtL7gIyt3tEkfyc=
apigo.cc/go/db v1.3.4/go.mod h1:/uELxvpf9i6cE/2fS/LprJl5JoIX3w/K9qKMy7Hdr98= apigo.cc/go/db v1.3.1/go.mod h1:Gc6HEmncoYRaaRy5yPjTegwc2pSwjNOTnINBmbomhcs=
apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y= apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs=
apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY= apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo=
apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4= apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg=
apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs= apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY=
apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk= apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE=
apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4= apigo.cc/go/id v1.3.0/go.mod h1:AFH3kMFwENfXNyijnAFWEhSF1o3y++UBPem1IUlrcxA=
apigo.cc/go/log v1.3.4 h1:UT8Neb9r4QjjbCFbTzw+ZeTxd+DmdmR5gNExeR4Cj+g= apigo.cc/go/log v1.3.2 h1:/m3V4MnlYnCG4XPHpWDsa4cw5suMaDVY1SgaVyjnBSo=
apigo.cc/go/log v1.3.4/go.mod h1:/Q/2r51xWSsrS4QN5U9jLiTw8n6qNC8kG9nuVHweY20= apigo.cc/go/log v1.3.2/go.mod h1:dz4bSz9BnOgutkUJJZfX3uDDwsMpUxt7WF50mLK9hgE=
apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8= apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU=
apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/redis v1.3.2 h1:iUWL/CHHnfonz0dJq6/V4IG3QuXBoHA2L1xnoGEbNEQ= apigo.cc/go/redis v1.3.0 h1:3NJE3xPXzhCwL+Mh1iyphFrsKWEuPlY26LHJfMVFSeU=
apigo.cc/go/redis v1.3.2/go.mod h1:/k5wcfAzB9jrfd9otabio9CPUxEsLPgEs4oggBG5sbs= apigo.cc/go/redis v1.3.0/go.mod h1:KPDPwMOER7WJX3Qev24LTeAOSmCl8OApe8iagPDxOUQ=
apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q= apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok=
apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE= apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs=
apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8= apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw=
apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw= apigo.cc/go/shell v1.3.0/go.mod h1:aNJiRWibxlA485yX3t+07IVAbrALKmxzv4oGEUC+hK4=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=

217
table.go
View File

@ -7,6 +7,7 @@ import (
"apigo.cc/go/cast" "apigo.cc/go/cast"
"apigo.cc/go/db" "apigo.cc/go/db"
"apigo.cc/go/id"
) )
// Table provides an AI-friendly interface for interacting with structured data or schema. // Table provides an AI-friendly interface for interacting with structured data or schema.
@ -81,10 +82,20 @@ func (t *Table) checkAuth(id string, action string) error {
var authArgs []any var authArgs []any
// 1. 追加 Creator 检查 // 1. 追加 Creator 检查
hasCreator := true hasCreator := false
if tableRec != nil {
tid := cast.String(tableRec["id"])
fields := GlobalCache.GetFields(tid)
for _, f := range fields {
if f.Name == "creator" {
hasCreator = true
break
}
}
}
if hasCreator { if hasCreator {
authConditions = append(authConditions, "`creator` = ?") authConditions = append(authConditions, "creator = ?")
authArgs = append(authArgs, t.userID) authArgs = append(authArgs, t.userID)
} }
@ -154,10 +165,20 @@ func (t *Table) appendAuthAndConstraint(whereStr string, args []any) (string, []
var authArgs []any var authArgs []any
// Check creator field // Check creator field
hasCreator := true hasCreator := false
if tableRec != nil {
tid := cast.String(tableRec["id"])
fields := GlobalCache.GetFields(tid)
for _, f := range fields {
if f.Name == "creator" {
hasCreator = true
break
}
}
}
if hasCreator { if hasCreator {
authConditions = append(authConditions, "`creator` = ?") authConditions = append(authConditions, "creator = ?")
authArgs = append(authArgs, t.userID) authArgs = append(authArgs, t.userID)
} }
@ -213,18 +234,19 @@ func (t *Table) reconstructAndSyncSchema() error {
} }
sb.WriteString("\n") sb.WriteString("\n")
// Always ensure standard fields are present with appropriate indices hasID := false
sb.WriteString(" id c10 PK\n") for _, f := range tblFields {
sb.WriteString(" createTime bi I\n") if cast.String(f["name"]) == "id" {
sb.WriteString(" creator v64\n") hasID = true
sb.WriteString(" updateTime bi I\n") break
sb.WriteString(" updater v64\n") }
}
if !hasID {
sb.WriteString(" id c10 PK\n")
}
for _, f := range tblFields { for _, f := range tblFields {
fname := cast.String(f["name"]) fname := cast.String(f["name"])
if fname == "id" || fname == "createTime" || fname == "creator" || fname == "updateTime" || fname == "updater" {
continue
}
ftype := cast.String(f["type"]) ftype := cast.String(f["type"])
if ftype == "" { if ftype == "" {
ftype = "v255" ftype = "v255"
@ -244,11 +266,7 @@ func (t *Table) reconstructAndSyncSchema() error {
sb.WriteString("\n") sb.WriteString("\n")
} }
err := t.db.Sync(sb.String()) return t.db.Sync(sb.String())
if err != nil {
return err
}
return GlobalCache.Load(t.app)
} }
// Set performs an upsert of one or more records. // Set performs an upsert of one or more records.
@ -258,17 +276,6 @@ func (t *Table) Set(data ...any) error {
} }
metaTouched := false metaTouched := false
var updatedRows []map[string]any
// Pre-fetch schema context for business tables
var tableSchema *TableSchema
var fields []FieldSchema
tableRec := GlobalCache.GetTable(t.Name)
if tableRec != nil {
cast.Convert(&tableSchema, tableRec)
fields = GlobalCache.GetFields(tableSchema.ID)
}
for _, d := range data { for _, d := range data {
record := make(map[string]any) record := make(map[string]any)
cast.Convert(&record, d) cast.Convert(&record, d)
@ -280,7 +287,8 @@ func (t *Table) Set(data ...any) error {
} }
if t.app.hooks.OnUpdatingRow != nil && t.Name != "_Table" && t.Name != "_Field" { if t.app.hooks.OnUpdatingRow != nil && t.Name != "_Table" && t.Name != "_Field" {
if err := t.app.hooks.OnUpdatingRow(record, tableSchema, fields); err != nil {
if err := t.app.hooks.OnUpdatingRow(t.Name, record); err != nil {
return err return err
} }
} }
@ -288,6 +296,10 @@ func (t *Table) Set(data ...any) error {
idVal := record["id"] idVal := record["id"]
var isInsert bool var isInsert bool
if idVal == nil || cast.String(idVal) == "" { if idVal == nil || cast.String(idVal) == "" {
record["id"] = t.db.NextID(t.Name)
if record["id"] == "" {
record["id"] = id.MakeID(10)
}
isInsert = true isInsert = true
} else { } else {
idStr := cast.String(idVal) idStr := cast.String(idVal)
@ -298,33 +310,52 @@ func (t *Table) Set(data ...any) error {
return err return err
} }
isInsert = false isInsert = false
// Prevent overwriting CreateTime and Creator on update
delete(record, "createTime")
delete(record, "creator")
} else { } else {
isInsert = true isInsert = true
} }
} }
// Always update updateTime and updater
record["updateTime"] = time.Now().UnixMilli()
record["updater"] = t.userID
var err error var err error
if isInsert { if isInsert {
record["createTime"] = record["updateTime"] hasCreator := false
if t.userID != SystemUserID { hasCreateTime := false
if t.Name == "_Policy" || t.Name == "_Backup" { tableRec := GlobalCache.GetTable(t.Name)
return fmt.Errorf("permission denied for %s", t.Name) if tableRec != nil {
tid := cast.String(tableRec["id"])
fields := GlobalCache.GetFields(tid)
for _, f := range fields {
if f.Name == "creator" {
hasCreator = true
}
if f.Name == "createTime" {
hasCreateTime = true
}
} }
record["creator"] = t.userID }
} else {
if record["creator"] == nil { if hasCreateTime || t.Name == "_Table" || t.Name == "_Field" {
record["createTime"] = time.Now().UnixMilli()
}
if t.userID != SystemUserID {
if hasCreator || strings.HasPrefix(t.Name, "_") {
if t.Name == "_Policy" || t.Name == "_Backup" {
return fmt.Errorf("permission denied for %s", t.Name)
}
record["creator"] = t.userID record["creator"] = t.userID
} }
} else {
if hasCreator || strings.HasPrefix(t.Name, "_") {
if record["creator"] == nil {
record["creator"] = t.userID
}
}
} }
err = t.db.Insert(t.Name, record).Error err = t.db.Insert(t.Name, record).Error
} else { } else {
// Prevent overwriting CreateTime and Creator on update
delete(record, "createTime")
delete(record, "creator")
err = t.db.Update(t.Name, record, "id = ?", cast.String(record["id"])).Error err = t.db.Update(t.Name, record, "id = ?", cast.String(record["id"])).Error
} }
@ -336,28 +367,17 @@ func (t *Table) Set(data ...any) error {
metaTouched = true metaTouched = true
if t.Name == "_Table" { if t.Name == "_Table" {
if isInsert && t.app.hooks.OnCreatedTable != nil { if isInsert && t.app.hooks.OnCreatedTable != nil {
var ts TableSchema t.app.hooks.OnCreatedTable(cast.String(record["name"]), record)
cast.Convert(&ts, record)
t.app.hooks.OnCreatedTable(&ts)
} }
} else if t.Name == "_Field" { } else if t.Name == "_Field" {
if t.app.hooks.OnUpdatedField != nil { if t.app.hooks.OnUpdatedField != nil {
var fs FieldSchema t.app.hooks.OnUpdatedField(cast.String(record["tableId"]), cast.String(record["name"]), record)
cast.Convert(&fs, record)
// Look up table schema
var fts *TableSchema
GlobalCache.lock.RLock()
tableName := GlobalCache.TableIDMap[fs.TableID]
fTableRec := GlobalCache.Tables[tableName]
GlobalCache.lock.RUnlock()
if fTableRec != nil {
cast.Convert(&fts, fTableRec)
}
t.app.hooks.OnUpdatedField(fts, &fs)
} }
} }
} else { } else {
updatedRows = append(updatedRows, record) if t.app.hooks.OnUpdatedRows != nil {
t.app.hooks.OnUpdatedRows(t.Name, 1)
}
} }
} }
@ -368,10 +388,6 @@ func (t *Table) Set(data ...any) error {
_ = GlobalCache.Load(t.app) _ = GlobalCache.Load(t.app)
} }
if len(updatedRows) > 0 && t.app.hooks.OnUpdatedRows != nil {
t.app.hooks.OnUpdatedRows(updatedRows, tableSchema, fields)
}
return nil return nil
} }
@ -484,17 +500,6 @@ func (t *Table) Remove(ids ...string) error {
return fmt.Errorf("authentication required") return fmt.Errorf("authentication required")
} }
var removedIDs []string
// Pre-fetch schema context for business tables
var tableSchema *TableSchema
var fields []FieldSchema
tableRec := GlobalCache.GetTable(t.Name)
if tableRec != nil {
cast.Convert(&tableSchema, tableRec)
fields = GlobalCache.GetFields(tableSchema.ID)
}
for _, id := range ids { for _, id := range ids {
if err := t.checkAuth(id, "write"); err != nil { if err := t.checkAuth(id, "write"); err != nil {
return err return err
@ -515,49 +520,55 @@ func (t *Table) Remove(ids ...string) error {
_ = GlobalCache.Load(t.app) _ = GlobalCache.Load(t.app)
if t.Name == "_Table" && record != nil && t.app.hooks.OnRemovedTable != nil { if t.Name == "_Table" && record != nil && t.app.hooks.OnRemovedTable != nil {
var ts TableSchema t.app.hooks.OnRemovedTable(cast.String(record["name"]))
cast.Convert(&ts, record)
t.app.hooks.OnRemovedTable(&ts)
} else if t.Name == "_Field" && record != nil && t.app.hooks.OnRemovedField != nil { } else if t.Name == "_Field" && record != nil && t.app.hooks.OnRemovedField != nil {
var fts *TableSchema t.app.hooks.OnRemovedField(cast.String(record["tableId"]), cast.String(record["name"]))
GlobalCache.lock.RLock()
tableName := GlobalCache.TableIDMap[cast.String(record["tableId"])]
fTableRec := GlobalCache.Tables[tableName]
GlobalCache.lock.RUnlock()
if fTableRec != nil {
cast.Convert(&fts, fTableRec)
}
t.app.hooks.OnRemovedField(fts, cast.String(record["name"]))
} }
} else { } else {
removedIDs = append(removedIDs, id) if t.app.hooks.OnRemovedRows != nil {
t.app.hooks.OnRemovedRows(t.Name, []string{id})
}
} }
} else { } else {
return res.Error return res.Error
} }
} }
if len(removedIDs) > 0 && t.app.hooks.OnRemovedRows != nil {
t.app.hooks.OnRemovedRows(removedIDs, tableSchema, fields)
}
return nil return nil
} }
// List retrieves multiple records. // List retrieves multiple records.
func (t *Table) List(where any, args ...any) ([]map[string]any, error) { func (t *Table) List(where any, args ...any) ([]map[string]any, error) {
req := QueryRequest{} if t.userID == "" {
return nil, fmt.Errorf("authentication required")
}
query := fmt.Sprintf("SELECT * FROM `%s` ", t.Name)
whereStr := ""
if where != nil { if where != nil {
switch v := where.(type) { switch v := where.(type) {
case string: case string:
req.Where = v whereStr = v
req.Args = args
case map[string]any: case map[string]any:
req.Where, req.Args = buildWhere(v) whereStr, args = buildWhere(v)
} }
} }
return t.Query(req) var err error
whereStr, args, err = t.appendAuthAndConstraint(whereStr, args)
if err != nil {
return nil, err
}
if whereStr != "" {
query += " WHERE " + whereStr
}
res := t.db.Query(query, args...)
if res.Error != nil {
return nil, res.Error
}
return res.MapResults(), nil
} }
// Query performs a structured query on the current table. // Query performs a structured query on the current table.
@ -566,15 +577,7 @@ func (t *Table) Query(req QueryRequest) ([]map[string]any, error) {
return nil, fmt.Errorf("authentication required") return nil, fmt.Errorf("authentication required")
} }
// Apply constraint to where clause sql, args, err := t.app.buildQuery(t.Name, req)
if t.constraint != nil {
if req.Where != "" {
// If it's a string, this is complex to merge safely without parsing SQL.
// Let's rely on appendAuthAndConstraint passing the constraint.
}
}
sql, args, err := t.app.buildQuery(t, req)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -14,27 +14,23 @@ func setupDB(t *testing.T, dbFile string) *TableDB {
_ = os.Remove(dbFile) _ = os.Remove(dbFile)
logger := log.DefaultLogger logger := log.DefaultLogger
logger.SetLevel(log.ERROR) logger.SetLevel(log.ERROR)
dbInst := GetDB("sqlite://"+dbFile, logger, "") dbInst := GetDB("sqlite://"+dbFile, logger).Auth(SystemUserID)
// Bootstrap system tables // Bootstrap system tables
err := dbInst.Bootstrap() _ = dbInst.syncSchema("")
if err != nil { return dbInst
panic(fmt.Sprintf("Bootstrap failed in setupDB: %v", err))
}
return dbInst.Auth(SystemUserID)
} }
func TestBootstrapAndSync(t *testing.T) { func TestBootstrapAndSync(t *testing.T) {
dbFile := "test_bootstrap.db" dbFile := "test_bootstrap.db"
_ = os.Remove(dbFile) _ = os.Remove(dbFile)
unauth := GetDB("sqlite://"+dbFile, log.DefaultLogger, "") dbInst := GetDB("sqlite://"+dbFile, log.DefaultLogger).Auth(SystemUserID)
defer os.Remove(dbFile) defer os.Remove(dbFile)
// 1. Bootstrap: sync system DSL // 1. Bootstrap: sync empty DSL, should still create system tables
err := unauth.Bootstrap() err := dbInst.syncSchema("")
if err != nil { if err != nil {
t.Fatalf("Bootstrap failed: %v", err) t.Fatalf("Bootstrap failed: %v", err)
} }
dbInst := unauth.Auth(SystemUserID)
// Verify system tables in cache // Verify system tables in cache
if GlobalCache.GetTable("_Table") == nil { if GlobalCache.GetTable("_Table") == nil {
@ -93,13 +89,6 @@ func TestAPIDrivenSchema(t *testing.T) {
if len(res) != 1 || res[0]["title"] != "Fix tests" { if len(res) != 1 || res[0]["title"] != "Fix tests" {
t.Errorf("Query from tasks failed: %v", res) t.Errorf("Query from tasks failed: %v", res)
} }
// Verify ID is automatically generated
if idVal := cast.String(res[0]["id"]); len(idVal) == 0 {
t.Errorf("ID was not automatically generated")
} else {
fmt.Printf("Generated ID: %s\n", idVal)
}
} }
func TestDataOperations(t *testing.T) { func TestDataOperations(t *testing.T) {
@ -312,7 +301,6 @@ func TestPolicyInterfaces(t *testing.T) {
// 5. List policies // 5. List policies
pols, err := user1.ListPolicy(map[string]any{"userID": "user2"}) pols, err := user1.ListPolicy(map[string]any{"userID": "user2"})
if err != nil || len(pols) == 0 || pols[0].UserID != "user2" { if err != nil || len(pols) == 0 || pols[0].UserID != "user2" {
t.Errorf("ListPolicy failed: %v, %v", err, pols) t.Errorf("ListPolicy failed: %v, %v", err, pols)
} }
@ -326,11 +314,11 @@ func TestHooks(t *testing.T) {
var tableCreated string var tableCreated string
var rowsUpdated int var rowsUpdated int
dbInst.hooks.OnCreatedTable = func(table *TableSchema) { dbInst.hooks.OnCreatedTable = func(tableName string, record map[string]any) {
tableCreated = table.Name tableCreated = tableName
} }
dbInst.hooks.OnUpdatedRows = func(rows []map[string]any, table *TableSchema, fields []FieldSchema) { dbInst.hooks.OnUpdatedRows = func(tableName string, count int) {
rowsUpdated += len(rows) rowsUpdated += count
} }
app := dbInst.Auth("admin") app := dbInst.Auth("admin")
@ -381,9 +369,8 @@ func BenchmarkTableSet(b *testing.B) {
dbFile := fmt.Sprintf("bench_ops_%d.db", time.Now().UnixNano()) dbFile := fmt.Sprintf("bench_ops_%d.db", time.Now().UnixNano())
defer os.Remove(dbFile) defer os.Remove(dbFile)
unauth := GetDB("sqlite://"+dbFile, logger, "") app := GetDB("sqlite://"+dbFile, logger).Auth(SystemUserID)
_ = unauth.Bootstrap() _ = app.syncSchema("")
app := unauth.Auth(SystemUserID)
_ = app.Table("bench_ops").SetField( _ = app.Table("bench_ops").SetField(
FieldSchema{Name: "name", Type: "v50", IsIndex: true}, FieldSchema{Name: "name", Type: "v50", IsIndex: true},
FieldSchema{Name: "val", Type: "i"}, FieldSchema{Name: "val", Type: "i"},

View File

@ -6,11 +6,8 @@ type TableSchema struct {
Memo string Memo string
EnableRLS bool EnableRLS bool
Settings map[string]any Settings map[string]any
IsSecret bool // 是否敏感表(不被索引)
CreateTime int64 CreateTime int64
Creator string Creator string
UpdateTime int64
Updater string
} }
type FlatPolicy struct { type FlatPolicy struct {