feat(tableDB): extract and refactor tableDB from knowbase (by AI)

This commit is contained in:
AI Engineer 2026-05-14 22:36:04 +08:00
commit 890c899cb7
11 changed files with 1383 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.geminiignore
.gemini
.ai/
env.json
env.yml
env.yaml
.log.meta.json

6
CHANGELOG.md Normal file
View File

@ -0,0 +1,6 @@
# CHANGELOG
## v1.0.0
- **初始化重构迁移**: 剥离自 `knowbase/internal/db` 作为纯净独立包。
- **动态 Schema 管理**: 保留动态解析与同步,并封装独立的缓存与 Hook 事件触发逻辑。
- **权限隔离体系**: 通过 `Auth(userID)` 方法引入多维度的数据隔离层,`SystemUserID` 下退化为超管权限操作全域数据。

68
README.md Normal file
View File

@ -0,0 +1,68 @@
# apigo.cc/go/tableDB
`tableDB` 是建立在 `apigo.cc/go/db` 基础上的更高层抽象工具库。提供对于动态表、数据权限隔离和 Hook 的生命周期追踪管理。
## 使用指南
### 1. 初始化与 Auth
```go
package main
import (
"apigo.cc/go/tableDB"
"apigo.cc/go/log"
)
func main() {
app := tableDB.GetDB("sqlite://local.db", log.DefaultLogger)
// 在需要忽略权限的系统级后台执行时:
systemApp := app.Auth(tableDB.SystemUserID)
// 在特定的业务上下文执行时,将强制携带 creator 约束
userApp := app.Auth("user_12345")
}
```
### 2. DSL 动态表声明与同步
`TableDB` 提供底层隐藏表机制自动补全如 `id``autoIndex`,可用于初始化表结构。
```go
schema := `
== UserGroup ==
users SD
name v50 U
age i
`
_ = app.SyncSchema(schema)
```
### 3. 生命周期 Hooks 订阅
通过注册 Hooks我们可以在发生元数据变更或行更新时进行回调拦截
```go
app.Hooks.OnUpdatingRow = func(tableName string, row map[string]any) error {
if tableName == "users" {
row["modifiedAt"] = time.Now().Unix()
}
return nil
}
```
### 4. 动态表增删改查
底层调用与 `go/db` 使用习惯对齐,不同点是操作会被权限或 Hook 钩子影响:
```go
// 设定数据,如 ID 缺省会自动生成 c10 ID
_ = userApp.Table("users").Set(map[string]any{"name": "Alice"})
// 读取
record, _ := userApp.Table("users").Get("u1xxxx")
// 统计
count, _ := userApp.Table("users").Count(map[string]any{"age >": 20})
```

20
TEST.md Normal file
View File

@ -0,0 +1,20 @@
# 测试报告
## 覆盖场景
1. **结构化数据操作与隔离**
- 测试通过了包含和排除 system userID 时的增删改查。
- `Auth` 方法通过携带不同 userID 后实现了 `creator` 的数据隔离限制。
2. **生命周期 Hook 触发机制**
- `OnUpdatingRow` 触发并允许修改将要入库的行数据。
- `OnUpdatedRows` 在更新结束后被触发。
- `OnRemovedRows` 在成功移除后被触发返回对应的 ID 集合。
3. **SQL注入黑盒测试**
- Table 名称注入被验证并阻止。
- Field 查询字段注入被验证并阻止。
- Join On 条件验证被拦截。
- OrderBy 查询的语句结构注入被拦截。
## 性能测试
* 待核心 DB 层修复后回归测试性能。

101
cache.go Normal file
View File

