feat(tableDB): extract and refactor tableDB from knowbase (by AI)
This commit is contained in:
commit
890c899cb7
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.geminiignore
|
||||||
|
.gemini
|
||||||
|
.ai/
|
||||||
|
env.json
|
||||||
|
env.yml
|
||||||
|
env.yaml
|
||||||
|
.log.meta.json
|
||||||
6
CHANGELOG.md
Normal file
6
CHANGELOG.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# CHANGELOG
|
||||||
|
|
||||||
|
## v1.0.0
|
||||||
|
- **初始化重构迁移**: 剥离自 `knowbase/internal/db` 作为纯净独立包。
|
||||||
|
- **动态 Schema 管理**: 保留动态解析与同步,并封装独立的缓存与 Hook 事件触发逻辑。
|
||||||
|
- **权限隔离体系**: 通过 `Auth(userID)` 方法引入多维度的数据隔离层,`SystemUserID` 下退化为超管权限操作全域数据。
|
||||||
68
README.md
Normal file
68
README.md
Normal 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
20
TEST.md
Normal 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
101
cache.go
Normal 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
312
db.go
Normal 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
35
go.mod
Normal 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
96
go.sum
Normal 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
397
table.go
Normal 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
315
tableDB_test.go
Normal 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
26
types.go
Normal 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
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user