package tableDB import ( "fmt" "strings" "time" "apigo.cc/go/cast" "apigo.cc/go/db" ) // 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 constraint map[string]any } // NewTable creates a new Table instance. Handles ":Field" suffix. func NewTable(name string, app *TableDB) *Table { actualName := name var constraint map[string]any 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, constraint: constraint, } } func (t *Table) checkAuth(id string, action string) error { if t.userID == SystemUserID { return nil } if action == "write" && (t.Name == "_Policy" || t.Name == "_Backup") { return fmt.Errorf("permission denied for %s", t.Name) } tableRec := GlobalCache.GetTable(t.Name) enableRLS := false if tableRec != nil { enableRLS = cast.Bool(tableRec["enableRLS"]) } if !enableRLS && !strings.HasPrefix(t.Name, "_") { return nil } policies := GlobalCache.GetFlatPolicies(t.userID, t.Name, action) hasFullAccess := false for _, p := range policies { if p.Condition == "" { hasFullAccess = true break } } if hasFullAccess { return nil } // 构建合并查询:一次性判断 Creator 和 所有 Conditions var authConditions []string var authArgs []any // 1. 追加 Creator 检查 hasCreator := true if hasCreator { authConditions = append(authConditions, "`creator` = ?") authArgs = append(authArgs, t.userID) } // 2. 追加 Policy Conditions for _, p := range policies { if p.Condition != "" { authConditions = append(authConditions, "("+p.Condition+")") authArgs = append(authArgs, p.ConditionArgs...) } } // 3. 组装并执行终极 1-RTT 查询 if len(authConditions) > 0 { authPart := "(" + strings.Join(authConditions, " OR ") + ")" checkSQL := fmt.Sprintf("SELECT 1 FROM `%s` WHERE id = ? AND %s LIMIT 1", t.Name, authPart) finalArgs := append([]any{id}, authArgs...) checkRes := t.db.Query(checkSQL, finalArgs...) if len(checkRes.MapOnR1()) > 0 { return nil // 验证通过! } } return fmt.Errorf("permission denied for %s record %s", t.Name, id) } 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 == SystemUserID { return whereStr, args, nil } tableRec := GlobalCache.GetTable(t.Name) enableRLS := false if tableRec != nil { enableRLS = cast.Bool(tableRec["enableRLS"]) } if !enableRLS && !strings.HasPrefix(t.Name, "_") { return whereStr, args, nil } policies := GlobalCache.GetFlatPolicies(t.userID, t.Name, "read") hasFullAccess := false for _, p := range policies { if p.Condition == "" { hasFullAccess = true break } } if hasFullAccess { return whereStr, args, nil } // Build dynamic SQL var authConditions []string var authArgs []any // Check creator field hasCreator := true if hasCreator { authConditions = append(authConditions, "`creator` = ?") authArgs = append(authArgs, t.userID) } for _, p := range policies { if p.Condition != "" { authConditions = append(authConditions, "("+p.Condition+")") authArgs = append(authArgs, p.ConditionArgs...) } } authPart := "0" if len(authConditions) > 0 { authPart = "(" + strings.Join(authConditions, " OR ") + ")" args = append(args, authArgs...) } if whereStr != "" { whereStr = "(" + whereStr + ") AND " + authPart } else { whereStr = authPart } 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 } tid := cast.String(tbl["id"]) tblFields := fieldMap[tid] if len(tblFields) == 0 { continue // Skip tables with no fields to avoid SQL errors } memo := cast.String(tbl["memo"]) sb.WriteString(name + " SD") if memo != "" { sb.WriteString(" //" + strings.ReplaceAll(memo, "\n", " ")) } sb.WriteString("\n") // Always ensure standard fields are present with appropriate indices sb.WriteString(" id c10 PK\n") sb.WriteString(" createTime bi I\n") sb.WriteString(" creator v64\n") sb.WriteString(" updateTime bi I\n") sb.WriteString(" updater v64\n") for _, f := range tblFields { fname := cast.String(f["name"]) if fname == "id" || fname == "createTime" || fname == "creator" || fname == "updateTime" || fname == "updater" { continue } ftype := cast.String(f["type"]) if ftype == "" { ftype = "v255" } isIndex := cast.Bool(f["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") } err := t.db.Sync(sb.String()) if err != nil { return err } return GlobalCache.Load(t.app) } // Set performs an upsert of one or more records. func (t *Table) Set(data ...any) error { if t.userID == "" { return fmt.Errorf("authentication required") } 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 { record := make(map[string]any) cast.Convert(&record, d) 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(record, tableSchema, fields); err != nil { return err } } idVal := record["id"] var isInsert bool if idVal == nil || cast.String(idVal) == "" { 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, "write"); err != nil { return err } isInsert = false } else { isInsert = true } } // Always update updateTime and updater record["updateTime"] = time.Now().UnixMilli() record["updater"] = t.userID var err error if isInsert { record["createTime"] = record["updateTime"] if t.userID != SystemUserID { if t.Name == "_Policy" || t.Name == "_Backup" { return fmt.Errorf("permission denied for %s", t.Name) } record["creator"] = t.userID } else { if record["creator"] == nil { record["creator"] = t.userID } } err = t.db.Insert(t.Name, record).Error } 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 } if err != nil { return err } if t.Name == "_Table" || t.Name == "_Field" || t.Name == "_Policy" { metaTouched = true if t.Name == "_Table" { if isInsert && t.app.hooks.OnCreatedTable != nil { var ts TableSchema cast.Convert(&ts, record) t.app.hooks.OnCreatedTable(&ts) } } else if t.Name == "_Field" { if t.app.hooks.OnUpdatedField != nil { var fs FieldSchema 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 { updatedRows = append(updatedRows, record) } } if metaTouched { if t.Name != "_Policy" { _ = t.reconstructAndSyncSchema() } _ = GlobalCache.Load(t.app) } if len(updatedRows) > 0 && t.app.hooks.OnUpdatedRows != nil { t.app.hooks.OnUpdatedRows(updatedRows, tableSchema, fields) } return nil } // SetField adds or updates one or more fields. If table doesn't exist, it will be created. func (t *Table) SetField(fields ...FieldSchema) error { if t.userID == "" { return fmt.Errorf("authentication required") } // 1. Ensure table exists in _Table tableRec := GlobalCache.GetTable(t.Name) if tableRec == nil { // Create table entry err := t.app.SetTable(TableSchema{Name: t.Name}) if err != nil { return err } tableRec = GlobalCache.GetTable(t.Name) if tableRec == nil { return fmt.Errorf("failed to create table entry for %s", t.Name) } // Automatically add default ID field to metadata _ = t.app.Table(t.Name + ":Field").Set(map[string]any{ "name": "id", "type": "c10", "isIndex": 1, "memo": "Primary Key", }) } // 2. Prepare field records fieldTable := t.app.Table(t.Name + ":Field") var fieldRecords []any for _, f := range fields { fRecord := map[string]any{ "name": f.Name, "type": f.Type, "isIndex": f.IsIndex, "memo": f.Memo, "settings": f.Settings, } if f.ID != "" { fRecord["id"] = f.ID } else { // If ID is missing, try to find existing field to update existing, _ := fieldTable.List(map[string]any{"name": f.Name}) if len(existing) > 0 { fRecord["id"] = existing[0]["id"] } } fieldRecords = append(fieldRecords, fRecord) } // 3. Batch Set fields return fieldTable.Set(fieldRecords...) } // RemoveField deletes one or more fields by name. func (t *Table) RemoveField(names ...string) error { if t.userID == "" { return fmt.Errorf("authentication required") } tableRec := GlobalCache.GetTable(t.Name) if tableRec == nil { return fmt.Errorf("table %s not found", t.Name) } fieldTable := t.app.Table(t.Name + ":Field") for _, name := range names { existing, _ := fieldTable.List(map[string]any{"name": name}) if len(existing) > 0 { err := fieldTable.Remove(cast.String(existing[0]["id"])) if err != nil { return err } } } return nil } // Get retrieves a single record. func (t *Table) Get(id string) (map[string]any, error) { if t.userID == "" { return nil, fmt.Errorf("authentication required") } if err := t.checkAuth(id, "read"); 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 one or more records by ID. func (t *Table) Remove(ids ...string) error { if t.userID == "" { 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 { if err := t.checkAuth(id, "write"); err != nil { return err } var record map[string]any if t.Name == "_Table" || t.Name == "_Field" || t.Name == "_Policy" { 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.Name == "_Field" || t.Name == "_Policy" { if t.Name != "_Policy" { _ = t.reconstructAndSyncSchema() } _ = GlobalCache.Load(t.app) if t.Name == "_Table" && record != nil && t.app.hooks.OnRemovedTable != nil { var ts TableSchema cast.Convert(&ts, record) t.app.hooks.OnRemovedTable(&ts) } else if t.Name == "_Field" && record != nil && t.app.hooks.OnRemovedField != nil { var fts *TableSchema 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 { removedIDs = append(removedIDs, id) } } else { return res.Error } } if len(removedIDs) > 0 && t.app.hooks.OnRemovedRows != nil { t.app.hooks.OnRemovedRows(removedIDs, tableSchema, fields) } return nil } // List retrieves multiple records. func (t *Table) List(where any, args ...any) ([]map[string]any, error) { req := QueryRequest{} if where != nil { switch v := where.(type) { case string: req.Where = v req.Args = args case map[string]any: req.Where, req.Args = buildWhere(v) } } return t.Query(req) } // Query performs a structured query on the current table. func (t *Table) Query(req QueryRequest) ([]map[string]any, error) { if t.userID == "" { return nil, fmt.Errorf("authentication required") } // Apply constraint to where clause 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 { return nil, err } res := t.db.Query(sql, 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) { if t.userID == "" { return 0, fmt.Errorf("authentication required") } 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) { if t.userID == "" { return nil, fmt.Errorf("authentication required") } 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) { if t.userID == "" { return nil, fmt.Errorf("authentication required") } 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 }