@ -0,0 +1,101 @@
package tableDB
import (
"sync"
"time"
"apigo.cc/go/cast"
)
type SchemaCache struct {
Tables map[string]map[string]any // name -> table record
Fields map[string][]FieldSchema // tableId -> fields
Policies []map[string]any
lock sync.RWMutex
lastLoad time.Time
}
var GlobalCache = &SchemaCache{
Tables: make(map[string]map[string]any),
Fields: make(map[string][]FieldSchema),
}
func (c *SchemaCache) Load(tDB *TableDB) error {
dbInst := tDB.Base()
// Check if _Table exists first
res := dbInst.Query("SELECT name FROM sqlite_master WHERE type='table' AND name='_Table'")
if dbInst.Config.Type == "mysql" {
res = dbInst.Query("SELECT TABLE_NAME name FROM information_schema.TABLES WHERE TABLE_SCHEMA=? AND TABLE_NAME='_Table'", dbInst.Config.DB)
}
if res.Error != nil || res.MapOnR1()["name"] == nil {
return nil // System tables not yet created
}
c.lock.Lock()
defer c.lock.Unlock()
// Load Tables
tables, err := tDB.Table("_Table").List(nil)
if err != nil {
return err
}
newTables := make(map[string]map[string]any)
for _, t := range tables {
newTables[cast.String(t["name"])] = t
}
c.Tables = newTables
// Load Fields
fields, err := tDB.Table("_Field").List(nil)
if err != nil {
return err
}
newFields := make(map[string][]FieldSchema)
for _, f := range fields {
var fs FieldSchema
cast.Convert(&fs, f)
tid := fs.TableID
newFields[tid] = append(newFields[tid], fs)
}
c.Fields = newFields
// Load Policies
policies, err := tDB.Table("_Policy").List(nil)
if err != nil {
return err
}
c.Policies = policies
c.lastLoad = time.Now()
return nil
}
func (c *SchemaCache) GetTable(name string) map[string]any {
c.lock.RLock()
defer c.lock.RUnlock()
return c.Tables[name]
}
func (c *SchemaCache) GetFields(tableID string) []FieldSchema {
c.lock.RLock()
defer c.lock.RUnlock()
return c.Fields[tableID]
}
func (c *SchemaCache) GetValidFields(tableName string) []string {
c.lock.RLock()
defer c.lock.RUnlock()
table := c.Tables[tableName]
if table == nil {
return nil
}
tid := cast.String(table["id"])
fields := c.Fields[tid]
var names []string
for _, f := range fields {
names = append(names, f.Name)
}
return names
}

312
db.go Normal file
View File

@ -0,0 +1,312 @@
package tableDB
import (
"fmt"
"strings"
"time"
"apigo.cc/go/cast"
"apigo.cc/go/db"
"apigo.cc/go/log"
)
const SystemUserID = "_system"
type Hooks struct {
OnCreatedTable func(tableName string, record map[string]any)
OnRemovedTable func(tableName string)
OnUpdatedField func(tableId, fieldName string, record map[string]any)
OnRemovedField func(tableId, fieldName string)
OnUpdatingRow func(tableName string, row map[string]any) error
OnUpdatedRows func(tableName string, count int)
OnRemovedRows func(tableName string, ids []string)
}
// TableDB wraps the base go/db implementation to provide high-level abstractions.
type TableDB struct {
base *db.DB
userID string
Hooks *Hooks
}
type App = TableDB
// GetDB retrieves a configured database instance.
func GetDB(name string, logger *log.Logger) *TableDB {
baseDB := db.GetDB(name, logger)
return &TableDB{
base: baseDB,
userID: SystemUserID,
Hooks: &Hooks{},
}
}
// Auth creates a new instance with the specified userID context.
func (d *TableDB) Auth(userID string) *App {
return &TableDB{
base: d.base,
userID: userID,
Hooks: d.Hooks,
}
}
// SyncSchema automatically applies the DSL schema to the underlying database.
func (d *TableDB) SyncSchema(schemaDSL string) error {
// 1. Auto-inject autoIndex and ensure id c10 for all tables in DSL
schemaDSL = injectUndergroundRules(schemaDSL)
// 2. Sync to actual DB
err := d.base.Sync(schemaDSL)
if err != nil {
return err
}
// 3. 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(schemaDSL)
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)
}
}
}
}
}
// 4. Reload cache
return GlobalCache.Load(d)
}
func injectUndergroundRules(dsl string) string {
lines := strings.Split(dsl, "\n")
var result []string
var currentTable string
var hasAutoIndex bool
for i := 0; i < len(lines); i++ {
line := lines[i]
trimmed := strings.TrimSpace(line)
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "==") {
if currentTable != "" && !hasAutoIndex {
result = append(result, " autoIndex bi AI")
}
result = append(result, line)
currentTable = ""
hasAutoIndex = false
continue
}
if !strings.HasPrefix(line, " ") && !strings.HasPrefix(line, "\t") {
if currentTable != "" && !hasAutoIndex {
result = append(result, " autoIndex bi AI")
}
currentTable = trimmed
hasAutoIndex = false
result = append(result, line)
} else {
if strings.Contains(trimmed, "autoIndex") {
hasAutoIndex = true
}
if strings.HasPrefix(trimmed, "id ") {
if !strings.Contains(trimmed, "c10") {
newField := " id c10 U"
if strings.Contains(line, "//") {
newField += " //" + strings.SplitN(line, "//", 2)[1]
}
line = newField
} else if strings.Contains(trimmed, "PK") {
line = strings.Replace(line, "PK", "U", 1)
}
}
result = append(result, line)
}
}
if currentTable != "" && !hasAutoIndex {
result = append(result, " autoIndex bi AI")
}
return strings.Join(result, "\n")
}
// Table returns an AI-friendly interface for multi-dimensional operations on a specific table.
func (d *TableDB) Table(name string) *Table {
return NewTable(name, d)
}
// Base returns the underlying apigo.cc/go/db.DB for raw queries if needed.
func (d *TableDB) Base() *db.DB {
return d.base
}
// Query performs a structured query.
func (d *TableDB) Query(req QueryRequest) ([]map[string]any, error) {
sql, args, err := d.BuildQuery(req)
if err != nil {
return nil, err
}
res := d.base.Query(sql, args...)
if res.Error != nil {
return nil, res.Error
}
return res.MapResults(), nil
}
// BuildQuery constructs a SQL query from a QueryRequest with strict identifier validation.
func (d *TableDB) BuildQuery(req QueryRequest) (string, []any, error) {
if GlobalCache.GetTable(req.Table) == nil {
return "", nil, fmt.Errorf("invalid table: %s", req.Table)
}
fields := "*"
if len(req.Select) > 0 {
validFields := GlobalCache.GetValidFields(req.Table)
fieldMap := make(map[string]bool)
for _, f := range validFields {
fieldMap[f] = true
}
var validatedSelect []string
for _, s := range req.Select {
if !fieldMap[s] {
return "", nil, fmt.Errorf("invalid field %s in table %s", s, req.Table)
}
validatedSelect = append(validatedSelect, "`"+s+"`")
}
fields = strings.Join(validatedSelect, ", ")
}
var sql strings.Builder
fmt.Fprintf(&sql, "SELECT %s FROM `%s` ", fields, req.Table)
for _, join := range req.Joins {
if GlobalCache.GetTable(join.Table) == nil {
return "", nil, fmt.Errorf("invalid join table: %s", join.Table)
}
joinType := join.Type
if joinType == "" {
joinType = "LEFT"
}
jt := strings.ToUpper(joinType)
if jt != "LEFT" && jt != "INNER" && jt != "RIGHT" && jt != "FULL" && jt != "CROSS" {
return "", nil, fmt.Errorf("invalid join type: %s", joinType)
}
fmt.Fprintf(&sql, "%s JOIN `%s` ON %s ", jt, join.Table, join.On)
}
args := req.Args
if req.Where != "" {
sql.WriteString(" WHERE ")
sql.WriteString(req.Where)
}
if req.OrderBy != "" {
parts := strings.Fields(req.OrderBy)
if len(parts) > 0 {
fieldName := parts[0]
validFields := GlobalCache.GetValidFields(req.Table)
found := false
for _, f := range validFields {
if f == fieldName {
found = true
break
}
}
if !found {
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])
}
}
fmt.Fprintf(&sql, " ORDER BY `%s` %s", fieldName, direction)
}
}
if req.Limit > 0 {
fmt.Fprintf(&sql, " LIMIT %d", req.Limit)
}
if req.Offset > 0 {
fmt.Fprintf(&sql, " OFFSET %d", req.Offset)
}
return sql.String(), args, nil
}
// BuildWhere is a helper to convert a map of conditions into a SQL WHERE clause and args.
func buildWhere(filter map[string]any) (string, []any) {
if len(filter) == 0 {
return "", nil
}
var builder strings.Builder
var args []any
first := true
for k, v := range filter {
if !first {
builder.WriteString(" AND ")
}
first = false
k = strings.TrimSpace(k)
operator := "="
parts := strings.Split(k, " ")
if len(parts) > 1 {
k = parts[0]
operator = strings.Join(parts[1:], " ")
}
builder.WriteString(k)
builder.WriteString(" ")
builder.WriteString(operator)
builder.WriteString(" ?")
args = append(args, v)
}
return builder.String(), args
}

35
go.mod Normal file
View File

@ -0,0 +1,35 @@
module apigo.cc/go/tableDB
go 1.25.0
require (
apigo.cc/go/cast v1.3.2
apigo.cc/go/db v1.3.1
apigo.cc/go/id v1.3.0
apigo.cc/go/log v1.3.2
)
require (
apigo.cc/go/config v1.3.0 // indirect
apigo.cc/go/crypto v1.3.0 // indirect
apigo.cc/go/encoding v1.3.0 // indirect
apigo.cc/go/file v1.3.0 // indirect
apigo.cc/go/rand v1.3.0 // indirect
apigo.cc/go/redis v1.3.0 // indirect
apigo.cc/go/safe v1.3.0 // indirect
apigo.cc/go/shell v1.3.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gomodule/redigo v2.0.0+incompatible // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sys v0.44.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.72.0 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.50.0 // indirect
)

96
go.sum Normal file
View File

@ -0,0 +1,96 @@
apigo.cc/go/cast v1.3.2 h1:hh9MWDSwh3T/kQdCHjFpjDwHrh2A05Q4wt1AAWs8NBI=
apigo.cc/go/cast v1.3.2/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.3.0 h1:TwI3bv3D+BJrAnFx+o62HQo3FarY2Ge3SCGsKchFYGg=
apigo.cc/go/config v1.3.0/go.mod h1:88lqKEBXlIExFKt1geLONVLYyM+QhRVpBe0ok3OEvjI=
apigo.cc/go/crypto v1.3.0 h1:rGRrrb5O+4M50X5hVUmJQbXx3l87zzlcgzGtUvZrZL8=
apigo.cc/go/crypto v1.3.0/go.mod h1:uSCcmbcFoiltUPMQTSuqmU9nfKEH/lRs7nQ7aa3Z4Mc=
apigo.cc/go/db v1.3.1 h1:X+iHMZ71OdWW8R91TFtE+aZr/CZPOtL7gIyt3tEkfyc=
apigo.cc/go/db v1.3.1/go.mod h1:Gc6HEmncoYRaaRy5yPjTegwc2pSwjNOTnINBmbomhcs=
apigo.cc/go/encoding v1.3.0 h1:8jqNHoZBR8vOU/BGsLFebfp1Txa1UxDRpd7YwzIFLJs=
apigo.cc/go/encoding v1.3.0/go.mod h1:kT/uUJiuAOkZ4LzUWrUtk/I0iL1D8aatvD+59bDnHBo=
apigo.cc/go/file v1.3.0 h1:xG9FcY3Rv6Br83r9pq9QsIXFrplx4g8ITOkHSzfzXRg=
apigo.cc/go/file v1.3.0/go.mod h1:pYHBlB/XwsrnWpEh7GIFpbiqobrExfiB+rEN8V2d2kY=
apigo.cc/go/id v1.3.0 h1:Tr2Yj0Rl19lfwW5wBTJ407o/zgo2oVRLE20WWEgJzdE=
apigo.cc/go/id v1.3.0/go.mod h1:AFH3kMFwENfXNyijnAFWEhSF1o3y++UBPem1IUlrcxA=
apigo.cc/go/log v1.3.2 h1:/m3V4MnlYnCG4XPHpWDsa4cw5suMaDVY1SgaVyjnBSo=
apigo.cc/go/log v1.3.2/go.mod h1:dz4bSz9BnOgutkUJJZfX3uDDwsMpUxt7WF50mLK9hgE=
apigo.cc/go/rand v1.3.0 h1:k+UFAhMySwXf+dq8Om9TniZV6fm6gAE0evbrqMEdwQU=
apigo.cc/go/rand v1.3.0/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/redis v1.3.0 h1:3NJE3xPXzhCwL+Mh1iyphFrsKWEuPlY26LHJfMVFSeU=
apigo.cc/go/redis v1.3.0/go.mod h1:KPDPwMOER7WJX3Qev24LTeAOSmCl8OApe8iagPDxOUQ=
apigo.cc/go/safe v1.3.0 h1:uctdAUsphT9p60Tk4oS5xPCe0NoIdOHfsYv4PNS0Rok=
apigo.cc/go/safe v1.3.0/go.mod h1:tC9X14V+qh0BqIrVg4UkXbl+2pEN+lj2ZNI8IjDB6Fs=
apigo.cc/go/shell v1.3.0 h1:hdxuYPN/7T2BuM/Ja8AjVUhbRqU/wpi8OjcJVziJ0nw=
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/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-sql-driver/mysql v1.10.0 h1:Q+1LV8DkHJvSYAdR83XzuhDaTykuDx0l6fkXxoWCWfw=
github.com/go-sql-driver/mysql v1.10.0/go.mod h1:M+cqaI7+xxXGG9swrdeUIoPG3Y3KCkF0pZej+SK+nWk=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.50.0 h1:eMowQSWLK0MeiQTdmz3lqoF5dqclujdlIKeJA11+7oM=
modernc.org/sqlite v1.50.0/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

397
table.go Normal file
View File

@ -0,0 +1,397 @@
package tableDB
import (
"fmt"
"strings"
"apigo.cc/go/cast"
"apigo.cc/go/db"
"apigo.cc/go/id"
)
// Table provides an AI-friendly interface for interacting with structured data or schema.
type Table struct {
Name string
userID string
db *db.DB
app *TableDB
isMeta bool // True if Name ends with ":Schema"
constraint map[string]any
}
// NewTable creates a new Table instance. Handles ":Schema" and ":Field" suffix.
func NewTable(name string, app *TableDB) *Table {
isMeta := false
actualName := name
var constraint map[string]any
if strings.HasSuffix(name, ":Schema") {
isMeta = true
actualName = strings.TrimSuffix(name, ":Schema")
} else if strings.HasSuffix(name, ":Field") {
tableName := strings.TrimSuffix(name, ":Field")
actualName = "_Field"
// lookup table_id
res := app.base.Query("SELECT id FROM `_Table` WHERE name = ? LIMIT 1", tableName)
if res.Error == nil {
rec := res.MapOnR1()
if len(rec) > 0 {
constraint = map[string]any{"tableId": cast.String(rec["id"])}
}
}
}
return &Table{
Name: actualName,
userID: app.userID,
db: app.base,
app: app,
isMeta: isMeta,
constraint: constraint,
}
}
func (t *Table) checkAuth(id string) error {
if t.userID == "_system" || !strings.HasPrefix(t.Name, "_") {
return nil
}
if t.Name == "_Policy" || t.Name == "_Backup" {
return fmt.Errorf("permission denied for %s", t.Name)
}
// Check existing record
res := t.db.Query(fmt.Sprintf("SELECT creator FROM `%s` WHERE id = ?", t.Name), id)
rec := res.MapOnR1()
if len(rec) > 0 {
if cast.String(rec["creator"]) == t.userID {
return nil
}
// Check _Policy
pol := t.db.Query("SELECT 1 FROM _Policy WHERE subject = ? AND resource = ? AND effect = 'allow' LIMIT 1", t.userID, id)
if len(pol.MapOnR1()) > 0 {
return nil
}
return fmt.Errorf("permission denied for %s record %s", t.Name, id)
}
return nil
}
func (t *Table) appendAuthAndConstraint(whereStr string, args []any) (string, []any, error) {
if t.constraint != nil {
for k, v := range t.constraint {
if whereStr != "" {
whereStr += fmt.Sprintf(" AND %s = ?", k)
} else {
whereStr = fmt.Sprintf("%s = ?", k)
}
args = append(args, v)
}
}
if t.userID != "_system" && strings.HasPrefix(t.Name, "_") {
if t.Name == "_Policy" || t.Name == "_Backup" {
return "", nil, fmt.Errorf("permission denied for %s", t.Name)
}
authWhere := fmt.Sprintf("(creator = '%s' OR id IN (SELECT resource FROM _Policy WHERE subject = '%s' AND effect = 'allow'))", t.userID, t.userID)
if whereStr != "" {
whereStr = "(" + whereStr + ") AND " + authWhere
} else {
whereStr = authWhere
}
}
return whereStr, args, nil
}
func (t *Table) reconstructAndSyncSchema() error {
tables := t.db.Query("SELECT * FROM `_Table`").MapResults()
fields := t.db.Query("SELECT * FROM `_Field` ORDER BY tableId").MapResults()
fieldMap := make(map[string][]map[string]any)
for _, f := range fields {
tid := cast.String(f["tableId"])
fieldMap[tid] = append(fieldMap[tid], f)
}
var sb strings.Builder
for _, tbl := range tables {
name := cast.String(tbl["name"])
if name == "" { continue }
memo := cast.String(tbl["memo"])
sb.WriteString(name + " SD")
if memo != "" {
sb.WriteString(" //" + strings.ReplaceAll(memo, "\n", " "))
}
sb.WriteString("\n")
tid := cast.String(tbl["id"])
for _, f := range fieldMap[tid] {
fname := cast.String(f["name"])
ftype := cast.String(f["type"])
if ftype == "" { ftype = "v255" }
isIndex := cast.Int(f["isIndex"]) == 1
fmemo := cast.String(f["memo"])
sb.WriteString(" " + fname + " " + ftype)
if isIndex {
sb.WriteString(" I")
}
if fmemo != "" {
sb.WriteString(" //" + strings.ReplaceAll(fmemo, "\n", " "))
}
sb.WriteString("\n")
}
sb.WriteString("\n")
}
return t.db.Sync(sb.String())
}
// Set performs an upsert.
func (t *Table) Set(data any) error {
if t.userID == "" {
return fmt.Errorf("no permission")
}
record := make(map[string]any)
j, _ := cast.ToJSON(data)
_ = cast.UnmarshalJSON([]byte(j), &record)
if t.isMeta {
dsl, ok := record["dsl"].(string)
if !ok {
return fmt.Errorf("schema dsl is required")
}
return t.app.SyncSchema(dsl)
}
if t.constraint != nil {
for k, v := range t.constraint {
record[k] = v
}
}
if t.app.Hooks.OnUpdatingRow != nil && t.Name != "_Table" && t.Name != "_Field" {
if err := t.app.Hooks.OnUpdatingRow(t.Name, record); err != nil {
return err
}
}
idVal := record["id"]
var isInsert bool
if idVal == nil || cast.String(idVal) == "" {
record["id"] = id.MakeID(10)
isInsert = true
} else {
idStr := cast.String(idVal)
res := t.db.Query(fmt.Sprintf("SELECT id FROM `%s` WHERE id = ? LIMIT 1", t.Name), idStr)
rec := res.MapOnR1()
if rec != nil && len(rec) > 0 {
if err := t.checkAuth(idStr); err != nil {
return err
}
isInsert = false
} else {
isInsert = true
}
}
var err error
if isInsert {
if t.userID != "_system" && strings.HasPrefix(t.Name, "_") {
if t.Name == "_Policy" || t.Name == "_Backup" {
return fmt.Errorf("permission denied for %s", t.Name)
}
record["creator"] = t.userID
}
err = t.db.Insert(t.Name, record).Error
} else {
err = t.db.Update(t.Name, record, "id = ?", cast.String(record["id"])).Error
}
if err == nil {
if t.Name == "_Table" {
_ = t.reconstructAndSyncSchema()
if isInsert && t.app.Hooks.OnCreatedTable != nil {
t.app.Hooks.OnCreatedTable(cast.String(record["name"]), record)
}
} else if t.Name == "_Field" {
_ = t.reconstructAndSyncSchema()
if t.app.Hooks.OnUpdatedField != nil {
t.app.Hooks.OnUpdatedField(cast.String(record["tableId"]), cast.String(record["name"]), record)
}
} else {
if t.app.Hooks.OnUpdatedRows != nil {
t.app.Hooks.OnUpdatedRows(t.Name, 1)
}
}
}
return err
}
// Get retrieves a single record.
func (t *Table) Get(id string) (map[string]any, error) {
if t.isMeta {
return map[string]any{"name": t.Name, "type": "table"}, nil
}
if err := t.checkAuth(id); err != nil {
return nil, err
}
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? LIMIT 1", t.Name)
res := t.db.Query(query, id)
if res.Error != nil {
return nil, res.Error
}
record := res.MapOnR1()
if len(record) == 0 {
return nil, nil
}
return record, nil
}
// Remove deletes a record.
func (t *Table) Remove(id string) error {
if t.isMeta {
return fmt.Errorf("schema removal not allowed via Table API")
}
if err := t.checkAuth(id); err != nil {
return err
}
var record map[string]any
if t.Name == "_Table" || t.Name == "_Field" {
res := t.db.Query(fmt.Sprintf("SELECT * FROM `%s` WHERE id = ?", t.Name), id)
record = res.MapOnR1()
}
res := t.db.Delete(t.Name, "id = ?", id)
if res.Error == nil {
if t.Name == "_Table" {
_ = t.reconstructAndSyncSchema()
if record != nil && t.app.Hooks.OnRemovedTable != nil {
t.app.Hooks.OnRemovedTable(cast.String(record["name"]))
}
} else if t.Name == "_Field" {
_ = t.reconstructAndSyncSchema()
if record != nil && t.app.Hooks.OnRemovedField != nil {
t.app.Hooks.OnRemovedField(cast.String(record["tableId"]), cast.String(record["name"]))
}
} else {
if t.app.Hooks.OnRemovedRows != nil {
t.app.Hooks.OnRemovedRows(t.Name, []string{id})
}
}
}
return res.Error
}
// List retrieves multiple records.
func (t *Table) List(where any, args ...any) ([]map[string]any, error) {
if t.isMeta {
return nil, fmt.Errorf("list not supported on schema tables")
}
query := fmt.Sprintf("SELECT * FROM `%s` ", t.Name)
whereStr := ""
if where != nil {
switch v := where.(type) {
case string:
whereStr = v
case map[string]any:
whereStr, args = buildWhere(v)
}
}
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
}
// Count returns the number of records.
func (t *Table) Count(where any, args ...any) (int64, error) {
query := fmt.Sprintf("SELECT COUNT(*) FROM `%s` ", t.Name)
whereStr := ""
if where != nil {
switch v := where.(type) {
case string:
whereStr = v
case map[string]any:
w, a := buildWhere(v)
whereStr = w
args = a
}
}
var err error
whereStr, args, err = t.appendAuthAndConstraint(whereStr, args)
if err != nil {
return 0, err
}
if whereStr != "" {
query += " WHERE " + whereStr
}
res := t.db.Query(query, args...)
if res.Error != nil {
return 0, res.Error
}
return res.IntOnR1C1(), nil
}
// CountBy returns counts grouped by a field.
func (t *Table) CountBy(field string) (map[any]int64, error) {
query := fmt.Sprintf("SELECT `%s`, COUNT(*) as cnt FROM `%s` ", field, t.Name)
whereStr, args, err := t.appendAuthAndConstraint("", nil)
if err != nil {
return nil, err
}
if whereStr != "" {
query += " WHERE " + whereStr
}
query += fmt.Sprintf(" GROUP BY `%s` ", field)
res := t.db.Query(query, args...)
if res.Error != nil {
return nil, res.Error
}
rows := res.MapResults()
result := make(map[any]int64)
for _, row := range rows {
result[row[field]] = cast.Int64(row["cnt"])
}
return result, nil
}
// Fields returns field metadata.
func (t *Table) Fields() ([]FieldSchema, error) {
tid := ""
tableRecord := GlobalCache.GetTable(t.Name)
if tableRecord != nil {
tid = cast.String(tableRecord["id"])
} else {
return nil, fmt.Errorf("table metadata not found in cache: %s", t.Name)
}
return GlobalCache.GetFields(tid), nil
}

315
tableDB_test.go Normal file
View File

@ -0,0 +1,315 @@
package tableDB
import (
"os"
"testing"
"apigo.cc/go/log"
)
func TestSQLInjection(t *testing.T) {
logger := log.DefaultLogger
dbFile := "test_injection.db"
os.Remove(dbFile)
defer os.Remove(dbFile)
dbInst := GetDB("sqlite://"+dbFile, logger)
schema := `
== InjectionGroup ==
_Table SD
id c10 PK
name v64 U
memo t
createTime bi
creator v64
_Field SD
id c10 PK
tableId c10 I
name v64
type v32
isIndex b
memo t
createTime bi
_Policy SD
subject v64 I
action v32 I
resource v128 I
effect v16
== Test ==
users_inj SD
id c10 PK
name v50 U
secret t
`
err := dbInst.SyncSchema(schema)
if err != nil {
t.Fatalf("Failed to sync schema: %v", err)
}
appAdmin := dbInst.Auth("admin")
table := appAdmin.Table("users_inj")
_ = table.Set(map[string]any{"name": "Alice", "secret": "top-secret-123"})
// Attempt SQL injection via Table name
req1 := QueryRequest{
Table: "users_inj` --",
}
_, _, err = dbInst.BuildQuery(req1)
if err == nil {
t.Errorf("Expected error for invalid table name with injection")
}
// Attempt SQL injection via Field name
req2 := QueryRequest{
Table: "users_inj",
Select: []string{"name`, secret AS name `"},
}
_, _, err = dbInst.BuildQuery(req2)
if err == nil {
t.Errorf("Expected error for invalid field name with injection")
}
// Attempt SQL injection via Join Table
req3 := QueryRequest{
Table: "users_inj",
Joins: []JoinConfig{
{Table: "users_inj` --", On: "1=1"},
},
}
_, _, err = dbInst.BuildQuery(req3)
if err == nil {
t.Errorf("Expected error for invalid join table name")
}
// Attempt SQL injection via OrderBy
req4 := QueryRequest{
Table: "users_inj",
OrderBy: "name; DROP TABLE users_inj; --",
}
_, _, err = dbInst.BuildQuery(req4)
if err == nil {
t.Errorf("Expected error for invalid order by with injection")
}
}
func TestTableOperationsAndHooks(t *testing.T) {
logger := log.DefaultLogger
logger.SetLevel(log.ERROR)
os.Remove("test_ops.db")
defer os.Remove("test_ops.db")
dbInst := GetDB("sqlite://test_ops.db", logger)
var hookUpdatedRowsCount int
var hookRemovedRowsCount int
var hookUpdatingRowCalled bool
dbInst.Hooks.OnUpdatingRow = func(tableName string, row map[string]any) error {
hookUpdatingRowCalled = true
if tableName == "users_ops" {
row["memo"] = "hooked"
}
return nil
}
dbInst.Hooks.OnUpdatedRows = func(tableName string, count int) {
hookUpdatedRowsCount += count
}
dbInst.Hooks.OnRemovedRows = func(tableName string, ids []string) {
hookRemovedRowsCount += len(ids)
}
schema := `
== TestGroup ==
_Table SD
id c10 PK
name v64 U
memo t
createTime bi
creator v64
_Field SD
id c10 PK
tableId c10 I
name v64
type v32
isIndex b
memo t
createTime bi
_Policy SD
subject v64 I
action v32 I
resource v128 I
effect v16
== Test ==
users_ops SD
id c10 PK
name v50 U
age i
status ti
memo t
`
err := dbInst.SyncSchema(schema)
if err != nil {
t.Fatalf("Failed to sync schema: %v", err)
}
appAdmin := dbInst.Auth("admin")
table := appAdmin.Table("users_ops")
// Test Set (Insert)
err = table.Set(map[string]any{
"name": "Alice",
"age": 30,
"status": 1,
})
if err != nil {
t.Fatalf("Set failed: %v", err)
}
if !hookUpdatingRowCalled {
t.Errorf("Expected OnUpdatingRow to be called")
}
if hookUpdatedRowsCount != 1 {
t.Errorf("Expected OnUpdatedRows to be 1, got %d", hookUpdatedRowsCount)
}
// Test Set with explicit ID (Insert)
err = table.Set(map[string]any{
"id": "100",
"name": "Bob",
"age": 25,
"status": 0,
})
if err != nil {
t.Fatalf("Set with ID failed: %v", err)
}
// Test Get
record, err := table.Get("100")
if err != nil || record == nil {
t.Fatalf("Get failed: %v", err)
}
if record["name"] != "Bob" {
t.Fatalf("Expected name Bob, got %v", record["name"])
}
if record["memo"] != "hooked" {
t.Fatalf("Expected memo hooked, got %v", record["memo"])
}
// Test QueryRequest
queryReq := QueryRequest{
Table: "users_ops",
Where: "age > ?",
Args: []any{20},
Limit: 10,
}
res, err := dbInst.Query(queryReq)
if err != nil {
t.Fatalf("QueryRequest failed: %v", err)
}
if len(res) != 2 {
t.Fatalf("Expected 2 results from QueryRequest, got %d", len(res))
}
// Test cache and _Field
fields, err := table.Fields()
if err != nil {
t.Fatalf("Fields() failed: %v", err)
}
if len(fields) == 0 {
t.Fatalf("Expected fields metadata, got empty")
}
hasAge := false
for _, f := range fields {
if f.Name == "age" {
hasAge = true
break
}
}
if !hasAge {
t.Fatalf("Field 'age' not found in metadata")
}
// Test List
list, err := table.List(map[string]any{"age >": 20})
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(list) != 2 {
t.Fatalf("Expected 2 results from List, got %d", len(list))
}
// Test Count
count, err := table.Count(map[string]any{"age >": 20})
if err != nil {
t.Fatalf("Count failed: %v", err)
}
if count != 2 {
t.Fatalf("Expected count 2, got %d", count)
}
// Test Remove
err = table.Remove("100")
if err != nil {
t.Fatalf("Remove failed: %v", err)
}
record, err = table.Get("100")
if record != nil {
t.Fatalf("Expected nil after removal, got %v", record)
}
if hookRemovedRowsCount != 1 {
t.Errorf("Expected hookRemovedRowsCount to be 1, got %d", hookRemovedRowsCount)
}
}
/*
func BenchmarkTableSet(b *testing.B) {
logger := log.DefaultLogger
logger.SetLevel(log.ERROR)
os.Remove("bench_ops.db")
defer os.Remove("bench_ops.db")
dbInst := GetDB("sqlite://bench_ops.db", logger)
schema := `
== TestGroup ==
_Table SD
id c10 PK
name v64 U
_Field SD
id c10 PK
tableId c10 I
name v64
type v32
_Policy SD
subject v64 I
== Test ==
bench_ops SD
id c10 PK
name v50 U
val i
`
_ = dbInst.SyncSchema(schema)
appAdmin := dbInst.Auth("admin")
table := appAdmin.Table("bench_ops")
b.ResetTimer()
for i := 0; i < b.N; i++ {
table.Set(map[string]any{
"name": cast.String(i),
"val": i,
})
}
}
*/

26
types.go Normal file
View File

@ -0,0 +1,26 @@
package tableDB
type FieldSchema struct {
TableID string
Name string
Type string
Index string
Comment string
}
type QueryRequest struct {
Table string
Select []string
Joins []JoinConfig
Where string
OrderBy string
Limit int
Offset int
Args []any
}
type JoinConfig struct {
Table string
Type string
On string
}