diff --git a/cmd/gf-source/README.md b/cmd/gf-source/README.md new file mode 100644 index 0000000..ddf7c63 --- /dev/null +++ b/cmd/gf-source/README.md @@ -0,0 +1,69 @@ +# GF-Source 自维护代码 + +这个目录包含了从 GoFrame (GF) 框架复制并修改的 DAO 生成工具源码,已将所有 `gdb` 相关引用替换为项目自定义的 `database` 包。 + +## 📁 目录结构 + +``` +gf-source/ +├── internal/ # 内部工具包 +│ ├── consts/ # 常量定义(从 GF internal/consts 复制) +│ └── utility/ # 工具函数 +│ ├── mlog/ # 日志工具 +│ └── utils/ # 通用工具 +├── templates/ # 代码生成模板 +│ ├── consts_gen_dao_template_dao.go +│ ├── consts_gen_dao_template_do.go +│ ├── consts_gen_dao_template_entity.go +│ └── consts_gen_dao_template_table.go +├── gendao*.go # DAO 生成核心逻辑 +├── go.mod # 子模块依赖 +└── go.sum +``` + +## 🔧 主要修改 + +### 1. 数据库接口替换 +- ✅ `github.com/gogf/gf/v2/database/gdb` → `git.magicany.cc/black1552/gin-base/database` +- ✅ `gdb.DB` → `database.DB` +- ✅ `gdb.ConfigNode` → `database.ConfigNode` +- ✅ `gdb.AddConfigNode()` → `database.AddConfigNode()` +- ✅ `gdb.Instance()` → `database.Instance()` +- ✅ `g.DB()` → `database.Database()` + +### 2. 内部包路径修改 +- ✅ `github.com/gogf/gf/cmd/gf/v2/internal/consts` → `git.magicany.cc/black1552/gin-base/cmd/gf-source/internal/consts` +- ✅ `github.com/gogf/gf/cmd/gf/v2/internal/utility/mlog` → `git.magicany.cc/black1552/gin-base/cmd/gf-source/internal/utility/mlog` +- ✅ `github.com/gogf/gf/cmd/gf/v2/internal/utility/utils` → `git.magicany.cc/black1552/gin-base/cmd/gf-source/internal/utility/utils` + +### 3. 包名统一 +- ✅ 所有 gendao 相关文件统一为 `package gendao` +- ✅ 模板文件移至 `templates/` 子目录(避免包名冲突) + +## 📦 编译 + +```bash +cd cmd/gf-source +go build . +``` + +## ⚠️ 注意事项 + +1. **这是参考代码**:这个目录主要用于参考 GF 的 DAO 生成逻辑,实际使用的是 `cmd/gin-dao-gen/main.go` +2. **不要直接导入**:这个子模块有独立的 go.mod,不应该被主项目直接导入 +3. **保持同步**:如果 GF 更新了 DAO 生成逻辑,需要手动同步这些文件并重新应用修改 + +## 🔄 更新流程 + +如果需要从 GF 更新代码: + +1. 从 `D:\web-object\gf\cmd\gf\internal\cmd\gendao` 复制最新文件 +2. 运行批量替换脚本将 `gdb` 改为 `database` +3. 运行批量替换脚本将 internal 路径改为本项目路径 +4. 测试编译确保没有错误 + +## 📝 相关文件 + +- 主程序:`cmd/gin-dao-gen/main.go` +- 数据库包:`database/` +- 配置文件:`config/config.toml` diff --git a/cmd/gf-source/go.mod b/cmd/gf-source/go.mod new file mode 100644 index 0000000..1b0bbd6 --- /dev/null +++ b/cmd/gf-source/go.mod @@ -0,0 +1,12 @@ +module git.magicany.cc/black1552/gin-base/cmd/gf-source + +go 1.25.0 + +require ( + git.magicany.cc/black1552/gin-base v0.0.0 + github.com/gogf/gf/v2 v2.10.0 + github.com/olekukonko/tablewriter v1.1.4 + golang.org/x/mod v0.33.0 +) + +replace git.magicany.cc/black1552/gin-base => ../.. diff --git a/cmd/gf-source/internal/consts/consts_gen_dao_template_dao.go b/cmd/gf-source/internal/consts/consts_gen_dao_template_dao.go index cffa30e..66e091d 100644 --- a/cmd/gf-source/internal/consts/consts_gen_dao_template_dao.go +++ b/cmd/gf-source/internal/consts/consts_gen_dao_template_dao.go @@ -15,6 +15,7 @@ package {{.TplPackageName}} import ( "{{.TplImportPrefix}}/internal" + "git.magicany.cc/black1552/gin-base/database" ) // {{.TplTableNameCamelLowerCase}}Dao is the data access object for the table {{.TplTableName}}. @@ -37,16 +38,16 @@ var ( {{if .TplTableSharding -}} // {{.TplTableNameCamelLowerCase}}ShardingHandler is the handler for sharding operations. // You can fill this sharding handler with your custom implementation. -func {{.TplTableNameCamelLowerCase}}ShardingHandler(m *gdb.Model) *gdb.Model { - m = m.Sharding(gdb.ShardingConfig{ - Table: gdb.ShardingTableConfig{ +func {{.TplTableNameCamelLowerCase}}ShardingHandler(m *database.Model) *database.Model { + m = m.Sharding(database.ShardingConfig{ + Table: database.ShardingTableConfig{ Enable: true, Prefix: "{{.TplTableShardingPrefix}}", // Replace Rule field with your custom sharding rule. // Or you can use "&gdb.DefaultShardingRule{}" for default sharding rule. Rule: nil, }, - Schema: gdb.ShardingSchemaConfig{}, + Schema: database.ShardingSchemaConfig{}, }) return m } @@ -65,9 +66,7 @@ package internal import ( "context" - - "github.com/gogf/gf/v2/database/gdb" - "github.com/gogf/gf/v2/frame/g" + "git.magicany.cc/black1552/gin-base/database" ) // {{.TplTableNameCamelCase}}Dao is the data access object for the table {{.TplTableName}}. @@ -75,7 +74,7 @@ type {{.TplTableNameCamelCase}}Dao struct { table string // table is the underlying table name of the DAO. group string // group is the database configuration group name of the current DAO. columns {{.TplTableNameCamelCase}}Columns // columns contains all the column names of Table for convenient usage. - handlers []gdb.ModelHandler // handlers for customized model modification. + handlers []database.ModelHandler // handlers for customized model modification. } // {{.TplTableNameCamelCase}}Columns defines and stores column names for the table {{.TplTableName}}. @@ -89,7 +88,7 @@ var {{.TplTableNameCamelLowerCase}}Columns = {{.TplTableNameCamelCase}}Columns{ } // New{{.TplTableNameCamelCase}}Dao creates and returns a new DAO object for table data access. -func New{{.TplTableNameCamelCase}}Dao(handlers ...gdb.ModelHandler) *{{.TplTableNameCamelCase}}Dao { +func New{{.TplTableNameCamelCase}}Dao(handlers ...database.ModelHandler) *{{.TplTableNameCamelCase}}Dao { return &{{.TplTableNameCamelCase}}Dao{ group: "{{.TplGroupName}}", table: "{{.TplTableName}}", @@ -99,8 +98,8 @@ func New{{.TplTableNameCamelCase}}Dao(handlers ...gdb.ModelHandler) *{{.TplTable } // DB retrieves and returns the underlying raw database management object of the current DAO. -func (dao *{{.TplTableNameCamelCase}}Dao) DB() gdb.DB { - return g.DB(dao.group) +func (dao *{{.TplTableNameCamelCase}}Dao) DB() database.DB { + return database.Database(dao.group) } // Table returns the table name of the current DAO. @@ -119,7 +118,7 @@ func (dao *{{.TplTableNameCamelCase}}Dao) Group() string { } // Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. -func (dao *{{.TplTableNameCamelCase}}Dao) Ctx(ctx context.Context) *gdb.Model { +func (dao *{{.TplTableNameCamelCase}}Dao) Ctx(ctx context.Context) *database.Model { model := dao.DB().Model(dao.table) for _, handler := range dao.handlers { model = handler(model) @@ -133,7 +132,7 @@ func (dao *{{.TplTableNameCamelCase}}Dao) Ctx(ctx context.Context) *gdb.Model { // // Note: Do not commit or roll back the transaction in function f, // as it is automatically handled by this function. -func (dao *{{.TplTableNameCamelCase}}Dao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { +func (dao *{{.TplTableNameCamelCase}}Dao) Transaction(ctx context.Context, f func(ctx context.Context, tx database.TX) error) (err error) { return dao.Ctx(ctx).Transaction(ctx, f) } ` diff --git a/cmd/gf-source/internal/consts/consts_gen_dao_template_entity.go b/cmd/gf-source/internal/consts/consts_gen_dao_template_entity.go index c54713a..69a53c8 100644 --- a/cmd/gf-source/internal/consts/consts_gen_dao_template_entity.go +++ b/cmd/gf-source/internal/consts/consts_gen_dao_template_entity.go @@ -8,7 +8,7 @@ package consts const TemplateGenDaoEntityContent = ` // ================================================================================= -// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. {{.TplCreatedAtDatetimeStr}} +// Code generated and maintained by Gen CLI tool. DO NOT EDIT. {{.TplCreatedAtDatetimeStr}} // ================================================================================= package {{.TplPackageName}} diff --git a/cmd/gf-source/internal/consts/consts_gen_dao_template_table.go b/cmd/gf-source/internal/consts/consts_gen_dao_template_table.go index a60c92d..8fe520b 100644 --- a/cmd/gf-source/internal/consts/consts_gen_dao_template_table.go +++ b/cmd/gf-source/internal/consts/consts_gen_dao_template_table.go @@ -15,20 +15,19 @@ package {{.TplPackageName}} import ( "context" - - "github.com/gogf/gf/v2/database/gdb" + "git.magicany.cc/black1552/gin-base/database" ) // {{.TplTableNameCamelCase}} defines the fields of table "{{.TplTableName}}" with their properties. // This map is used internally by GoFrame ORM to understand table structure. -var {{.TplTableNameCamelCase}} = map[string]*gdb.TableField{ +var {{.TplTableNameCamelCase}} = map[string]*database.TableField{ {{.TplTableFields}} } // Set{{.TplTableNameCamelCase}}TableFields registers the table fields definition to the database instance. // db: database instance that implements gdb.DB interface. // schema: optional schema/namespace name, especially for databases that support schemas. -func Set{{.TplTableNameCamelCase}}TableFields(ctx context.Context, db gdb.DB, schema ...string) error { +func Set{{.TplTableNameCamelCase}}TableFields(ctx context.Context, db database.DB, schema ...string) error { return db.GetCore().SetTableFields(ctx, "{{.TplTableName}}", {{.TplTableNameCamelCase}}, schema...) } diff --git a/cmd/gf-source/templates/consts_gen_dao_template_dao.go b/cmd/gf-source/templates/consts_gen_dao_template_dao.go index 3904f00..db54e33 100644 --- a/cmd/gf-source/templates/consts_gen_dao_template_dao.go +++ b/cmd/gf-source/templates/consts_gen_dao_template_dao.go @@ -67,7 +67,6 @@ import ( "context" "git.magicany.cc/black1552/gin-base/database" - "github.com/gogf/gf/v2/frame/g" ) // {{.TplTableNameCamelCase}}Dao is the data access object for the table {{.TplTableName}}. @@ -100,7 +99,7 @@ func New{{.TplTableNameCamelCase}}Dao(handlers ...database.ModelHandler) *{{.Tpl // DB retrieves and returns the underlying raw database management object of the current DAO. func (dao *{{.TplTableNameCamelCase}}Dao) DB() database.DB { - return g.DB(dao.group) + return database.Database(dao.group) } // Table returns the table name of the current DAO. diff --git a/cmd/gf-source/templates/consts_gen_dao_template_table.go b/cmd/gf-source/templates/consts_gen_dao_template_table.go index e930f30..1997651 100644 --- a/cmd/gf-source/templates/consts_gen_dao_template_table.go +++ b/cmd/gf-source/templates/consts_gen_dao_template_table.go @@ -31,5 +31,4 @@ var {{.TplTableNameCamelCase}} = map[string]*database.TableField{ func Set{{.TplTableNameCamelCase}}TableFields(ctx context.Context, db database.DB, schema ...string) error { return db.GetCore().SetTableFields(ctx, "{{.TplTableName}}", {{.TplTableNameCamelCase}}, schema...) } - ` diff --git a/cmd/gin-dao-gen/main.go b/cmd/gin-dao-gen/main.go new file mode 100644 index 0000000..aaadf5f --- /dev/null +++ b/cmd/gin-dao-gen/main.go @@ -0,0 +1,376 @@ +package main + +import ( + "context" + "fmt" + "os" + "strings" + + "git.magicany.cc/black1552/gin-base/config" + "git.magicany.cc/black1552/gin-base/database" + _ "git.magicany.cc/black1552/gin-base/database/drivers" +) + +func main() { + ctx := context.Background() + + // 加载配置 + cfg := config.GetAllConfig() + if cfg == nil { + fmt.Println("❌ 错误: 配置为空") + os.Exit(1) + } + + // 检查数据库配置 + dbConfigMap, ok := cfg["database"].(map[string]any) + if !ok || len(dbConfigMap) == 0 { + fmt.Println("❌ 错误: 未找到数据库配置") + os.Exit(1) + } + + // 获取默认数据库配置 + defaultDbConfig, ok := dbConfigMap["default"].(map[string]any) + if !ok { + fmt.Println("❌ 错误: 未找到 default 数据库配置") + os.Exit(1) + } + + // 提取配置值 + host := getStringValue(defaultDbConfig, "host", "127.0.0.1") + port := getStringValue(defaultDbConfig, "port", "3306") + name := getStringValue(defaultDbConfig, "name", "test") + dbType := getStringValue(defaultDbConfig, "type", "mysql") + + fmt.Println("=== Gin-Base DAO 代码生成工具 ===") + fmt.Printf("📊 数据库: %s\n", name) + fmt.Printf("🔧 类型: %s\n", dbType) + fmt.Printf("🌐 主机: %s:%s\n\n", host, port) + + // 初始化数据库连接 + err := initDatabaseFromMap(dbConfigMap) + if err != nil { + fmt.Printf("❌ 数据库初始化失败: %v\n", err) + os.Exit(1) + } + + // 获取数据库实例 + db := database.Database() + + // 获取所有表 + tables, err := db.Tables(ctx) + if err != nil { + fmt.Printf("❌ 获取表列表失败: %v\n", err) + os.Exit(1) + } + + fmt.Printf("📋 找到 %d 个表:\n", len(tables)) + for i, table := range tables { + fmt.Printf(" %d. %s\n", i+1, table) + } + fmt.Println() + + // 询问用户要生成的表 + var selectedTables []string + if len(os.Args) > 1 { + // 从命令行参数获取表名 + selectedTables = os.Args[1:] + } else { + // 默认生成所有表 + selectedTables = tables + fmt.Println("💡 提示: 可以通过命令行参数指定要生成的表") + fmt.Println(" 例如: gin-dao-gen users orders") + } + + // 创建输出目录 + dirs := []string{ + "./internal/dao", + "./internal/model/do", + "./internal/model/entity", + "./internal/model/table", + } + + for _, dir := range dirs { + if err := os.MkdirAll(dir, 0755); err != nil { + fmt.Printf("❌ 创建目录失败 %s: %v\n", dir, err) + os.Exit(1) + } + } + + // 为每个表生成代码 + for _, tableName := range selectedTables { + fmt.Printf("\n🔨 正在生成表 [%s] 的代码...\n", tableName) + + // 获取表字段信息 + fields, err := db.TableFields(ctx, tableName) + if err != nil { + fmt.Printf(" ⚠️ 获取表字段失败: %v\n", err) + continue + } + + // 生成 Entity + entityName := tableNameToStructName(tableName) + generateEntity(tableName, entityName, fields) + + // 生成 DO + generateDO(tableName, entityName, fields) + + // 生成 DAO + generateDAO(tableName, entityName) + + // 生成 Table + generateTable(tableName, entityName, fields) + + fmt.Printf(" ✅ 完成\n") + } + + fmt.Println("\n🎉 代码生成完成!") + fmt.Println("📁 生成的文件位于:") + fmt.Println(" - ./internal/dao/") + fmt.Println(" - ./internal/model/do/") + fmt.Println(" - ./internal/model/entity/") + fmt.Println(" - ./internal/model/table/") +} + +// 从 Map 初始化数据库 +func initDatabaseFromMap(dbConfigMap map[string]any) error { + for name, nodeConfig := range dbConfigMap { + nodeMap, ok := nodeConfig.(map[string]any) + if !ok { + continue + } + + configNode := database.ConfigNode{ + Host: getStringValue(nodeMap, "host", "127.0.0.1"), + Port: getStringValue(nodeMap, "port", "3306"), + User: getStringValue(nodeMap, "user", "root"), + Pass: getStringValue(nodeMap, "pass", ""), + Name: getStringValue(nodeMap, "name", ""), + Type: getStringValue(nodeMap, "type", "mysql"), + Role: database.Role(getStringValue(nodeMap, "role", "master")), + Debug: getBoolValue(nodeMap, "debug", false), + Prefix: getStringValue(nodeMap, "prefix", ""), + Charset: getStringValue(nodeMap, "charset", "utf8"), + } + + if err := database.AddConfigNode(name, configNode); err != nil { + return fmt.Errorf("add config node %s failed: %w", name, err) + } + } + + return nil +} + +// 辅助函数:从 map 中获取字符串值 +func getStringValue(m map[string]any, key string, defaultValue string) string { + if val, ok := m[key]; ok { + if str, ok := val.(string); ok { + return str + } + } + return defaultValue +} + +// 辅助函数:从 map 中获取布尔值 +func getBoolValue(m map[string]any, key string, defaultValue bool) bool { + if val, ok := m[key]; ok { + if b, ok := val.(bool); ok { + return b + } + } + return defaultValue +} + +// 表名转结构体名 +func tableNameToStructName(tableName string) string { + parts := strings.Split(tableName, "_") + var result strings.Builder + for _, part := range parts { + if len(part) > 0 { + result.WriteString(strings.ToUpper(part[:1])) + result.WriteString(part[1:]) + } + } + return result.String() +} + +// 生成 Entity 文件 +func generateEntity(tableName, entityName string, fields map[string]*database.TableField) { + filename := fmt.Sprintf("./internal/model/entity/%s.go", tableName) + + var content strings.Builder + content.WriteString("package entity\n\n") + content.WriteString("// Auto-generated by gin-base gen dao tool\n\n") + content.WriteString(fmt.Sprintf("// %s represents the entity for table %s\n", entityName, tableName)) + content.WriteString(fmt.Sprintf("type %s struct {\n", entityName)) + + for _, field := range fields { + fieldName := fieldNameToStructName(field.Name) + goType := dbTypeToGoType(field.Type) + jsonTag := field.Name + + content.WriteString(fmt.Sprintf("\t%s %s `json:\"%s\" description:\"%s\"`\n", + fieldName, goType, jsonTag, field.Comment)) + } + + content.WriteString("}\n") + + if err := os.WriteFile(filename, []byte(content.String()), 0644); err != nil { + fmt.Printf(" ❌ 写入文件失败: %v\n", err) + } else { + fmt.Printf(" 📄 生成 Entity: %s\n", filename) + } +} + +// 生成 DO 文件 +func generateDO(tableName, entityName string, fields map[string]*database.TableField) { + filename := fmt.Sprintf("./internal/model/do/%s.go", tableName) + + var content strings.Builder + content.WriteString("package do\n\n") + content.WriteString("import \"github.com/gogf/gf/v2/frame/g\"\n\n") + content.WriteString("// Auto-generated by gin-base gen dao tool\n\n") + content.WriteString(fmt.Sprintf("// %s represents the data object for table %s\n", entityName, tableName)) + content.WriteString(fmt.Sprintf("type %s struct {\n\tg.Meta `orm:\"table:%s, do:true\"`\n\n", entityName, tableName)) + + for _, field := range fields { + fieldName := fieldNameToStructName(field.Name) + goType := dbTypeToGoType(field.Type) + + content.WriteString(fmt.Sprintf("\t%s *%s `json:\"%s,omitempty\"`\n", + fieldName, goType, field.Name)) + } + + content.WriteString("}\n") + + if err := os.WriteFile(filename, []byte(content.String()), 0644); err != nil { + fmt.Printf(" ❌ 写入文件失败: %v\n", err) + } else { + fmt.Printf(" 📄 生成 DO: %s\n", filename) + } +} + +// 生成 DAO 文件 +func generateDAO(tableName, entityName string) { + filename := fmt.Sprintf("./internal/dao/%s.go", tableName) + lowerName := strings.ToLower(entityName[:1]) + entityName[1:] + + var content strings.Builder + content.WriteString("package dao\n\n") + content.WriteString("import (\n") + content.WriteString("\t\"git.magicany.cc/black1552/gin-base/database\"\n") + content.WriteString(fmt.Sprintf("\t\"git.magicany.cc/black1552/gin-base/internal/model/entity\"\n")) + content.WriteString(")\n\n") + content.WriteString("// Auto-generated by gin-base gen dao tool\n\n") + content.WriteString(fmt.Sprintf("// %s is the DAO for table %s\n", entityName, tableName)) + content.WriteString(fmt.Sprintf("var %s = New%s()\n\n", lowerName, entityName)) + content.WriteString(fmt.Sprintf("// %s creates and returns a new DAO instance\n", entityName)) + content.WriteString(fmt.Sprintf("func New%s() *%sDao {\n", entityName, entityName)) + content.WriteString(fmt.Sprintf("\treturn &%sDao{\n", entityName)) + content.WriteString("\t\ttable: \"" + tableName + "\",\n") + content.WriteString("\t}\n") + content.WriteString("}\n\n") + content.WriteString(fmt.Sprintf("// %sDao is the data access object for %s\n", entityName, tableName)) + content.WriteString(fmt.Sprintf("type %sDao struct {\n", entityName)) + content.WriteString("\ttable string\n") + content.WriteString("}\n\n") + content.WriteString("// Table returns the table name\n") + content.WriteString(fmt.Sprintf("func (d *%sDao) Table() string {\n", entityName)) + content.WriteString("\treturn d.table\n") + content.WriteString("}\n\n") + content.WriteString("// DB returns the database instance\n") + content.WriteString("func (d *DB) DB() database.DB {\n") + content.WriteString("\treturn database.Database()\n") + content.WriteString("}\n") + + if err := os.WriteFile(filename, []byte(content.String()), 0644); err != nil { + fmt.Printf(" ❌ 写入文件失败: %v\n", err) + } else { + fmt.Printf(" 📄 生成 DAO: %s\n", filename) + } +} + +// 生成 Table 文件 +func generateTable(tableName, entityName string, fields map[string]*database.TableField) { + filename := fmt.Sprintf("./internal/model/table/%s.go", tableName) + + var content strings.Builder + content.WriteString("package table\n\n") + content.WriteString("// Auto-generated by gin-base gen dao tool\n\n") + content.WriteString("const (\n") + content.WriteString(fmt.Sprintf("\t// %s is the table name\n", entityName)) + content.WriteString(fmt.Sprintf("\t%s = \"%s\"\n", entityName, tableName)) + content.WriteString(")\n\n") + content.WriteString("// Columns defines all columns of the table\n") + content.WriteString("var Columns = struct {\n") + + for _, field := range fields { + fieldName := fieldNameToConstName(field.Name) + content.WriteString(fmt.Sprintf("\t%s string\n", fieldName)) + } + content.WriteString("}{\n") + + for _, field := range fields { + fieldName := fieldNameToConstName(field.Name) + content.WriteString(fmt.Sprintf("\t%s: \"%s\",\n", fieldName, field.Name)) + } + content.WriteString("}\n") + + if err := os.WriteFile(filename, []byte(content.String()), 0644); err != nil { + fmt.Printf(" ❌ 写入文件失败: %v\n", err) + } else { + fmt.Printf(" 📄 生成 Table: %s\n", filename) + } +} + +// 字段名转结构体字段名 +func fieldNameToStructName(fieldName string) string { + parts := strings.Split(fieldName, "_") + var result strings.Builder + for _, part := range parts { + if len(part) > 0 { + result.WriteString(strings.ToUpper(part[:1])) + result.WriteString(part[1:]) + } + } + return result.String() +} + +// 字段名转常量名 +func fieldNameToConstName(fieldName string) string { + parts := strings.Split(fieldName, "_") + var result strings.Builder + for i, part := range parts { + if i > 0 { + result.WriteString("_") + } + result.WriteString(strings.ToUpper(part)) + } + return result.String() +} + +// 数据库类型转 Go 类型 +func dbTypeToGoType(dbType string) string { + dbType = strings.ToLower(dbType) + + switch { + case strings.Contains(dbType, "int"): + if strings.Contains(dbType, "bigint") { + return "int64" + } + return "int" + case strings.Contains(dbType, "float"), strings.Contains(dbType, "double"), strings.Contains(dbType, "decimal"): + return "float64" + case strings.Contains(dbType, "bool"): + return "bool" + case strings.Contains(dbType, "datetime"), strings.Contains(dbType, "timestamp"): + return "*gtime.Time" + case strings.Contains(dbType, "date"): + return "*gtime.Time" + case strings.Contains(dbType, "text"), strings.Contains(dbType, "char"), strings.Contains(dbType, "varchar"): + return "string" + case strings.Contains(dbType, "blob"), strings.Contains(dbType, "binary"): + return "[]byte" + default: + return "string" + } +} diff --git a/cmd/go.mod b/cmd/go.mod deleted file mode 100644 index 6a91ff4..0000000 --- a/cmd/go.mod +++ /dev/null @@ -1,81 +0,0 @@ -module git.magicany.cc/black1552/gin-base/cmd/gen - -go 1.25.0 - -require ( - git.magicany.cc/black1552/gin-base v1.0.2009 - github.com/gogf/gf/v2 v2.10.0 - github.com/olekukonko/tablewriter v1.1.4 - github.com/schollz/progressbar/v3 v3.19.0 - golang.org/x/mod v0.34.0 - golang.org/x/tools v0.43.0 -) - -require ( - filippo.io/edwards25519 v1.2.0 // indirect - github.com/BurntSushi/toml v1.6.0 // indirect - github.com/ClickHouse/clickhouse-go/v2 v2.0.15 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clbanning/mxj/v2 v2.7.0 // indirect - github.com/clipperhouse/displaywidth v0.10.0 // indirect - github.com/clipperhouse/uax29/v2 v2.6.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect - github.com/fatih/color v1.19.0 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-sql-driver/mysql v1.9.3 // indirect - github.com/go-viper/mapstructure/v2 v2.5.0 // indirect - github.com/goccy/go-json v0.10.6 // indirect - github.com/golang-jwt/jwt/v5 v5.3.1 // indirect - github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect - github.com/golang-sql/sqlexp v0.1.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/grokify/html-strip-tags-go v0.1.0 // indirect - github.com/lib/pq v1.12.0 // indirect - github.com/magiconair/properties v1.8.10 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.21 // indirect - github.com/microsoft/go-mssqldb v1.7.1 // indirect - github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect - github.com/ncruces/go-strftime v1.0.0 // indirect - github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect - github.com/olekukonko/errors v1.2.0 // indirect - github.com/olekukonko/ll v0.1.8 // indirect - github.com/paulmach/orb v0.7.1 // indirect - github.com/pelletier/go-toml/v2 v2.3.0 // indirect - github.com/pierrec/lz4/v4 v4.1.14 // indirect - github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/sagikazarmark/locafero v0.12.0 // indirect - github.com/shopspring/decimal v1.3.1 // indirect - github.com/sijms/go-ora/v2 v2.7.10 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.21.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/sdk v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/net v0.52.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.41.0 // indirect - golang.org/x/text v0.35.0 // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - modernc.org/libc v1.70.0 // indirect - modernc.org/mathutil v1.7.1 // indirect - modernc.org/memory v1.11.0 // indirect - modernc.org/sqlite v1.48.0 // indirect -) - -replace git.magicany.cc/black1552/gin-base => ./.. diff --git a/cmd/go.sum b/cmd/go.sum deleted file mode 100644 index 69f5ffa..0000000 --- a/cmd/go.sum +++ /dev/null @@ -1,277 +0,0 @@ -filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= -filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1 h1:sO0/P7g68FrryJzljemN+6GTssUXdANk6aJ7T1ZxnsQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1 h1:MyVTgWR8qd/Jw1Le0NZebGBUCLbtak3bJ3z1OlqZBpw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1 h1:DzHpqpoJVaCgOUdVHxE8QB52S6NiVdDQvGlny1qvPqA= -github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= -github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/ClickHouse/clickhouse-go v1.5.4/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= -github.com/ClickHouse/clickhouse-go/v2 v2.0.15 h1:lLAZliqrZEygkxosLaW1qHyeTb4Ho7fVCZ0WKCpLocU= -github.com/ClickHouse/clickhouse-go/v2 v2.0.15/go.mod h1:Z21o82zD8FFqefOQDg93c0XITlxGbTsWQuRm588Azkk= -github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= -github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= -github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= -github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= -github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= -github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= -github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= -github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= -github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= -github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= -github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= -github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= -github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= -github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= -github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= -github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= -github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= -github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= -github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs= -github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= -github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= -github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= -github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= -github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -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.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= -github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= -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/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.12.0 h1:mC1zeiNamwKBecjHarAr26c/+d8V5w/u4J0I/yASbJo= -github.com/lib/pq v1.12.0/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= -github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= -github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= -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/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= -github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/microsoft/go-mssqldb v1.7.1 h1:KU/g8aWeM3Hx7IMOFpiwYiUkU+9zeISb4+tx3ScVfsM= -github.com/microsoft/go-mssqldb v1.7.1/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= -github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= -github.com/mkevac/debugcharts v0.0.0-20191222103121-ae1c48aa8615/go.mod h1:Ad7oeElCZqA1Ufj0U9/liOF4BtVepxRcTvr2ey7zTvM= -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/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= -github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= -github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= -github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= -github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= -github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= -github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= -github.com/paulmach/orb v0.7.1 h1:Zha++Z5OX/l168sqHK3k4z18LDvr+YAO/VjK0ReQ9rU= -github.com/paulmach/orb v0.7.1/go.mod h1:FWRlTgl88VI1RBx/MkrwWDRhQ96ctqMCh8boXhmqB/A= -github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= -github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM= -github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= -github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= -github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -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/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= -github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= -github.com/schollz/progressbar/v3 v3.19.0 h1:Ea18xuIRQXLAUidVDox3AbwfUhD0/1IvohyTutOIFoc= -github.com/schollz/progressbar/v3 v3.19.0/go.mod h1:IsO3lpbaGuzh8zIMzgY3+J8l4C8GjO0Y9S69eFvNsec= -github.com/shirou/gopsutil v2.19.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= -github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/sijms/go-ora/v2 v2.7.10 h1:GSLdj0PYYgSndhsnm7b6p32OqgnwnUZSkFb3j+htfhI= -github.com/sijms/go-ora/v2 v2.7.10/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= -github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= -go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191220220014-0732a990476f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220429233432-b5fbb4746d32/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -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/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= -gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -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.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= -modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= -modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= -modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= -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.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw= -modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo= -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.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4= -modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig= -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= diff --git a/cmd/main.go b/cmd/main.go index aaadf5f..a4a9682 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -53,8 +53,14 @@ func main() { os.Exit(1) } - // 获取数据库实例 - db := database.Database() + // 获取数据库实例(使用 default 组) + db := database.Database("default") + + // 调试信息:打印当前使用的数据库名称 + config := db.GetConfig() + if config != nil { + fmt.Printf("🔍 调试: 当前数据库名 = %s, 类型 = %s\n", config.Name, config.Type) + } // 获取所有表 tables, err := db.Tables(ctx) @@ -63,6 +69,20 @@ func main() { os.Exit(1) } + // 如果没找到表,尝试直接查询验证 + if len(tables) == 0 { + fmt.Println("⚠️ 警告: 未找到任何表,尝试直接查询...") + result, err := db.Query(ctx, "SHOW TABLES") + if err != nil { + fmt.Printf("❌ 直接查询失败: %v\n", err) + } else { + fmt.Printf("🔍 直接查询结果: %d 行\n", len(result)) + for _, row := range result { + fmt.Printf(" - %v\n", row) + } + } + } + fmt.Printf("📋 找到 %d 个表:\n", len(tables)) for i, table := range tables { fmt.Printf(" %d. %s\n", i+1, table) @@ -104,7 +124,13 @@ func main() { fields, err := db.TableFields(ctx, tableName) if err != nil { fmt.Printf(" ⚠️ 获取表字段失败: %v\n", err) - continue + return // 使用 return 而不是 continue + } + + // 调试:打印字段信息 + fmt.Printf(" 🔍 调试: 找到 %d 个字段\n", len(fields)) + for name, field := range fields { + fmt.Printf(" - %s: %s (%s)\n", name, field.Type, field.Comment) } // 生成 Entity diff --git a/database/consts/consts.go b/database/consts/consts.go new file mode 100644 index 0000000..710f7ac --- /dev/null +++ b/database/consts/consts.go @@ -0,0 +1,60 @@ +package consts + +import ( + "context" + + "github.com/gogf/gf/v2/container/gvar" + "github.com/gogf/gf/v2/util/gmeta" +) + +type ( + Var = gvar.Var // Var is a universal variable interface, like generics. + Ctx = context.Context // Ctx is alias of frequently-used type context.Context. + Meta = gmeta.Meta // Meta is alias of frequently-used type gmeta.Meta. +) + +type ( + Map = map[string]any // Map is alias of frequently-used map type map[string]any. + MapAnyAny = map[any]any // MapAnyAny is alias of frequently-used map type map[any]any. + MapAnyStr = map[any]string // MapAnyStr is alias of frequently-used map type map[any]string. + MapAnyInt = map[any]int // MapAnyInt is alias of frequently-used map type map[any]int. + MapStrAny = map[string]any // MapStrAny is alias of frequently-used map type map[string]any. + MapStrStr = map[string]string // MapStrStr is alias of frequently-used map type map[string]string. + MapStrInt = map[string]int // MapStrInt is alias of frequently-used map type map[string]int. + MapIntAny = map[int]any // MapIntAny is alias of frequently-used map type map[int]any. + MapIntStr = map[int]string // MapIntStr is alias of frequently-used map type map[int]string. + MapIntInt = map[int]int // MapIntInt is alias of frequently-used map type map[int]int. + MapAnyBool = map[any]bool // MapAnyBool is alias of frequently-used map type map[any]bool. + MapStrBool = map[string]bool // MapStrBool is alias of frequently-used map type map[string]bool. + MapIntBool = map[int]bool // MapIntBool is alias of frequently-used map type map[int]bool. +) + +type ( + List = []Map // List is alias of frequently-used slice type []Map. + ListAnyAny = []MapAnyAny // ListAnyAny is alias of frequently-used slice type []MapAnyAny. + ListAnyStr = []MapAnyStr // ListAnyStr is alias of frequently-used slice type []MapAnyStr. + ListAnyInt = []MapAnyInt // ListAnyInt is alias of frequently-used slice type []MapAnyInt. + ListStrAny = []MapStrAny // ListStrAny is alias of frequently-used slice type []MapStrAny. + ListStrStr = []MapStrStr // ListStrStr is alias of frequently-used slice type []MapStrStr. + ListStrInt = []MapStrInt // ListStrInt is alias of frequently-used slice type []MapStrInt. + ListIntAny = []MapIntAny // ListIntAny is alias of frequently-used slice type []MapIntAny. + ListIntStr = []MapIntStr // ListIntStr is alias of frequently-used slice type []MapIntStr. + ListIntInt = []MapIntInt // ListIntInt is alias of frequently-used slice type []MapIntInt. + ListAnyBool = []MapAnyBool // ListAnyBool is alias of frequently-used slice type []MapAnyBool. + ListStrBool = []MapStrBool // ListStrBool is alias of frequently-used slice type []MapStrBool. + ListIntBool = []MapIntBool // ListIntBool is alias of frequently-used slice type []MapIntBool. +) + +type ( + Slice = []any // Slice is alias of frequently-used slice type []any. + SliceAny = []any // SliceAny is alias of frequently-used slice type []any. + SliceStr = []string // SliceStr is alias of frequently-used slice type []string. + SliceInt = []int // SliceInt is alias of frequently-used slice type []int. +) + +type ( + Array = []any // Array is alias of frequently-used slice type []any. + ArrayAny = []any // ArrayAny is alias of frequently-used slice type []any. + ArrayStr = []string // ArrayStr is alias of frequently-used slice type []string. + ArrayInt = []int // ArrayInt is alias of frequently-used slice type []int. +) diff --git a/database/drivers/clickhouse/clickhouse.go b/database/drivers/clickhouse/clickhouse.go index 0db0767..ca81113 100644 --- a/database/drivers/clickhouse/clickhouse.go +++ b/database/drivers/clickhouse/clickhouse.go @@ -4,58 +4,60 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -// Package clickhouse implements database.Driver for ClickHouse database. +// Package clickhouse implements database.Driver, which supports operations for database ClickHouse. package clickhouse import ( - "database/sql" - "fmt" - - _ "github.com/ClickHouse/clickhouse-go/v2" + "context" + "errors" "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/os/gctx" ) -// Driver is the driver for ClickHouse database. +// Driver is the driver for clickhouse database. type Driver struct { *database.Core } +var ( + errUnsupportedInsertIgnore = errors.New("unsupported method:InsertIgnore") + errUnsupportedInsertGetId = errors.New("unsupported method:InsertGetId") + errUnsupportedReplace = errors.New("unsupported method:Replace") + errUnsupportedBegin = errors.New("unsupported method:Begin") + errUnsupportedTransaction = errors.New("unsupported method:Transaction") +) + const ( - quoteChar = `"` + updateFilterPattern = `(?i)UPDATE[\s]+?(\w+[\.]?\w+)[\s]+?SET` + deleteFilterPattern = `(?i)DELETE[\s]+?FROM[\s]+?(\w+[\.]?\w+)` + filterTypePattern = `(?i)^UPDATE|DELETE` + needParsedSqlInCtx gctx.StrKey = "NeedParsedSql" + driverName = "clickhouse" ) func init() { - if err := database.Register("clickhouse", New()); err != nil { + if err := database.Register(`clickhouse`, New()); err != nil { panic(err) } } -// New creates and returns a driver that implements database.Driver for ClickHouse. +// New create and returns a driver that implements database.Driver, which supports operations for clickhouse. func New() database.Driver { return &Driver{} } -// New creates and returns a database object for ClickHouse. +// New creates and returns a database object for clickhouse. +// It implements the interface of database.Driver for extra database driver installation. func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.DB, error) { return &Driver{ Core: core, }, nil } -// GetChars returns the security char for ClickHouse. -func (d *Driver) GetChars() (charLeft string, charRight string) { - return quoteChar, quoteChar -} - -// Open creates and returns an underlying sql.DB object for ClickHouse. -func (d *Driver) Open(config *database.ConfigNode) (*sql.DB, error) { - var source string - if config.Link != "" { - source = config.Link - } else { - source = fmt.Sprintf("clickhouse://%s:%s@%s:%s/%s", - config.User, config.Pass, config.Host, config.Port, config.Name) +func (d *Driver) injectNeedParsedSql(ctx context.Context) context.Context { + if ctx.Value(needParsedSqlInCtx) != nil { + return ctx } - return sql.Open("clickhouse", source) + return context.WithValue(ctx, needParsedSqlInCtx, true) } diff --git a/database/drivers/clickhouse/clickhouse_convert.go b/database/drivers/clickhouse/clickhouse_convert.go new file mode 100644 index 0000000..ccde351 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_convert.go @@ -0,0 +1,83 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql/driver" + "time" + + "github.com/google/uuid" + "github.com/shopspring/decimal" + + "github.com/gogf/gf/v2/os/gtime" +) + +// ConvertValueForField converts value to the type of the record field. +func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fieldValue any) (any, error) { + switch itemValue := fieldValue.(type) { + case time.Time: + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue.IsZero() { + return nil, nil + } + return itemValue, nil + + case uuid.UUID: + return itemValue, nil + + case *time.Time: + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue == nil || itemValue.IsZero() { + return nil, nil + } + return itemValue, nil + + case gtime.Time: + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue.IsZero() { + return nil, nil + } + // for gtime type, needs to get time.Time + return itemValue.Time, nil + + case *gtime.Time: + // If the time is zero, it then updates it to nil, + // which will insert/update the value to database as "null". + if itemValue == nil || itemValue.IsZero() { + return nil, nil + } + // for gtime type, needs to get time.Time + return itemValue.Time, nil + + case decimal.Decimal: + return itemValue, nil + + case *decimal.Decimal: + if itemValue != nil { + return *itemValue, nil + } + return nil, nil + + default: + // if the other type implements valuer for the driver package + // the converted result is used + // otherwise the interface data is committed + valuer, ok := itemValue.(driver.Valuer) + if !ok { + return itemValue, nil + } + convertedValue, err := valuer.Value() + if err != nil { + return nil, err + } + return convertedValue, nil + } +} diff --git a/database/drivers/clickhouse/clickhouse_do_commit.go b/database/drivers/clickhouse/clickhouse_do_commit.go new file mode 100644 index 0000000..d366d79 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_do_commit.go @@ -0,0 +1,19 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +// DoCommit commits current sql and arguments to underlying sql driver. +func (d *Driver) DoCommit(ctx context.Context, in database.DoCommitInput) (out database.DoCommitOutput, err error) { + ctx = d.InjectIgnoreResult(ctx) + return d.Core.DoCommit(ctx, in) +} diff --git a/database/drivers/clickhouse/clickhouse_do_delete.go b/database/drivers/clickhouse/clickhouse_do_delete.go new file mode 100644 index 0000000..2c8dbea --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_do_delete.go @@ -0,0 +1,20 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql" + + "git.magicany.cc/black1552/gin-base/database" +) + +// DoDelete does "DELETE FROM ... " statement for the table. +func (d *Driver) DoDelete(ctx context.Context, link database.Link, table string, condition string, args ...any) (result sql.Result, err error) { + ctx = d.injectNeedParsedSql(ctx) + return d.Core.DoDelete(ctx, link, table, condition, args...) +} diff --git a/database/drivers/clickhouse/clickhouse_do_filter.go b/database/drivers/clickhouse/clickhouse_do_filter.go new file mode 100644 index 0000000..a70f844 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_do_filter.go @@ -0,0 +1,83 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "fmt" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/text/gregex" +) + +// DoFilter handles the sql before posts it to database. +func (d *Driver) DoFilter( + ctx context.Context, link database.Link, originSql string, args []any, +) (newSql string, newArgs []any, err error) { + if len(args) == 0 { + return originSql, args, nil + } + // Convert placeholder char '?' to string "$x". + var index int + originSql, _ = gregex.ReplaceStringFunc(`\?`, originSql, func(s string) string { + index++ + return fmt.Sprintf(`$%d`, index) + }) + + // Only SQL generated through the framework is processed. + if !d.getNeedParsedSqlFromCtx(ctx) { + return originSql, args, nil + } + + // replace STD SQL to Clickhouse SQL grammar + modeRes, err := gregex.MatchString(filterTypePattern, strings.TrimSpace(originSql)) + if err != nil { + return "", nil, err + } + if len(modeRes) == 0 { + return originSql, args, nil + } + + // Only delete/ UPDATE statements require filter + switch strings.ToUpper(modeRes[0]) { + case "UPDATE": + // MySQL eg: UPDATE table_name SET field1=new-value1, field2=new-value2 [WHERE Clause] + // Clickhouse eg: ALTER TABLE [db.]table UPDATE column1 = expr1 [, ...] WHERE filter_expr + newSql, err = gregex.ReplaceStringFuncMatch( + updateFilterPattern, originSql, + func(s []string) string { + return fmt.Sprintf("ALTER TABLE %s UPDATE", s[1]) + }, + ) + if err != nil { + return "", nil, err + } + return newSql, args, nil + + case "DELETE": + // MySQL eg: DELETE FROM table_name [WHERE Clause] + // Clickhouse eg: ALTER TABLE [db.]table [ON CLUSTER cluster] DELETE WHERE filter_expr + newSql, err = gregex.ReplaceStringFuncMatch( + deleteFilterPattern, originSql, + func(s []string) string { + return fmt.Sprintf("ALTER TABLE %s DELETE", s[1]) + }, + ) + if err != nil { + return "", nil, err + } + return newSql, args, nil + + default: + return originSql, args, nil + } +} + +func (d *Driver) getNeedParsedSqlFromCtx(ctx context.Context) bool { + return ctx.Value(needParsedSqlInCtx) != nil +} diff --git a/database/drivers/clickhouse/clickhouse_do_insert.go b/database/drivers/clickhouse/clickhouse_do_insert.go new file mode 100644 index 0000000..f2e6198 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_do_insert.go @@ -0,0 +1,76 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "git.magicany.cc/black1552/gin-base/database" +) + +// DoInsert inserts or updates data for given table. +// The list parameter must contain at least one record, which was previously validated. +func (d *Driver) DoInsert( + ctx context.Context, link database.Link, table string, list database.List, option database.DoInsertOption, +) (result sql.Result, err error) { + var keys, valueHolder []string + + // Handle the field names and placeholders. + for k := range list[0] { + keys = append(keys, k) + valueHolder = append(valueHolder, "?") + } + // Prepare the batch result pointer. + var ( + charL, charR = d.Core.GetChars() + keysStr = charL + strings.Join(keys, charR+","+charL) + charR + holderStr = strings.Join(valueHolder, ",") + tx database.TX + stmt *database.Stmt + ) + tx, err = d.Core.Begin(ctx) + if err != nil { + return + } + // It here uses defer to guarantee transaction be committed or roll-backed. + defer func() { + if err == nil { + _ = tx.Commit() + } else { + _ = tx.Rollback() + } + }() + stmt, err = tx.Prepare(fmt.Sprintf( + "INSERT INTO %s(%s) VALUES (%s)", + d.QuotePrefixTableName(table), keysStr, + holderStr, + )) + if err != nil { + return + } + + defer func() { + _ = stmt.Close() + }() + + for i := range len(list) { + // Values that will be committed to underlying database driver. + params := make([]any, 0) + for _, k := range keys { + params = append(params, list[i][k]) + } + // Prepare is allowed to execute only once in a transaction opened by clickhouse + result, err = stmt.ExecContext(ctx, params...) + if err != nil { + return + } + } + return +} diff --git a/database/drivers/clickhouse/clickhouse_do_update.go b/database/drivers/clickhouse/clickhouse_do_update.go new file mode 100644 index 0000000..f2e8625 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_do_update.go @@ -0,0 +1,20 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql" + + "git.magicany.cc/black1552/gin-base/database" +) + +// DoUpdate does "UPDATE ... " statement for the table. +func (d *Driver) DoUpdate(ctx context.Context, link database.Link, table string, data any, condition string, args ...any) (result sql.Result, err error) { + ctx = d.injectNeedParsedSql(ctx) + return d.Core.DoUpdate(ctx, link, table, data, condition, args...) +} diff --git a/database/drivers/clickhouse/clickhouse_insert.go b/database/drivers/clickhouse/clickhouse_insert.go new file mode 100644 index 0000000..49bb679 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_insert.go @@ -0,0 +1,27 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "database/sql" +) + +// InsertIgnore Other queries for modifying data parts are not supported: REPLACE, MERGE, UPSERT, INSERT UPDATE. +func (d *Driver) InsertIgnore(ctx context.Context, table string, data any, batch ...int) (sql.Result, error) { + return nil, errUnsupportedInsertIgnore +} + +// InsertAndGetId Other queries for modifying data parts are not supported: REPLACE, MERGE, UPSERT, INSERT UPDATE. +func (d *Driver) InsertAndGetId(ctx context.Context, table string, data any, batch ...int) (int64, error) { + return 0, errUnsupportedInsertGetId +} + +// Replace Other queries for modifying data parts are not supported: REPLACE, MERGE, UPSERT, INSERT UPDATE. +func (d *Driver) Replace(ctx context.Context, table string, data any, batch ...int) (sql.Result, error) { + return nil, errUnsupportedReplace +} diff --git a/database/drivers/clickhouse/clickhouse_open.go b/database/drivers/clickhouse/clickhouse_open.go new file mode 100644 index 0000000..99d3c29 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_open.go @@ -0,0 +1,46 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "database/sql" + "fmt" + "net/url" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +// Open creates and returns an underlying sql.DB object for clickhouse. +func (d *Driver) Open(config *database.ConfigNode) (db *sql.DB, err error) { + var source string + // clickhouse://username:password@host1:9000,host2:9000/database?dial_timeout=200ms&max_execution_time=60 + if config.Pass != "" { + source = fmt.Sprintf( + "clickhouse://%s:%s@%s:%s/%s?debug=%t", + config.User, url.PathEscape(config.Pass), + config.Host, config.Port, config.Name, config.Debug, + ) + } else { + source = fmt.Sprintf( + "clickhouse://%s@%s:%s/%s?debug=%t", + config.User, config.Host, config.Port, config.Name, config.Debug, + ) + } + if config.Extra != "" { + source = fmt.Sprintf("%s&%s", source, config.Extra) + } + if db, err = sql.Open(driverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, driverName, source, + ) + return nil, err + } + return +} diff --git a/database/drivers/clickhouse/clickhouse_ping.go b/database/drivers/clickhouse/clickhouse_ping.go new file mode 100644 index 0000000..4bb2fbd --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_ping.go @@ -0,0 +1,41 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "database/sql" + "fmt" + + "github.com/ClickHouse/clickhouse-go/v2" +) + +// PingMaster pings the master node to check authentication or keeps the connection alive. +func (d *Driver) PingMaster() error { + conn, err := d.Master() + if err != nil { + return err + } + return d.ping(conn) +} + +// PingSlave pings the slave node to check authentication or keeps the connection alive. +func (d *Driver) PingSlave() error { + conn, err := d.Slave() + if err != nil { + return err + } + return d.ping(conn) +} + +// ping Returns the Clickhouse specific error. +func (d *Driver) ping(conn *sql.DB) error { + err := conn.Ping() + if exception, ok := err.(*clickhouse.Exception); ok { + return fmt.Errorf("[%d]%s", exception.Code, exception.Message) + } + return err +} diff --git a/database/drivers/clickhouse/clickhouse_table_fields.go b/database/drivers/clickhouse/clickhouse_table_fields.go new file mode 100644 index 0000000..c5320b9 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_table_fields.go @@ -0,0 +1,70 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/util/gutil" +) + +const ( + tableFieldsColumns = `name,position,default_expression,comment,type,is_in_partition_key,is_in_sorting_key,is_in_primary_key,is_in_sampling_key` +) + +// TableFields retrieves and returns the fields' information of specified table of current schema. +// Also see DriverMysql.TableFields. +func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*database.TableField, err error) { + var ( + result database.Result + link database.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + var ( + getColumnsSql = fmt.Sprintf( + "select %s from `system`.columns c where `table` = '%s'", + tableFieldsColumns, table, + ) + ) + result, err = d.DoSelect(ctx, link, getColumnsSql) + if err != nil { + return nil, err + } + fields = make(map[string]*database.TableField) + for _, m := range result { + var ( + isNull = false + fieldType = m["type"].String() + ) + // in clickhouse , field type like is Nullable(int) + fieldsResult, _ := gregex.MatchString(`^Nullable\((.*?)\)`, fieldType) + if len(fieldsResult) == 2 { + isNull = true + fieldType = fieldsResult[1] + } + position := m["position"].Int() + if result[0]["position"].Int() != 0 { + position -= 1 + } + fields[m["name"].String()] = &database.TableField{ + Index: position, + Name: m["name"].String(), + Default: m["default_expression"].Val(), + Comment: m["comment"].String(), + // Key: m["Key"].String(), + Type: fieldType, + Null: isNull, + } + } + return fields, nil +} diff --git a/database/drivers/clickhouse/clickhouse_tables.go b/database/drivers/clickhouse/clickhouse_tables.go new file mode 100644 index 0000000..ccdb9a3 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_tables.go @@ -0,0 +1,36 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" +) + +const ( + tablesSqlTmp = "select name from `system`.tables where database = '%s'" +) + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result database.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, fmt.Sprintf(tablesSqlTmp, d.GetConfig().Name)) + if err != nil { + return + } + for _, m := range result { + tables = append(tables, m["name"].String()) + } + return +} diff --git a/database/drivers/clickhouse/clickhouse_transaction.go b/database/drivers/clickhouse/clickhouse_transaction.go new file mode 100644 index 0000000..3d11fd3 --- /dev/null +++ b/database/drivers/clickhouse/clickhouse_transaction.go @@ -0,0 +1,23 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package clickhouse + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +// Begin starts and returns the transaction object. +func (d *Driver) Begin(ctx context.Context) (tx database.TX, err error) { + return nil, errUnsupportedBegin +} + +// Transaction wraps the transaction logic using function `f`. +func (d *Driver) Transaction(ctx context.Context, f func(ctx context.Context, tx database.TX) error) error { + return errUnsupportedTransaction +} diff --git a/database/drivers/mssql/mssql.go b/database/drivers/mssql/mssql.go index b26ba4e..45f45fe 100644 --- a/database/drivers/mssql/mssql.go +++ b/database/drivers/mssql/mssql.go @@ -4,58 +4,45 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -// Package mssql implements database.Driver for Microsoft SQL Server. +// Package mssql implements database.Driver, which supports operations for MSSQL. package mssql import ( - "database/sql" - "fmt" - _ "github.com/microsoft/go-mssqldb" "git.magicany.cc/black1552/gin-base/database" ) -// Driver is the driver for SQL Server database. +// Driver is the driver for SQL server database. type Driver struct { *database.Core } const ( - quoteChar = `"` + rowNumberAliasForSelect = `ROW_NUMBER__` + quoteChar = `"` ) func init() { - if err := database.Register("mssql", New()); err != nil { + if err := database.Register(`mssql`, New()); err != nil { panic(err) } } -// New creates and returns a driver that implements database.Driver for SQL Server. +// New create and returns a driver that implements database.Driver, which supports operations for Mssql. func New() database.Driver { return &Driver{} } -// New creates and returns a database object for SQL Server. +// New creates and returns a database object for SQL server. +// It implements the interface of database.Driver for extra database driver installation. func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.DB, error) { return &Driver{ Core: core, }, nil } -// GetChars returns the security char for SQL Server. +// GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// Open creates and returns an underlying sql.DB object for SQL Server. -func (d *Driver) Open(config *database.ConfigNode) (*sql.DB, error) { - var source string - if config.Link != "" { - source = config.Link - } else { - source = fmt.Sprintf("sqlserver://%s:%s@%s:%s?database=%s", - config.User, config.Pass, config.Host, config.Port, config.Name) - } - return sql.Open("sqlserver", source) -} diff --git a/database/drivers/mssql/mssql_do_commit.go b/database/drivers/mssql/mssql_do_commit.go new file mode 100644 index 0000000..f5f3a41 --- /dev/null +++ b/database/drivers/mssql/mssql_do_commit.go @@ -0,0 +1,29 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +// DoCommit commits current sql and arguments to underlying sql driver. +func (d *Driver) DoCommit(ctx context.Context, in database.DoCommitInput) (out database.DoCommitOutput, err error) { + out, err = d.Core.DoCommit(ctx, in) + if err != nil { + return + } + if len(out.Records) > 0 { + // remove auto added field. + for i, record := range out.Records { + delete(record, rowNumberAliasForSelect) + out.Records[i] = record + } + } + return +} diff --git a/database/drivers/mssql/mssql_do_exec.go b/database/drivers/mssql/mssql_do_exec.go new file mode 100644 index 0000000..74e7b12 --- /dev/null +++ b/database/drivers/mssql/mssql_do_exec.go @@ -0,0 +1,192 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + "database/sql" + "fmt" + "regexp" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +const ( + // INSERT statement prefixes + insertPrefixDefault = "INSERT INTO" + insertPrefixIgnore = "INSERT IGNORE INTO" + + // Database field attributes + fieldExtraIdentity = "IDENTITY" + fieldKeyPrimary = "PRI" + + // SQL keywords and syntax markers + outputKeyword = "OUTPUT" + insertValuesMarker = ") VALUES" // find the position of the string "VALUES" in the INSERT SQL statement to embed output code for retrieving the last inserted ID + + // Object and field references + insertedObjectName = "INSERTED" + + // Result field names and aliases + affectCountExpression = " 1 as AffectCount" + lastInsertIdFieldAlias = "ID" +) + +// DoExec commits the sql string and its arguments to underlying driver +// through given link object and returns the execution result. +func (d *Driver) DoExec(ctx context.Context, link database.Link, sqlStr string, args ...interface{}) (result sql.Result, err error) { + // Transaction checks. + if link == nil { + if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil { + // Firstly, check and retrieve transaction link from context. + link = &txLinkMssql{tx.GetSqlTX()} + } else if link, err = d.MasterLink(); err != nil { + // Or else it creates one from master node. + return nil, err + } + } else if !link.IsTransaction() { + // If current link is not transaction link, it checks and retrieves transaction from context. + if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil { + link = &txLinkMssql{tx.GetSqlTX()} + } + } + + // SQL filtering. + sqlStr, args = d.FormatSqlBeforeExecuting(sqlStr, args) + sqlStr, args, err = d.DoFilter(ctx, link, sqlStr, args) + if err != nil { + return nil, err + } + + if !strings.HasPrefix(sqlStr, insertPrefixDefault) && !strings.HasPrefix(sqlStr, insertPrefixIgnore) { + return d.Core.DoExec(ctx, link, sqlStr, args) + } + // Find the first position of VALUES marker in the INSERT statement. + pos := strings.Index(sqlStr, insertValuesMarker) + + table := d.GetTableNameFromSql(sqlStr) + outPutSql := d.GetInsertOutputSql(ctx, table) + // rebuild sql add output + var ( + sqlValueBefore = sqlStr[:pos+1] + sqlValueAfter = sqlStr[pos+1:] + ) + + sqlStr = fmt.Sprintf("%s%s%s", sqlValueBefore, outPutSql, sqlValueAfter) + + // fmt.Println("sql str:", sqlStr) + // Link execution. + var out database.DoCommitOutput + out, err = d.DoCommit(ctx, database.DoCommitInput{ + Link: link, + Sql: sqlStr, + Args: args, + Stmt: nil, + Type: database.SqlTypeQueryContext, + IsTransaction: link.IsTransaction(), + }) + if err != nil { + return &Result{lastInsertId: 0, rowsAffected: 0, err: err}, err + } + stdSqlResult := out.Records + if len(stdSqlResult) == 0 { + err = gerror.WrapCode( + gcode.CodeDbOperationError, + gerror.New("affected count is zero"), + `sql.Result.RowsAffected failed`, + ) + return &Result{lastInsertId: 0, rowsAffected: 0, err: err}, err + } + // For batch insert, OUTPUT clause returns one row per inserted row. + // So the rowsAffected should be the count of returned records. + rowsAffected := int64(len(stdSqlResult)) + // get last_insert_id from the first returned row + lastInsertId := stdSqlResult[0].GMap().GetVar(lastInsertIdFieldAlias).Int64() + + return &Result{lastInsertId: lastInsertId, rowsAffected: rowsAffected}, err +} + +// GetTableNameFromSql get table name from sql statement +// It handles table string like: +// "user" +// "user u" +// "DbLog.dbo.user", +// "user as u". +func (d *Driver) GetTableNameFromSql(sqlStr string) (table string) { + // INSERT INTO "ip_to_id"("ip") OUTPUT 1 as AffectCount,INSERTED.id as ID VALUES(?) + var ( + leftChars, rightChars = d.GetChars() + trimStr = leftChars + rightChars + "[] " + pattern = "INTO(.+?)\\(" + regCompile = regexp.MustCompile(pattern) + tableInfo = regCompile.FindStringSubmatch(sqlStr) + ) + // get the first one. after the first it may be content of the value, it's not table name. + table = tableInfo[1] + table = strings.Trim(table, " ") + if strings.Contains(table, ".") { + tmpAry := strings.Split(table, ".") + // the last one is table name + table = tmpAry[len(tmpAry)-1] + } else if strings.Contains(table, "as") || strings.Contains(table, " ") { + tmpAry := strings.Split(table, "as") + if len(tmpAry) < 2 { + tmpAry = strings.Split(table, " ") + } + // get the first one + table = tmpAry[0] + } + table = strings.Trim(table, trimStr) + return table +} + +// txLink is used to implement interface Link for TX. +type txLinkMssql struct { + *sql.Tx +} + +// IsTransaction returns if current Link is a transaction. +func (l *txLinkMssql) IsTransaction() bool { + return true +} + +// IsOnMaster checks and returns whether current link is operated on master node. +// Note that, transaction operation is always operated on master node. +func (l *txLinkMssql) IsOnMaster() bool { + return true +} + +// GetInsertOutputSql gen get last_insert_id code +func (d *Driver) GetInsertOutputSql(ctx context.Context, table string) string { + fds, errFd := d.GetDB().TableFields(ctx, table) + if errFd != nil { + return "" + } + extraSqlAry := make([]string, 0) + extraSqlAry = append(extraSqlAry, fmt.Sprintf(" %s %s", outputKeyword, affectCountExpression)) + incrNo := 0 + if len(fds) > 0 { + for _, fd := range fds { + // has primary key and is auto-increment + if fd.Extra == fieldExtraIdentity && fd.Key == fieldKeyPrimary && !fd.Null { + incrNoStr := "" + if incrNo == 0 { // fixed first field named id, convenient to get + incrNoStr = fmt.Sprintf(" as %s", lastInsertIdFieldAlias) + } + + extraSqlAry = append(extraSqlAry, fmt.Sprintf("%s.%s%s", insertedObjectName, fd.Name, incrNoStr)) + incrNo++ + } + // fmt.Printf("null:%t name:%s key:%s k:%s \n", fd.Null, fd.Name, fd.Key, k) + } + } + return strings.Join(extraSqlAry, ",") + // sql example:INSERT INTO "ip_to_id"("ip") OUTPUT 1 as AffectCount,INSERTED.id as ID VALUES(?) +} diff --git a/database/drivers/mssql/mssql_do_filter.go b/database/drivers/mssql/mssql_do_filter.go new file mode 100644 index 0000000..a5951b5 --- /dev/null +++ b/database/drivers/mssql/mssql_do_filter.go @@ -0,0 +1,155 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + "fmt" + "strconv" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +var ( + selectWithOrderSqlTmp = ` +SELECT * FROM ( + SELECT ROW_NUMBER() OVER (ORDER BY %s) as ROW_NUMBER__, %s + FROM (%s) as InnerQuery +) as TMP_ +WHERE TMP_.ROW_NUMBER__ > %d AND TMP_.ROW_NUMBER__ <= %d` + selectWithoutOrderSqlTmp = ` +SELECT * FROM ( + SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as ROW_NUMBER__, %s + FROM (%s) as InnerQuery +) as TMP_ +WHERE TMP_.ROW_NUMBER__ > %d AND TMP_.ROW_NUMBER__ <= %d` +) + +func init() { + var err error + selectWithOrderSqlTmp, err = database.FormatMultiLineSqlToSingle(selectWithOrderSqlTmp) + if err != nil { + panic(err) + } + selectWithoutOrderSqlTmp, err = database.FormatMultiLineSqlToSingle(selectWithoutOrderSqlTmp) + if err != nil { + panic(err) + } +} + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *Driver) DoFilter( + ctx context.Context, link database.Link, sql string, args []any, +) (newSql string, newArgs []any, err error) { + var index int + // Convert placeholder char '?' to string "@px". + newSql, err = gregex.ReplaceStringFunc("\\?", sql, func(s string) string { + index++ + return fmt.Sprintf("@p%d", index) + }) + if err != nil { + return "", nil, err + } + newSql, err = gregex.ReplaceString("\"", "", newSql) + if err != nil { + return "", nil, err + } + newSql, err = d.parseSql(newSql) + if err != nil { + return "", nil, err + } + newArgs = args + return d.Core.DoFilter(ctx, link, newSql, newArgs) +} + +// parseSql does some replacement of the sql before commits it to underlying driver, +// for support of microsoft sql server. +func (d *Driver) parseSql(toBeCommittedSql string) (string, error) { + var ( + err error + operation = gstr.StrTillEx(toBeCommittedSql, " ") + keyword = strings.ToUpper(gstr.Trim(operation)) + ) + switch keyword { + case "SELECT": + toBeCommittedSql, err = d.handleSelectSqlReplacement(toBeCommittedSql) + if err != nil { + return "", err + } + } + return toBeCommittedSql, nil +} + +func (d *Driver) handleSelectSqlReplacement(toBeCommittedSql string) (newSql string, err error) { + // SELECT * FROM USER WHERE ID=1 LIMIT 1 + match, err := gregex.MatchString(`^SELECT(.+?)LIMIT\s+1$`, toBeCommittedSql) + if err != nil { + return "", err + } + if len(match) > 1 { + return fmt.Sprintf(`SELECT TOP 1 %s`, strings.TrimSpace(match[1])), nil + } + + // SELECT * FROM USER WHERE AGE>18 ORDER BY ID DESC LIMIT 100, 200 + pattern := `(?i)SELECT(.+?)(ORDER BY.+?)?\s*LIMIT\s*(\d+)(?:\s*,\s*(\d+))?` + if !gregex.IsMatchString(pattern, toBeCommittedSql) { + return toBeCommittedSql, nil + } + + allMatch, err := gregex.MatchString(pattern, toBeCommittedSql) + if err != nil { + return "", err + } + + // Extract SELECT part + selectStr := strings.TrimSpace(allMatch[1]) + + // Extract ORDER BY part + orderStr := "" + if len(allMatch[2]) > 0 { + orderStr = strings.TrimSpace(allMatch[2]) + // Remove "ORDER BY" prefix as it will be used in OVER clause + orderStr = strings.TrimPrefix(orderStr, "ORDER BY") + orderStr = strings.TrimSpace(orderStr) + } + + // Calculate LIMIT and OFFSET values + first, _ := strconv.Atoi(allMatch[3]) // LIMIT first parameter + limit := 0 + if len(allMatch) > 4 && allMatch[4] != "" { + limit, _ = strconv.Atoi(allMatch[4]) // LIMIT second parameter + } else { + limit = first + first = 0 + } + + // Build the final query + if orderStr != "" { + // Have ORDER BY clause + newSql = fmt.Sprintf( + selectWithOrderSqlTmp, + orderStr, // ORDER BY clause for ROW_NUMBER + "*", // Select all columns + fmt.Sprintf("SELECT %s", selectStr), // Original SELECT + first, // OFFSET + first+limit, // OFFSET + LIMIT + ) + } else { + // Without ORDER BY clause + newSql = fmt.Sprintf( + selectWithoutOrderSqlTmp, + "*", // Select all columns + fmt.Sprintf("SELECT %s", selectStr), // Original SELECT + first, // OFFSET + first+limit, // OFFSET + LIMIT + ) + } + return newSql, nil +} diff --git a/database/drivers/mssql/mssql_do_filter_test.go b/database/drivers/mssql/mssql_do_filter_test.go new file mode 100644 index 0000000..2f8a17a --- /dev/null +++ b/database/drivers/mssql/mssql_do_filter_test.go @@ -0,0 +1,130 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + "testing" + + "github.com/gogf/gf/v2/test/gtest" +) + +func TestDriver_DoFilter(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + d := &Driver{} + + // Test SELECT with LIMIT + sql := "SELECT * FROM users WHERE id = ? LIMIT 10" + args := []any{1} + newSql, newArgs, err := d.DoFilter(context.Background(), nil, sql, args) + t.AssertNil(err) + t.Assert(newArgs, args) + // DoFilter should transform the SQL for MSSQL compatibility + t.AssertNE(newSql, "") + + // Test INSERT statement (should remain unchanged except for placeholder) + sql = "INSERT INTO users (name) VALUES (?)" + args = []any{"test"} + newSql, newArgs, err = d.DoFilter(context.Background(), nil, sql, args) + t.AssertNil(err) + t.Assert(newArgs, args) + t.AssertNE(newSql, "") + + // Test UPDATE statement + sql = "UPDATE users SET name = ? WHERE id = ?" + args = []any{"test", 1} + newSql, newArgs, err = d.DoFilter(context.Background(), nil, sql, args) + t.AssertNil(err) + t.Assert(newArgs, args) + t.AssertNE(newSql, "") + + // Test DELETE statement + sql = "DELETE FROM users WHERE id = ?" + args = []any{1} + newSql, newArgs, err = d.DoFilter(context.Background(), nil, sql, args) + t.AssertNil(err) + t.Assert(newArgs, args) + t.AssertNE(newSql, "") + }) +} + +func TestDriver_handleSelectSqlReplacement(t *testing.T) { + gtest.C(t, func(t *gtest.T) { + d := &Driver{} + + // LIMIT 1 + inputSql := "SELECT * FROM User WHERE ID = 1 LIMIT 1" + expectedSql := "SELECT TOP 1 * FROM User WHERE ID = 1" + resultSql, err := d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + // LIMIT query with offset and number of rows + inputSql = "SELECT * FROM User ORDER BY ID DESC LIMIT 100, 200" + expectedSql = "SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY ID DESC) as ROW_NUMBER__, * FROM (SELECT * FROM User) as InnerQuery ) as TMP_ WHERE TMP_.ROW_NUMBER__ > 100 AND TMP_.ROW_NUMBER__ <= 300" + resultSql, err = d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + // Simple query with no LIMIT + inputSql = "SELECT * FROM User WHERE age > 18" + expectedSql = "SELECT * FROM User WHERE age > 18" + resultSql, err = d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + // without LIMIT + inputSql = "SELECT * FROM User ORDER BY ID DESC" + expectedSql = "SELECT * FROM User ORDER BY ID DESC" + resultSql, err = d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + // LIMIT query with only rows + inputSql = "SELECT * FROM User LIMIT 50" + expectedSql = "SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as ROW_NUMBER__, * FROM (SELECT * FROM User) as InnerQuery ) as TMP_ WHERE TMP_.ROW_NUMBER__ > 0 AND TMP_.ROW_NUMBER__ <= 50" + resultSql, err = d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + // LIMIT query without ORDER BY + inputSql = "SELECT * FROM User LIMIT 30" + expectedSql = "SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as ROW_NUMBER__, * FROM (SELECT * FROM User) as InnerQuery ) as TMP_ WHERE TMP_.ROW_NUMBER__ > 0 AND TMP_.ROW_NUMBER__ <= 30" + resultSql, err = d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + // Complex query with ORDER BY and LIMIT + inputSql = "SELECT name, age FROM User WHERE age > 18 ORDER BY age ASC LIMIT 10, 5" + expectedSql = "SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY age ASC) as ROW_NUMBER__, * FROM (SELECT name, age FROM User WHERE age > 18) as InnerQuery ) as TMP_ WHERE TMP_.ROW_NUMBER__ > 10 AND TMP_.ROW_NUMBER__ <= 15" + resultSql, err = d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + // Complex conditional queries have limits + inputSql = "SELECT * FROM User WHERE age > 18 AND status = 'active' LIMIT 100, 50" + expectedSql = "SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as ROW_NUMBER__, * FROM (SELECT * FROM User WHERE age > 18 AND status = 'active') as InnerQuery ) as TMP_ WHERE TMP_.ROW_NUMBER__ > 100 AND TMP_.ROW_NUMBER__ <= 150" + resultSql, err = d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + // A LIMIT query that contains subquery + inputSql = "SELECT * FROM (SELECT * FROM User WHERE age > 18) AS subquery LIMIT 10" + expectedSql = "SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as ROW_NUMBER__, * FROM (SELECT * FROM (SELECT * FROM User WHERE age > 18) AS subquery) as InnerQuery ) as TMP_ WHERE TMP_.ROW_NUMBER__ > 0 AND TMP_.ROW_NUMBER__ <= 10" + resultSql, err = d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + // Queries with complex ORDER BY and LIMIT + inputSql = "SELECT name, age FROM User WHERE age > 18 ORDER BY age DESC, name ASC LIMIT 20, 10" + expectedSql = "SELECT * FROM ( SELECT ROW_NUMBER() OVER (ORDER BY age DESC, name ASC) as ROW_NUMBER__, * FROM (SELECT name, age FROM User WHERE age > 18) as InnerQuery ) as TMP_ WHERE TMP_.ROW_NUMBER__ > 20 AND TMP_.ROW_NUMBER__ <= 30" + resultSql, err = d.handleSelectSqlReplacement(inputSql) + t.AssertNil(err) + t.Assert(resultSql, expectedSql) + + }) +} diff --git a/database/drivers/mssql/mssql_do_insert.go b/database/drivers/mssql/mssql_do_insert.go new file mode 100644 index 0000000..81a3042 --- /dev/null +++ b/database/drivers/mssql/mssql_do_insert.go @@ -0,0 +1,203 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/container/gset" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gstr" +) + +// DoInsert inserts or updates data for given table. +// The list parameter must contain at least one record, which was previously validated. +func (d *Driver) DoInsert( + ctx context.Context, link database.Link, table string, list database.List, option database.DoInsertOption, +) (result sql.Result, err error) { + switch option.InsertOption { + case database.InsertOptionSave: + return d.doSave(ctx, link, table, list, option) + + case database.InsertOptionReplace: + // MSSQL does not support REPLACE INTO syntax, use SAVE instead. + return d.doSave(ctx, link, table, list, option) + + case database.InsertOptionIgnore: + // MSSQL does not support INSERT IGNORE syntax, use MERGE instead. + return d.doInsertIgnore(ctx, link, table, list, option) + + default: + return d.Core.DoInsert(ctx, link, table, list, option) + } +} + +// doSave support upsert for MSSQL +func (d *Driver) doSave(ctx context.Context, + link database.Link, table string, list database.List, option database.DoInsertOption, +) (result sql.Result, err error) { + return d.doMergeInsert(ctx, link, table, list, option, true) +} + +// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for MSSQL database. +// It only inserts records when there's no conflict on primary/unique keys. +func (d *Driver) doInsertIgnore(ctx context.Context, + link database.Link, table string, list database.List, option database.DoInsertOption, +) (result sql.Result, err error) { + return d.doMergeInsert(ctx, link, table, list, option, false) +} + +// doMergeInsert implements MERGE-based insert operations for MSSQL database. +// When withUpdate is true, it performs upsert (insert or update). +// When withUpdate is false, it performs insert ignore (insert only when no conflict). +func (d *Driver) doMergeInsert( + ctx context.Context, + link database.Link, table string, list database.List, option database.DoInsertOption, withUpdate bool, +) (result sql.Result, err error) { + // If OnConflict is not specified, automatically get the primary key of the table + conflictKeys := option.OnConflict + if len(conflictKeys) == 0 { + primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table) + if err != nil { + return nil, gerror.WrapCode( + gcode.CodeInternalError, + err, + `failed to get primary keys for table`, + ) + } + foundPrimaryKey := false + for _, primaryKey := range primaryKeys { + for dataKey := range list[0] { + if strings.EqualFold(dataKey, primaryKey) { + foundPrimaryKey = true + break + } + } + if foundPrimaryKey { + break + } + } + if !foundPrimaryKey { + return nil, gerror.NewCodef( + gcode.CodeMissingParameter, + `Replace/Save/InsertIgnore operation requires conflict detection: `+ + `either specify OnConflict() columns or ensure table '%s' has a primary key in the data`, + table, + ) + } + // TODO consider composite primary keys. + conflictKeys = primaryKeys + } + + var ( + one = list[0] + oneLen = len(one) + charL, charR = d.GetChars() + conflictKeySet = gset.NewStrSet(false) + + // queryHolders: Handle data with Holder that need to be merged + // queryValues: Handle data that need to be merged + // insertKeys: Handle valid keys that need to be inserted + // insertValues: Handle values that need to be inserted + // updateValues: Handle values that need to be updated (only when withUpdate=true) + queryHolders = make([]string, oneLen) + queryValues = make([]any, oneLen) + insertKeys = make([]string, oneLen) + insertValues = make([]string, oneLen) + updateValues []string + ) + + // conflictKeys slice type conv to set type + for _, conflictKey := range conflictKeys { + conflictKeySet.Add(gstr.ToUpper(conflictKey)) + } + + index := 0 + for key, value := range one { + queryHolders[index] = "?" + queryValues[index] = value + insertKeys[index] = charL + key + charR + insertValues[index] = "T2." + charL + key + charR + + // Build updateValues only when withUpdate is true + // Filter conflict keys and soft created fields from updateValues + if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) { + updateValues = append( + updateValues, + fmt.Sprintf(`T1.%s = T2.%s`, charL+key+charR, charL+key+charR), + ) + } + index++ + } + + var ( + batchResult = new(database.SqlResult) + sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys) + ) + r, err := d.DoExec(ctx, link, sqlStr, queryValues...) + if err != nil { + return r, err + } + if n, err := r.RowsAffected(); err != nil { + return r, err + } else { + batchResult.Result = r + batchResult.Affected += n + } + return batchResult, nil +} + +// parseSqlForMerge generates MERGE statement for MSSQL database. +// When updateValues is empty, it only inserts (INSERT IGNORE behavior). +// When updateValues is provided, it performs upsert (INSERT or UPDATE). +// Examples: +// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) +// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ... +func parseSqlForMerge(table string, + queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string, +) (sqlStr string) { + var ( + queryHolderStr = strings.Join(queryHolders, ",") + insertKeyStr = strings.Join(insertKeys, ",") + insertValueStr = strings.Join(insertValues, ",") + duplicateKeyStr string + ) + + // Build ON condition + for index, keys := range duplicateKey { + if index != 0 { + duplicateKeyStr += " AND " + } + duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys) + } + + // Build SQL based on whether UPDATE is needed + pattern := gstr.Trim( + `MERGE INTO %s T1 USING (VALUES(%s)) T2 (%s) ON (%s) WHEN NOT MATCHED THEN INSERT(%s) VALUES (%s)`, + ) + if len(updateValues) > 0 { + // Upsert: INSERT or UPDATE + pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`) + return fmt.Sprintf( + pattern+";", + table, + queryHolderStr, + insertKeyStr, + duplicateKeyStr, + insertKeyStr, + insertValueStr, + strings.Join(updateValues, ","), + ) + } + // Insert Ignore: INSERT only + return fmt.Sprintf(pattern+";", table, queryHolderStr, insertKeyStr, duplicateKeyStr, insertKeyStr, insertValueStr) +} diff --git a/database/drivers/mssql/mssql_open.go b/database/drivers/mssql/mssql_open.go new file mode 100644 index 0000000..148cbc4 --- /dev/null +++ b/database/drivers/mssql/mssql_open.go @@ -0,0 +1,62 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "database/sql" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gstr" +) + +// Open creates and returns an underlying sql.DB object for mssql. +func (d *Driver) Open(config *database.ConfigNode) (db *sql.DB, err error) { + source, err := configNodeToSource(config) + if err != nil { + return nil, err + } + underlyingDriverName := "sqlserver" + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} + +func configNodeToSource(config *database.ConfigNode) (string, error) { + var source string + source = fmt.Sprintf( + "user id=%s;password=%s;server=%s;encrypt=disable", + config.User, config.Pass, config.Host, + ) + if config.Name != "" { + source = fmt.Sprintf("%s;database=%s", source, config.Name) + } + if config.Port != "" { + source = fmt.Sprintf("%s;port=%s", source, config.Port) + } + if config.Extra != "" { + extraMap, err := gstr.Parse(config.Extra) + if err != nil { + return "", gerror.WrapCodef( + gcode.CodeInvalidParameter, + err, + `invalid extra configuration: %s`, config.Extra, + ) + } + for k, v := range extraMap { + source += fmt.Sprintf(`;%s=%s`, k, v) + } + } + return source, nil +} diff --git a/database/drivers/mssql/mssql_result.go b/database/drivers/mssql/mssql_result.go new file mode 100644 index 0000000..57f3f41 --- /dev/null +++ b/database/drivers/mssql/mssql_result.go @@ -0,0 +1,22 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +// Result instance of sql.Result +type Result struct { + lastInsertId int64 + rowsAffected int64 + err error +} + +func (r *Result) LastInsertId() (int64, error) { + return r.lastInsertId, r.err +} + +func (r *Result) RowsAffected() (int64, error) { + return r.rowsAffected, r.err +} diff --git a/database/drivers/mssql/mssql_table_fields.go b/database/drivers/mssql/mssql_table_fields.go new file mode 100644 index 0000000..b43c3e3 --- /dev/null +++ b/database/drivers/mssql/mssql_table_fields.go @@ -0,0 +1,88 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/util/gutil" +) + +var ( + tableFieldsSqlTmp = ` +SELECT + c.name AS Field, + CASE + WHEN t.name IN ('datetime', 'datetime2', 'smalldatetime', 'date', 'time', 'text', 'ntext', 'image', 'xml') THEN t.name + WHEN t.name IN ('decimal', 'numeric') THEN t.name + '(' + CAST(c.precision AS varchar(20)) + ',' + CAST(c.scale AS varchar(20)) + ')' + WHEN t.name IN ('char', 'varchar', 'binary', 'varbinary') THEN t.name + '(' + CASE WHEN c.max_length = -1 THEN 'max' ELSE CAST(c.max_length AS varchar(20)) END + ')' + WHEN t.name IN ('nchar', 'nvarchar') THEN t.name + '(' + CASE WHEN c.max_length = -1 THEN 'max' ELSE CAST(c.max_length/2 AS varchar(20)) END + ')' + ELSE t.name + END AS Type, + CASE WHEN c.is_nullable = 1 THEN 'YES' ELSE 'NO' END AS [Null], + CASE WHEN pk.column_id IS NOT NULL THEN 'PRI' ELSE '' END AS [Key], + CASE WHEN c.is_identity = 1 THEN 'IDENTITY' ELSE '' END AS Extra, + ISNULL(dc.definition, '') AS [Default], + ISNULL(CAST(ep.value AS nvarchar(max)), '') AS [Comment] +FROM sys.columns c +INNER JOIN sys.objects o ON c.object_id = o.object_id AND o.type = 'U' AND o.is_ms_shipped = 0 +INNER JOIN sys.types t ON c.user_type_id = t.user_type_id +LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id +LEFT JOIN sys.extended_properties ep ON c.object_id = ep.major_id AND c.column_id = ep.minor_id AND ep.name = 'MS_Description' +LEFT JOIN ( + SELECT ic.object_id, ic.column_id + FROM sys.index_columns ic + INNER JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id + WHERE i.is_primary_key = 1 +) pk ON c.object_id = pk.object_id AND c.column_id = pk.column_id +WHERE o.name = '%s' +ORDER BY c.column_id +` +) + +func init() { + var err error + tableFieldsSqlTmp, err = database.FormatMultiLineSqlToSingle(tableFieldsSqlTmp) + if err != nil { + panic(err) + } +} + +// TableFields retrieves and returns the fields' information of specified table of current schema. +// +// Also see DriverMysql.TableFields. +func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*database.TableField, err error) { + var ( + result database.Result + link database.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + structureSql := fmt.Sprintf(tableFieldsSqlTmp, table) + result, err = d.DoSelect(ctx, link, structureSql) + if err != nil { + return nil, err + } + fields = make(map[string]*database.TableField) + for i, m := range result { + fields[m["Field"].String()] = &database.TableField{ + Index: i, + Name: m["Field"].String(), + Type: m["Type"].String(), + Null: m["Null"].Bool(), + Key: m["Key"].String(), + Default: m["Default"].Val(), + Extra: m["Extra"].String(), + Comment: m["Comment"].String(), + } + } + return fields, nil +} diff --git a/database/drivers/mssql/mssql_tables.go b/database/drivers/mssql/mssql_tables.go new file mode 100644 index 0000000..cf9be2e --- /dev/null +++ b/database/drivers/mssql/mssql_tables.go @@ -0,0 +1,38 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mssql + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +const ( + tablesSqlTmp = `SELECT name FROM sys.objects WHERE type='U' AND is_ms_shipped = 0 ORDER BY name` +) + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result database.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + + result, err = d.DoSelect(ctx, link, tablesSqlTmp) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} diff --git a/database/drivers/mysql/mysql.go b/database/drivers/mysql/mysql.go index f19c0d4..f1df5d2 100644 --- a/database/drivers/mysql/mysql.go +++ b/database/drivers/mysql/mysql.go @@ -4,20 +4,16 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -// Package mysql implements database.Driver for MySQL database. +// Package mysql implements database.Driver, which supports operations for database MySQL. package mysql import ( - "database/sql" - "fmt" - "strings" - - _ "github.com/go-sql-driver/mysql" - "git.magicany.cc/black1552/gin-base/database" + "git.magicany.cc/black1552/gin-base/database/consts" + _ "github.com/go-sql-driver/mysql" ) -// Driver is the driver for MySQL database. +// Driver is the driver for mysql database. type Driver struct { *database.Core } @@ -28,24 +24,23 @@ const ( func init() { var ( - err error - driverObj = New() + err error + driverObj = New() + driverNames = consts.SliceStr{"mysql", "mariadb", "tidb"} // TODO remove mariadb and tidb in future versions. ) - // Register for both mysql and mariadb (compatible protocol) - if err = database.Register("mysql", driverObj); err != nil { - panic(err) - } - if err = database.Register("mariadb", driverObj); err != nil { - panic(err) + for _, driverName := range driverNames { + if err = database.Register(driverName, driverObj); err != nil { + panic(err) + } } } -// New creates and returns a driver that implements database.Driver for MySQL. +// New create and returns a driver that implements database.Driver, which supports operations for MySQL. func New() database.Driver { return &Driver{} } -// New creates and returns a database object for MySQL. +// New creates and returns a database object for mysql. // It implements the interface of database.Driver for extra database driver installation. func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.DB, error) { return &Driver{ @@ -53,31 +48,7 @@ func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.D }, nil } -// GetChars returns the security char for MySQL database. +// GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// Open creates and returns an underlying sql.DB object for MySQL. -func (d *Driver) Open(config *database.ConfigNode) (*sql.DB, error) { - var ( - source string - username = config.User - password = config.Pass - protocol = "tcp" - dbName = config.Name - params = make([]string, 0) - ) - - if config.Extra != "" { - params = append(params, config.Extra) - } - - // Default params - params = append(params, "loc=Local", "parseTime=true") - - source = fmt.Sprintf("%s:%s@%s(%s:%s)/%s?%s", - username, password, protocol, config.Host, config.Port, dbName, strings.Join(params, "&")) - - return sql.Open("mysql", source) -} diff --git a/database/drivers/mysql/mysql_do_filter.go b/database/drivers/mysql/mysql_do_filter.go new file mode 100644 index 0000000..20d7ba2 --- /dev/null +++ b/database/drivers/mysql/mysql_do_filter.go @@ -0,0 +1,20 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mysql + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +// DoFilter handles the sql before posts it to database. +func (d *Driver) DoFilter( + ctx context.Context, link database.Link, sql string, args []any, +) (newSql string, newArgs []any, err error) { + return d.Core.DoFilter(ctx, link, sql, args) +} diff --git a/database/drivers/mysql/mysql_open.go b/database/drivers/mysql/mysql_open.go new file mode 100644 index 0000000..05dd528 --- /dev/null +++ b/database/drivers/mysql/mysql_open.go @@ -0,0 +1,60 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mysql + +import ( + "database/sql" + "fmt" + "net/url" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +// Open creates and returns an underlying sql.DB object for mysql. +// Note that it converts time.Time argument to local timezone in default. +func (d *Driver) Open(config *database.ConfigNode) (db *sql.DB, err error) { + var ( + source = configNodeToSource(config) + underlyingDriverName = "mysql" + ) + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} + +// [username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN] +func configNodeToSource(config *database.ConfigNode) string { + var ( + source string + portStr string + ) + if config.Port != "" { + portStr = ":" + config.Port + } + source = fmt.Sprintf( + "%s:%s@%s(%s%s)/%s?charset=%s", + config.User, config.Pass, config.Protocol, config.Host, portStr, config.Name, config.Charset, + ) + if config.Timezone != "" { + if strings.Contains(config.Timezone, "/") { + config.Timezone = url.QueryEscape(config.Timezone) + } + source = fmt.Sprintf("%s&loc=%s", source, config.Timezone) + } + if config.Extra != "" { + source = fmt.Sprintf("%s&%s", source, config.Extra) + } + return source +} diff --git a/database/drivers/mysql/mysql_table_fields.go b/database/drivers/mysql/mysql_table_fields.go new file mode 100644 index 0000000..ac73ad8 --- /dev/null +++ b/database/drivers/mysql/mysql_table_fields.go @@ -0,0 +1,105 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mysql + +import ( + "context" + "fmt" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/util/gutil" +) + +var ( + // tableFieldsSqlByMariadb is the query statement for retrieving table fields' information in MariaDB. + // Deprecated: Use package `contrib/drivers/mariadb` instead. + // TODO remove in next version. + tableFieldsSqlByMariadb = ` +SELECT + c.COLUMN_NAME AS 'Field', + ( CASE WHEN ch.CHECK_CLAUSE LIKE 'json_valid%%' THEN 'json' ELSE c.COLUMN_TYPE END ) AS 'Type', + c.COLLATION_NAME AS 'Collation', + c.IS_NULLABLE AS 'Null', + c.COLUMN_KEY AS 'Key', + ( CASE WHEN c.COLUMN_DEFAULT = 'NULL' OR c.COLUMN_DEFAULT IS NULL THEN NULL ELSE c.COLUMN_DEFAULT END) AS 'Default', + c.EXTRA AS 'Extra', + c.PRIVILEGES AS 'Privileges', + c.COLUMN_COMMENT AS 'Comment' +FROM + information_schema.COLUMNS AS c + LEFT JOIN information_schema.CHECK_CONSTRAINTS AS ch ON c.TABLE_NAME = ch.TABLE_NAME + AND c.TABLE_SCHEMA = ch.CONSTRAINT_SCHEMA + AND c.COLUMN_NAME = ch.CONSTRAINT_NAME +WHERE + c.TABLE_SCHEMA = '%s' + AND c.TABLE_NAME = '%s' + ORDER BY c.ORDINAL_POSITION` +) + +func init() { + var err error + tableFieldsSqlByMariadb, err = database.FormatMultiLineSqlToSingle(tableFieldsSqlByMariadb) + if err != nil { + panic(err) + } +} + +// TableFields retrieves and returns the fields' information of specified table of current +// schema. +// +// The parameter `link` is optional, if given nil it automatically retrieves a raw sql connection +// as its link to proceed necessary sql query. +// +// Note that it returns a map containing the field name and its corresponding fields. +// As a map is unsorted, the TableField struct has a "Index" field marks its sequence in +// the fields. +// +// It's using cache feature to enhance the performance, which is never expired util the +// process restarts. +func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*database.TableField, err error) { + var ( + result database.Result + link database.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + tableFieldsSql string + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + dbType := d.GetConfig().Type + switch dbType { + // Deprecated: Use package `contrib/drivers/mariadb` instead. + // TODO remove in next version. + case "mariadb": + tableFieldsSql = fmt.Sprintf(tableFieldsSqlByMariadb, usedSchema, table) + default: + tableFieldsSql = fmt.Sprintf(`SHOW FULL COLUMNS FROM %s`, d.QuoteWord(table)) + } + + result, err = d.DoSelect( + ctx, link, + tableFieldsSql, + ) + if err != nil { + return nil, err + } + fields = make(map[string]*database.TableField) + for i, m := range result { + fields[m["Field"].String()] = &database.TableField{ + Index: i, + Name: m["Field"].String(), + Type: m["Type"].String(), + Null: strings.EqualFold(m["Null"].String(), "YES"), + Key: m["Key"].String(), + Default: m["Default"].Val(), + Extra: m["Extra"].String(), + Comment: m["Comment"].String(), + } + } + return fields, nil +} diff --git a/database/drivers/mysql/mysql_tables.go b/database/drivers/mysql/mysql_tables.go new file mode 100644 index 0000000..8880889 --- /dev/null +++ b/database/drivers/mysql/mysql_tables.go @@ -0,0 +1,33 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package mysql + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result database.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, `SHOW TABLES`) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} diff --git a/database/drivers/mysql/testdata/date_time_example.sql b/database/drivers/mysql/testdata/date_time_example.sql new file mode 100644 index 0000000..4dc0568 --- /dev/null +++ b/database/drivers/mysql/testdata/date_time_example.sql @@ -0,0 +1,9 @@ +CREATE TABLE `date_time_example` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `year` year DEFAULT NULL COMMENT 'year', + `date` date DEFAULT NULL COMMENT 'Date', + `time` time DEFAULT NULL COMMENT 'time', + `datetime` datetime DEFAULT NULL COMMENT 'datetime', + `timestamp` timestamp NULL DEFAULT NULL COMMENT 'Timestamp', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; \ No newline at end of file diff --git a/database/drivers/mysql/testdata/fix_gdb_join.sql b/database/drivers/mysql/testdata/fix_gdb_join.sql new file mode 100644 index 0000000..48cf1ef --- /dev/null +++ b/database/drivers/mysql/testdata/fix_gdb_join.sql @@ -0,0 +1,151 @@ + + +DROP TABLE IF EXISTS `common_resource`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `common_resource` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `app_id` bigint(20) NOT NULL, + `resource_id` varchar(64) NOT NULL, + `src_instance_id` varchar(64) DEFAULT NULL, + `region` varchar(36) DEFAULT NULL, + `zone` varchar(36) DEFAULT NULL, + `database_kind` varchar(20) NOT NULL, + `source_type` varchar(64) NOT NULL, + `ip` varchar(64) DEFAULT NULL, + `port` int(10) DEFAULT NULL, + `vpc_id` varchar(20) DEFAULT NULL, + `subnet_id` varchar(20) DEFAULT NULL, + `proxy_ip` varchar(64) DEFAULT NULL, + `proxy_port` int(10) DEFAULT NULL, + `proxy_id` bigint(20) DEFAULT NULL, + `proxy_snat_ip` varchar(64) DEFAULT NULL, + `lease_at` timestamp NULL DEFAULT NULL, + `uin` varchar(32) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_resource` (`app_id`,`src_instance_id`,`vpc_id`,`subnet_id`,`ip`,`port`), + KEY `resource_id` (`resource_id`) +) ENGINE=InnoDB AUTO_INCREMENT=21 DEFAULT CHARSET=utf8mb4 COMMENT='资源公共信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `common_resource` +-- + +LOCK TABLES `common_resource` WRITE; +/*!40000 ALTER TABLE `common_resource` DISABLE KEYS */; +INSERT INTO `common_resource` VALUES (1,1,'2','2','2','3','1','1','1',1,'1','1','1',1,1,'1',NULL,''),(3,2,'3','3','3','3','3','3','3',3,'3','3','3',3,3,'3',NULL,''),(18,1303697168,'dmc-rgnh9qre','vdb-6b6m3u1u','ap-guangzhou','','vdb','cloud','10.0.1.16',80,'vpc-m3dchft7','subnet-9as3a3z2','9.27.72.189',11131,228476,'169.254.128.5, ','2023-11-08 08:13:04',''),(20,1303697168,'dmc-4grzi4jg','tdsqlshard-313spncx','ap-guangzhou','','tdsql','cloud','10.255.0.27',3306,'vpc-407k0e8x','subnet-qhkkk3bo','30.86.239.200',24087,0,'',NULL,''); +/*!40000 ALTER TABLE `common_resource` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `managed_resource` +-- + +DROP TABLE IF EXISTS `managed_resource`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `managed_resource` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `instance_id` varchar(64) NOT NULL, + `resource_id` varchar(64) NOT NULL, + `resource_name` varchar(64) DEFAULT NULL, + `status` varchar(36) NOT NULL DEFAULT 'valid', + `status_message` varchar(64) DEFAULT NULL, + `user` varchar(64) NOT NULL, + `password` varchar(1024) NOT NULL, + `pay_mode` tinyint(1) DEFAULT '0', + `safe_publication` bit(1) DEFAULT b'0', + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `expired_at` timestamp NULL DEFAULT NULL, + `deleted` tinyint(1) NOT NULL DEFAULT '0', + `resource_mark_id` int(11) DEFAULT NULL, + `comments` varchar(64) DEFAULT NULL, + `rule_template_id` varchar(64) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `resource_id` (`resource_id`), + KEY `instance_id` (`instance_id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COMMENT='管控实例表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `managed_resource` +-- + +LOCK TABLES `managed_resource` WRITE; +/*!40000 ALTER TABLE `managed_resource` DISABLE KEYS */; +INSERT INTO `managed_resource` VALUES (1,'2','3','1','1','1','1','1',1,_binary '','2023-11-06 12:14:21','2023-11-06 12:14:21',NULL,1,1,'1',''),(2,'3','2','1','1','1','1','1',1,_binary '\0','2023-11-06 12:15:07','2023-11-06 12:15:07',NULL,1,2,'1',''),(5,'dmcins-jxy0x75m','dmc-rgnh9qre','erichmao-vdb-test','invalid','The Ip field is required','root','2e39af3dd1d447e2b1437b40c62c35995fa22b370c7455ff7815dace3a6e8891ccadcfc893fe1342a4102d742bd7a3e603cd0ac1fcdc072d7c0b5be5836ec87306981b629f9b59aedf0316e9504ab172fa1c95756d5b260114e4feaa0b19223fb61cb268cc4818307ed193dbab830cf556b91cde182686eb70f70ea77f69eff66230dec2ce92bd3352cad31abf47597a5cc6a0d638381dc3bae7aa1b142730790a6d4cefdef1bd460061c966ad5008c2b5fc971b7f4d7dddffa5b1456c45e2917763dd8fffb1fa7fc4783feca95dafc9a9f4edf21b0579f76b0a3154f087e3b9a7fc49af8ff92b12e7b03caa865e72e777dd9d35a11910df0d55ead90e47d5f8',1,_binary '','2023-11-08 08:13:20','2023-11-09 05:31:07',NULL,0,11,NULL,'12345'),(6,'dmcins-erxms6ya','dmc-4grzi4jg','erichmao-vdb-test','invalid','The Ip field is required','leotaowang','641d846cf75bc7944202251d97dca8335f7f149dd4fd911ca5b87c71ef1dc5d0a66c4e5021ef7ad53136cda2fb2567d34e3dd1a7666e3f64ebf532eb2a55d84952aac86b4211f563f7b9da7dd0f88ec288d6680d3513cea0c1b7ad7babb474717f77ebbc9d63bb458adaf982887da9e63df957ffda572c1c3ed187471b99fdc640b45fed76a6d50dc1090eee79b4d94d056c4d43416133481f55bd040759398680104a84d801e6475dcfe919a00859908296747430b728a00c8d54256ae220235a138e0bbf08fe8b6fc8589971436b55bff966154721a91adbdc9c2b6f50ef5849ed77e5b028116abac51584b8d401cd3a88d18df127006358ed33fc3fa6f480',1,_binary '','2023-11-08 22:15:17','2023-11-09 05:31:07',NULL,0,11,NULL,'12345'); +/*!40000 ALTER TABLE `managed_resource` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `rules_template` +-- + +DROP TABLE IF EXISTS `rules_template`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `rules_template` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `app_id` bigint(20) DEFAULT NULL, + `name` varchar(255) NOT NULL, + `database_kind` varchar(64) DEFAULT NULL, + `is_default` tinyint(1) NOT NULL DEFAULT '0', + `win_rules` varchar(2048) DEFAULT NULL, + `inception_rules` varchar(2048) DEFAULT NULL, + `auto_exec_rules` varchar(2048) DEFAULT NULL, + `order_check_step` varchar(2048) DEFAULT NULL, + `template_id` varchar(64) NOT NULL DEFAULT '', + `version` int(11) NOT NULL DEFAULT '1', + `deleted` tinyint(1) NOT NULL DEFAULT '0', + `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `is_system` tinyint(1) NOT NULL DEFAULT '0', + `uin` varchar(64) DEFAULT NULL, + `subAccountUin` varchar(64) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_template_id` (`template_id`), + UNIQUE KEY `uniq_name` (`name`,`app_id`,`deleted`,`uin`) USING BTREE +) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `rules_template` +-- + +LOCK TABLES `rules_template` WRITE; +/*!40000 ALTER TABLE `rules_template` DISABLE KEYS */; +/*!40000 ALTER TABLE `rules_template` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `resource_mark` +-- + +DROP TABLE IF EXISTS `resource_mark`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `resource_mark` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `app_id` bigint(20) NOT NULL, + `mark_name` varchar(64) NOT NULL, + `color` varchar(11) NOT NULL, + `creator` varchar(32) NOT NULL, + `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + UNIQUE KEY `app_id_name` (`app_id`,`mark_name`) +) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 COMMENT='标签信息表'; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `resource_mark` +-- + +LOCK TABLES `resource_mark` WRITE; +/*!40000 ALTER TABLE `resource_mark` DISABLE KEYS */; +INSERT INTO `resource_mark` VALUES (10,1,'test','red','1','2023-11-06 02:45:46','2023-11-06 02:45:46'); +/*!40000 ALTER TABLE `resource_mark` ENABLE KEYS */; +UNLOCK TABLES; + diff --git a/database/drivers/mysql/testdata/fix_gdb_join_expect.sql b/database/drivers/mysql/testdata/fix_gdb_join_expect.sql new file mode 100644 index 0000000..2ee4a53 --- /dev/null +++ b/database/drivers/mysql/testdata/fix_gdb_join_expect.sql @@ -0,0 +1 @@ +SELECT `managed_resource`.`resource_id`,`managed_resource`.`user`,`managed_resource`.`status`,`managed_resource`.`status_message`,`managed_resource`.`safe_publication`,`managed_resource`.`rule_template_id`,`managed_resource`.`created_at`,`managed_resource`.`comments`,`managed_resource`.`expired_at`,`managed_resource`.`resource_mark_id`,`managed_resource`.`instance_id`,`managed_resource`.`resource_name`,`managed_resource`.`pay_mode`,`resource_mark`.`mark_name`,`resource_mark`.`color`,`rules_template`.`name`,`common_resource`.`src_instance_id`,`common_resource`.`database_kind`,`common_resource`.`source_type`,`common_resource`.`ip`,`common_resource`.`port` FROM `managed_resource` LEFT JOIN `common_resource` ON (`managed_resource`.`resource_id`=`common_resource`.`resource_id`) LEFT JOIN `resource_mark` ON (`managed_resource`.`resource_mark_id` = `resource_mark`.`id`) LEFT JOIN `rules_template` ON (`managed_resource`.`rule_template_id` = `rules_template`.`template_id`) ORDER BY `src_instance_id` ASC \ No newline at end of file diff --git a/database/drivers/mysql/testdata/fix_gdb_order_by.sql b/database/drivers/mysql/testdata/fix_gdb_order_by.sql new file mode 100644 index 0000000..2ba7126 --- /dev/null +++ b/database/drivers/mysql/testdata/fix_gdb_order_by.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS `employee` +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + age INT NOT NULL +); + +INSERT INTO employee(name, age) VALUES ('John', 30); +INSERT INTO employee(name, age) VALUES ('Mary', 28); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/1380.sql b/database/drivers/mysql/testdata/issues/1380.sql new file mode 100644 index 0000000..59aefb5 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/1380.sql @@ -0,0 +1,35 @@ +CREATE TABLE `jfy_gift` ( +`id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT, +`gift_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '礼品名称', +`at_least_recharge_count` int(0) UNSIGNED NOT NULL DEFAULT 1 COMMENT '最少兑换数量', +`comments` json NOT NULL COMMENT '礼品留言', +`content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '礼品详情', +`cost_price` decimal(10, 2) NULL DEFAULT NULL COMMENT '成本价', +`cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '封面', +`covers` json NOT NULL COMMENT '礼品图片库', +`description` varchar(62) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '礼品备注', +`express_type` json NOT NULL COMMENT '配送方式', +`gift_type` int(0) NOT NULL COMMENT '礼品类型:1:实物;2:虚拟;3:优惠券;4:积分券', +`has_props` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否有多个属性', +`is_limit_sell` tinyint(0) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否限购', +`limit_customer_tags` json NOT NULL COMMENT '语序购买的会员标签', +`limit_sell_custom` tinyint(0) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否开启允许购买的会员标签', +`limit_sell_cycle` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '限购周期', +`limit_sell_cycle_count` int(0) NOT NULL COMMENT '限购期内允许购买的数量', +`limit_sell_type` tinyint(0) NOT NULL COMMENT '限购类型', +`market_price` decimal(10, 2) NOT NULL COMMENT '市场价', +`out_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '内部编码', +`props` json NOT NULL COMMENT '规格', +`skus` json NOT NULL COMMENT 'SKU', +`score_price` decimal(10, 2) NOT NULL COMMENT '兑换所需积分', +`stock` int(0) NOT NULL COMMENT '库存', +`create_at` datetime(0) NOT NULL COMMENT '创建日期', +`store_id` int(0) NOT NULL COMMENT '所属商城', +`status` int(0) UNSIGNED NULL DEFAULT 1 COMMENT '1:下架;20:审核中;30:复审中;99:上架', +`view_count` int(0) NOT NULL DEFAULT 0 COMMENT '访问量', +`sell_count` int(0) NULL DEFAULT 0 COMMENT '销量', +PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + + +INSERT INTO `jfy_gift` VALUES (17, 'GIFT', 1, '[{\"name\": \"身份证\", \"field\": \"idcard\", \"required\": false}, {\"name\": \"留言2\", \"field\": \"text\", \"required\": false}]', '

礼品详情

', 0.00, '', '{\"list\": [{\"uid\": \"vc-upload-1629292486099-3\", \"url\": \"https://cdn.taobao.com/sULsYiwaOPjsKGoBXwKtuewPzACpBDfQ.jpg\", \"name\": \"O1CN01OH6PIP1Oc5ot06U17_!!922361725.jpg\", \"status\": \"done\"}, {\"uid\": \"vc-upload-1629292486099-4\", \"url\": \"https://cdn.taobao.com/lqLHDcrFTgNvlWyXfLYZwmsrODzIBtFH.jpg\", \"name\": \"O1CN018hBckI1Oc5ouc8ppl_!!922361725.jpg\", \"status\": \"done\"}, {\"uid\": \"vc-upload-1629292486099-5\", \"url\": \"https://cdn.taobao.com/pvqyutXckICmHhbPBQtrVLHuMlXuGxUg.jpg\", \"name\": \"O1CN0185Ubp91Oc5osQTTcc_!!922361725.jpg\", \"status\": \"done\"}]}', '支持个性定制的父亲节老师长辈的专属礼物', '[\"快递包邮\", \"同城配送\"]', 1, 0, 0, '[]', 0, 'day', 0, 1, 0.00, '259402', '[{\"name\": \"颜色\", \"values\": [\"红色\", \"蓝色\"]}]', '[{\"name\": \"red\", \"stock\": 10, \"gift_id\": 1, \"cost_price\": 80, \"score_price\": 188, \"market_price\": 388}, {\"name\": \"blue\", \"stock\": 100, \"gift_id\": 2, \"cost_price\": 81, \"score_price\": 200, \"market_price\": 288}]', 10.00, 0, '2021-08-18 21:26:13', 100004, 99, 0, 0); diff --git a/database/drivers/mysql/testdata/issues/1401.sql b/database/drivers/mysql/testdata/issues/1401.sql new file mode 100644 index 0000000..b090873 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/1401.sql @@ -0,0 +1,32 @@ +-- ---------------------------- +-- Table structure for parcel_items +-- ---------------------------- +DROP TABLE IF EXISTS `parcel_items`; +CREATE TABLE `parcel_items` ( + `id` int(11) NOT NULL, + `parcel_id` int(11) NULL DEFAULT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of parcel_items +-- ---------------------------- +INSERT INTO `parcel_items` VALUES (1, 1, '新品'); +INSERT INTO `parcel_items` VALUES (2, 3, '新品2'); + +-- ---------------------------- +-- Table structure for parcels +-- ---------------------------- +DROP TABLE IF EXISTS `parcels`; +CREATE TABLE `parcels` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of parcels +-- ---------------------------- +INSERT INTO `parcels` VALUES (1); +INSERT INTO `parcels` VALUES (2); +INSERT INTO `parcels` VALUES (3); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/1412.sql b/database/drivers/mysql/testdata/issues/1412.sql new file mode 100644 index 0000000..453fca7 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/1412.sql @@ -0,0 +1,30 @@ +-- ---------------------------- +-- Table structure for items +-- ---------------------------- +CREATE TABLE `items` ( + `id` int(11) NOT NULL, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of items +-- ---------------------------- +INSERT INTO `items` VALUES (1, '金秋产品1'); +INSERT INTO `items` VALUES (2, '金秋产品2'); + +-- ---------------------------- +-- Table structure for parcels +-- ---------------------------- +CREATE TABLE `parcels` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `item_id` int(11) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of parcels +-- ---------------------------- +INSERT INTO `parcels` VALUES (1, 1); +INSERT INTO `parcels` VALUES (2, 2); +INSERT INTO `parcels` VALUES (3, 0); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/2105.sql b/database/drivers/mysql/testdata/issues/2105.sql new file mode 100644 index 0000000..55f09b8 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/2105.sql @@ -0,0 +1,9 @@ +CREATE TABLE `issue2105` ( + `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, + `json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin, + PRIMARY KEY (`id`) +) ENGINE=InnoDB; + + +INSERT INTO `issue2105` VALUES ('1', NULL); +INSERT INTO `issue2105` VALUES ('2', '[{\"Name\": \"任务类型\", \"Value\": \"高价值\"}, {\"Name\": \"优先级\", \"Value\": \"高\"}, {\"Name\": \"是否亮点功能\", \"Value\": \"是\"}]'); diff --git a/database/drivers/mysql/testdata/issues/2119.sql b/database/drivers/mysql/testdata/issues/2119.sql new file mode 100644 index 0000000..89da7d1 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/2119.sql @@ -0,0 +1,47 @@ +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for sys_role +-- ---------------------------- +DROP TABLE IF EXISTS `sys_role`; +CREATE TABLE `sys_role` ( + `id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '||s', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '角色名称||s,r', + `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '角色 code||s,r', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '描述信息|text', + `weight` int(0) UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序||r|min:0#发布状态不能小于 0', + `status_id` int(0) UNSIGNED NOT NULL DEFAULT 1 COMMENT '发布状态|hasOne|f:status,fk:id', + `created_at` datetime(0) NULL DEFAULT NULL, + `updated_at` datetime(0) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `code`(`code`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1091 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统角色表' ROW_FORMAT = Compact; + +-- ---------------------------- +-- Records of sys_role +-- ---------------------------- +INSERT INTO `sys_role` VALUES (1, '开发人员', 'developer', '123123', 900, 2, '2022-09-03 21:25:03', '2022-09-09 23:35:23'); +INSERT INTO `sys_role` VALUES (2, '管理员', 'admin', '', 800, 1, '2022-09-03 21:25:03', '2022-09-09 23:00:17'); +INSERT INTO `sys_role` VALUES (3, '运营', 'operator', '', 700, 1, '2022-09-03 21:25:03', '2022-09-03 21:25:03'); +INSERT INTO `sys_role` VALUES (4, '客服', 'service', '', 600, 1, '2022-09-03 21:25:03', '2022-09-03 21:25:03'); +INSERT INTO `sys_role` VALUES (5, '收银', 'account', '', 500, 1, '2022-09-03 21:25:03', '2022-09-03 21:25:03'); + +-- ---------------------------- +-- Table structure for sys_status +-- ---------------------------- +DROP TABLE IF EXISTS `sys_status`; +CREATE TABLE `sys_status` ( + `id` int(0) UNSIGNED NOT NULL AUTO_INCREMENT, + `en` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '英文名称', + `cn` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '中文名称', + `weight` int(0) UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序权重', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '发布状态' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of sys_status +-- ---------------------------- +INSERT INTO `sys_status` VALUES (1, 'on line', '上线', 900); +INSERT INTO `sys_status` VALUES (2, 'undecided', '未决定', 800); +INSERT INTO `sys_status` VALUES (3, 'off line', '下线', 700); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/2439.sql b/database/drivers/mysql/testdata/issues/2439.sql new file mode 100644 index 0000000..0949263 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/2439.sql @@ -0,0 +1,19 @@ +CREATE TABLE `a` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + PRIMARY KEY (id) USING BTREE +) ENGINE = InnoDB; +INSERT INTO `a` (`id`) VALUES ('2'); + +CREATE TABLE `b` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NOT NULL , + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB; +INSERT INTO `b` (`id`, `name`) VALUES ('2', 'a'); +INSERT INTO `b` (`id`, `name`) VALUES ('3', 'b'); + +CREATE TABLE `c` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB; +INSERT INTO `c` (`id`) VALUES ('2'); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/2643.sql b/database/drivers/mysql/testdata/issues/2643.sql new file mode 100644 index 0000000..e145460 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/2643.sql @@ -0,0 +1,7 @@ +CREATE TABLE `issue2643` ( + `id` INT(10) NULL DEFAULT NULL, + `name` VARCHAR(50) NULL DEFAULT NULL, + `value` INT(10) NULL DEFAULT NULL, + `dept` VARCHAR(50) NULL DEFAULT NULL +) +ENGINE=InnoDB \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/3086.sql b/database/drivers/mysql/testdata/issues/3086.sql new file mode 100644 index 0000000..329ca84 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/3086.sql @@ -0,0 +1,10 @@ +CREATE TABLE `issue3086_user` +( + `id` int(10) unsigned NOT NULL COMMENT 'User ID', + `passport` varchar(45) NOT NULL COMMENT 'User Passport', + `password` varchar(45) DEFAULT NULL COMMENT 'User Password', + `nickname` varchar(45) DEFAULT NULL COMMENT 'User Nickname', + `create_at` datetime DEFAULT NULL COMMENT 'Created Time', + `update_at` datetime DEFAULT NULL COMMENT 'Updated Time', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/database/drivers/mysql/testdata/issues/3218.sql b/database/drivers/mysql/testdata/issues/3218.sql new file mode 100644 index 0000000..93b5f9d --- /dev/null +++ b/database/drivers/mysql/testdata/issues/3218.sql @@ -0,0 +1,14 @@ +CREATE TABLE `issue3218_sys_config` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '配置名称', + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '配置值', + `created_at` timestamp NULL DEFAULT NULL COMMENT '创建时间', + `updated_at` timestamp NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `name`(`name`(191)) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci; + +-- ---------------------------- +-- Records of sys_config +-- ---------------------------- +INSERT INTO `issue3218_sys_config` VALUES (49, 'site', '{\"banned_ip\":\"22\",\"filings\":\"2222\",\"fixed_page\":\"\",\"site_name\":\"22\",\"version\":\"22\"}', '2023-12-19 14:08:25', '2023-12-19 14:08:25'); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/3626.sql b/database/drivers/mysql/testdata/issues/3626.sql new file mode 100644 index 0000000..e4ee530 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/3626.sql @@ -0,0 +1,5 @@ +CREATE TABLE `issue3626` ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/3754.sql b/database/drivers/mysql/testdata/issues/3754.sql new file mode 100644 index 0000000..e6cdac0 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/3754.sql @@ -0,0 +1,8 @@ +CREATE TABLE `issue3754` ( + id int(11) NOT NULL, + name varchar(45) DEFAULT NULL, + create_at datetime(0) DEFAULT NULL, + update_at datetime(0) DEFAULT NULL, + delete_at datetime(0) DEFAULT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/3915.sql b/database/drivers/mysql/testdata/issues/3915.sql new file mode 100644 index 0000000..6fa6b86 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/3915.sql @@ -0,0 +1,9 @@ +CREATE TABLE `issue3915` ( + `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT 'user id', + `a` float DEFAULT NULL COMMENT 'user name', + `b` float DEFAULT NULL COMMENT 'user status', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `issue3915` (`id`,`a`,`b`) VALUES (1,1,2); +INSERT INTO `issue3915` (`id`,`a`,`b`) VALUES (2,5,4); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/4034.sql b/database/drivers/mysql/testdata/issues/4034.sql new file mode 100644 index 0000000..abb99ce --- /dev/null +++ b/database/drivers/mysql/testdata/issues/4034.sql @@ -0,0 +1,8 @@ +CREATE TABLE issue4034 ( + id INT PRIMARY KEY AUTO_INCREMENT, + passport VARCHAR(255), + password VARCHAR(255), + nickname VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/issues/4086.sql b/database/drivers/mysql/testdata/issues/4086.sql new file mode 100644 index 0000000..5e7ba66 --- /dev/null +++ b/database/drivers/mysql/testdata/issues/4086.sql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS `issue4086`; +CREATE TABLE `issue4086` ( + `proxy_id` bigint NOT NULL, + `recommend_ids` json DEFAULT NULL, + `photos` json DEFAULT NULL, + PRIMARY KEY (`proxy_id`) +) ENGINE=InnoDB; + +INSERT INTO `issue4086` (`proxy_id`, `recommend_ids`, `photos`) VALUES (1, '[584, 585]', 'null'); +INSERT INTO `issue4086` (`proxy_id`, `recommend_ids`, `photos`) VALUES (2, '[]', NULL); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/reservedwords_table_tpl.sql b/database/drivers/mysql/testdata/reservedwords_table_tpl.sql new file mode 100644 index 0000000..c844a58 --- /dev/null +++ b/database/drivers/mysql/testdata/reservedwords_table_tpl.sql @@ -0,0 +1,20 @@ +CREATE TABLE %s ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `key` varchar(45) DEFAULT NULL, + `category_id` int(10) unsigned NOT NULL, + `user_id` int(10) unsigned NOT NULL, + `title` varchar(255) NOT NULL, + `content` mediumtext NOT NULL, + `sort` int(10) unsigned DEFAULT '0', + `brief` varchar(255) DEFAULT NULL, + `thumb` varchar(255) DEFAULT NULL, + `tags` varchar(900) DEFAULT NULL, + `referer` varchar(255) DEFAULT NULL, + `status` smallint(5) unsigned DEFAULT '0', + `view_count` int(10) unsigned DEFAULT '0', + `zan_count` int(10) unsigned DEFAULT NULL, + `cai_count` int(10) unsigned DEFAULT NULL, + `created_at` datetime DEFAULT NULL, + `updated_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/database/drivers/mysql/testdata/table_with_prefix.sql b/database/drivers/mysql/testdata/table_with_prefix.sql new file mode 100644 index 0000000..8c4188d --- /dev/null +++ b/database/drivers/mysql/testdata/table_with_prefix.sql @@ -0,0 +1,7 @@ +CREATE TABLE `instance` ( + `f_id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(255) NULL DEFAULT '', + PRIMARY KEY (`f_id`) USING BTREE +) ENGINE = InnoDB; + +INSERT INTO `instance` VALUES (1, 'john'); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/with_multiple_depends.sql b/database/drivers/mysql/testdata/with_multiple_depends.sql new file mode 100644 index 0000000..f4327b9 --- /dev/null +++ b/database/drivers/mysql/testdata/with_multiple_depends.sql @@ -0,0 +1,33 @@ + +CREATE TABLE `table_a` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `alias` varchar(255) NULL DEFAULT '', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB; + +INSERT INTO `table_a` VALUES (1, 'table_a_test1'); +INSERT INTO `table_a` VALUES (2, 'table_a_test2'); + +CREATE TABLE `table_b` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `table_a_id` int(11) NOT NULL, + `alias` varchar(255) NULL DEFAULT '', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB; + +INSERT INTO `table_b` VALUES (10, 1, 'table_b_test1'); +INSERT INTO `table_b` VALUES (20, 2, 'table_b_test2'); +INSERT INTO `table_b` VALUES (30, 1, 'table_b_test3'); +INSERT INTO `table_b` VALUES (40, 2, 'table_b_test4'); + +CREATE TABLE `table_c` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `table_b_id` int(11) NOT NULL, + `alias` varchar(255) NULL DEFAULT '', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB; + +INSERT INTO `table_c` VALUES (100, 10, 'table_c_test1'); +INSERT INTO `table_c` VALUES (200, 10, 'table_c_test2'); +INSERT INTO `table_c` VALUES (300, 20, 'table_c_test3'); +INSERT INTO `table_c` VALUES (400, 30, 'table_c_test4'); \ No newline at end of file diff --git a/database/drivers/mysql/testdata/with_tpl_user.sql b/database/drivers/mysql/testdata/with_tpl_user.sql new file mode 100644 index 0000000..032e9c5 --- /dev/null +++ b/database/drivers/mysql/testdata/with_tpl_user.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + name varchar(45) NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/database/drivers/mysql/testdata/with_tpl_user_detail.sql b/database/drivers/mysql/testdata/with_tpl_user_detail.sql new file mode 100644 index 0000000..54bbccd --- /dev/null +++ b/database/drivers/mysql/testdata/with_tpl_user_detail.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS %s ( + uid int(10) unsigned NOT NULL AUTO_INCREMENT, + address varchar(45) NOT NULL, + PRIMARY KEY (uid) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/database/drivers/mysql/testdata/with_tpl_user_scores.sql b/database/drivers/mysql/testdata/with_tpl_user_scores.sql new file mode 100644 index 0000000..d1640ee --- /dev/null +++ b/database/drivers/mysql/testdata/with_tpl_user_scores.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS %s ( + id int(10) unsigned NOT NULL AUTO_INCREMENT, + uid int(10) unsigned NOT NULL, + score int(10) unsigned NOT NULL, + PRIMARY KEY (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; \ No newline at end of file diff --git a/database/drivers/oracle/oracle.go b/database/drivers/oracle/oracle.go index 5951af0..bf3aea0 100644 --- a/database/drivers/oracle/oracle.go +++ b/database/drivers/oracle/oracle.go @@ -4,58 +4,43 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -// Package oracle implements database.Driver for Oracle database. +// Package oracle implements database.Driver, which supports operations for database Oracle. package oracle import ( - "database/sql" - "fmt" - - _ "github.com/sijms/go-ora/v2" - "git.magicany.cc/black1552/gin-base/database" ) -// Driver is the driver for Oracle database. +// Driver is the driver for oracle database. type Driver struct { *database.Core } const ( - quoteChar = `"` + rowNumberAliasForSelect = `ROW_NUMBER__` + quoteChar = `"` ) func init() { - if err := database.Register("oracle", New()); err != nil { + if err := database.Register(`oracle`, New()); err != nil { panic(err) } } -// New creates and returns a driver that implements database.Driver for Oracle. +// New create and returns a driver that implements database.Driver, which supports operations for Oracle. func New() database.Driver { return &Driver{} } -// New creates and returns a database object for Oracle. +// New creates and returns a database object for oracle. +// It implements the interface of database.Driver for extra database driver installation. func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.DB, error) { return &Driver{ Core: core, }, nil } -// GetChars returns the security char for Oracle. +// GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// Open creates and returns an underlying sql.DB object for Oracle. -func (d *Driver) Open(config *database.ConfigNode) (*sql.DB, error) { - var source string - if config.Link != "" { - source = config.Link - } else { - source = fmt.Sprintf("oracle://%s:%s@%s:%s/%s", - config.User, config.Pass, config.Host, config.Port, config.Name) - } - return sql.Open("oracle", source) -} diff --git a/database/drivers/oracle/oracle_do_commit.go b/database/drivers/oracle/oracle_do_commit.go new file mode 100644 index 0000000..7b8717e --- /dev/null +++ b/database/drivers/oracle/oracle_do_commit.go @@ -0,0 +1,29 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package oracle + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +// DoCommit commits current sql and arguments to underlying sql driver. +func (d *Driver) DoCommit(ctx context.Context, in database.DoCommitInput) (out database.DoCommitOutput, err error) { + out, err = d.Core.DoCommit(ctx, in) + if err != nil { + return + } + if len(out.Records) > 0 { + // remove auto added field. + for i, record := range out.Records { + delete(record, rowNumberAliasForSelect) + out.Records[i] = record + } + } + return +} diff --git a/database/drivers/oracle/oracle_do_exec.go b/database/drivers/oracle/oracle_do_exec.go new file mode 100644 index 0000000..c536986 --- /dev/null +++ b/database/drivers/oracle/oracle_do_exec.go @@ -0,0 +1,120 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package oracle + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +const ( + returningClause = " RETURNING %s INTO ?" +) + +// DoExec commits the sql string and its arguments to underlying driver +// through given link object and returns the execution result. +// It handles INSERT statements specially to support LastInsertId. +func (d *Driver) DoExec( + ctx context.Context, link database.Link, sql string, args ...interface{}, +) (result sql.Result, err error) { + var ( + isUseCoreDoExec = true + primaryKey string + pkField database.TableField + ) + + // Transaction checks. + if link == nil { + if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil { + link = tx + } else if link, err = d.MasterLink(); err != nil { + return nil, err + } + } else if !link.IsTransaction() { + if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil { + link = tx + } + } + + // Check if it is an insert operation with primary key from context. + if value := ctx.Value(internalPrimaryKeyInCtx); value != nil { + if field, ok := value.(database.TableField); ok { + pkField = field + isUseCoreDoExec = false + } + } + + // Check if it is an INSERT statement with primary key. + if !isUseCoreDoExec && pkField.Name != "" && strings.Contains(strings.ToUpper(sql), "INSERT INTO") { + primaryKey = pkField.Name + // Oracle supports RETURNING clause to get the last inserted id + sql += fmt.Sprintf(returningClause, d.QuoteWord(primaryKey)) + } else { + // Use default DoExec for non-INSERT or no primary key scenarios + return d.Core.DoExec(ctx, link, sql, args...) + } + + // Only the insert operation with primary key can execute the following code + + // SQL filtering. + sql, args = d.FormatSqlBeforeExecuting(sql, args) + sql, args, err = d.DoFilter(ctx, link, sql, args) + if err != nil { + return nil, err + } + + // Prepare output variable for RETURNING clause + var lastInsertId int64 + // Append the output parameter for the RETURNING clause + args = append(args, &lastInsertId) + + // Link execution. + _, err = d.DoCommit(ctx, database.DoCommitInput{ + Link: link, + Sql: sql, + Args: args, + Stmt: nil, + Type: database.SqlTypeExecContext, + IsTransaction: link.IsTransaction(), + }) + + if err != nil { + return &Result{ + lastInsertId: 0, + rowsAffected: 0, + lastInsertIdError: err, + }, err + } + + // Get rows affected from the result + // For single insert with RETURNING clause, affected is always 1 + var affected int64 = 1 + + // Check if the primary key field type supports LastInsertId + if !strings.Contains(strings.ToLower(pkField.Type), "int") { + return &Result{ + lastInsertId: 0, + rowsAffected: affected, + lastInsertIdError: gerror.NewCodef( + gcode.CodeNotSupported, + "LastInsertId is not supported by primary key type: %s", + pkField.Type, + ), + }, nil + } + + return &Result{ + lastInsertId: lastInsertId, + rowsAffected: affected, + }, nil +} diff --git a/database/drivers/oracle/oracle_do_filter.go b/database/drivers/oracle/oracle_do_filter.go new file mode 100644 index 0000000..67909a7 --- /dev/null +++ b/database/drivers/oracle/oracle_do_filter.go @@ -0,0 +1,143 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package oracle + +import ( + "context" + "fmt" + "strconv" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +var ( + newSqlReplacementTmp = ` +SELECT * FROM ( + SELECT GFORM.*, ROWNUM ROW_NUMBER__ FROM (%s %s) GFORM WHERE ROWNUM <= %d +) WHERE ROW_NUMBER__ > %d +` +) + +func init() { + var err error + newSqlReplacementTmp, err = database.FormatMultiLineSqlToSingle(newSqlReplacementTmp) + if err != nil { + panic(err) + } +} + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *Driver) DoFilter(ctx context.Context, link database.Link, sql string, args []any) (newSql string, newArgs []any, err error) { + var index int + newArgs = args + // Convert placeholder char '?' to string ":vx". + newSql, err = gregex.ReplaceStringFunc("\\?", sql, func(s string) string { + index++ + return fmt.Sprintf(":v%d", index) + }) + if err != nil { + return + } + newSql, err = gregex.ReplaceString("\"", "", newSql) + if err != nil { + return + } + newSql, err = d.parseSql(newSql) + if err != nil { + return + } + return d.Core.DoFilter(ctx, link, newSql, newArgs) +} + +// parseSql does some replacement of the sql before commits it to underlying driver, +// for support of oracle server. +func (d *Driver) parseSql(toBeCommittedSql string) (string, error) { + var ( + err error + operation = gstr.StrTillEx(toBeCommittedSql, " ") + keyword = strings.ToUpper(gstr.Trim(operation)) + ) + switch keyword { + case "SELECT": + toBeCommittedSql, err = d.handleSelectSqlReplacement(toBeCommittedSql) + if err != nil { + return "", err + } + } + return toBeCommittedSql, nil +} + +func (d *Driver) handleSelectSqlReplacement(toBeCommittedSql string) (newSql string, err error) { + var ( + match [][]string + patten = `^\s*(?i)(SELECT)|(LIMIT\s*(\d+)\s*,{0,1}\s*(\d*))` + ) + match, err = gregex.MatchAllString(patten, toBeCommittedSql) + if err != nil { + return "", err + } + if len(match) == 0 { + return toBeCommittedSql, nil + } + var index = 1 + if len(match) < 2 || strings.HasPrefix(match[index][0], "LIMIT") == false { + return toBeCommittedSql, nil + } + // only handle `SELECT ... LIMIT ...` statement. + queryExpr, err := gregex.MatchString("((?i)SELECT)(.+)((?i)LIMIT)", toBeCommittedSql) + if err != nil { + return "", err + } + if len(queryExpr) == 0 { + return toBeCommittedSql, nil + } + if len(queryExpr) != 4 || + strings.EqualFold(queryExpr[1], "SELECT") == false || + strings.EqualFold(queryExpr[3], "LIMIT") == false { + return toBeCommittedSql, nil + } + page, limit := 0, 0 + for i := 1; i < len(match[index]); i++ { + if len(strings.TrimSpace(match[index][i])) == 0 { + continue + } + if strings.HasPrefix(match[index][i], "LIMIT") { + if match[index][i+2] != "" { + page, err = strconv.Atoi(match[index][i+1]) + if err != nil { + return "", err + } + limit, err = strconv.Atoi(match[index][i+2]) + if err != nil { + return "", err + } + if page <= 0 { + page = 1 + } + limit = (page/limit + 1) * limit + page, err = strconv.Atoi(match[index][i+1]) + if err != nil { + return "", err + } + } else { + limit, err = strconv.Atoi(match[index][i+1]) + if err != nil { + return "", err + } + } + break + } + } + var newReplacedSql = fmt.Sprintf( + newSqlReplacementTmp, + queryExpr[1], queryExpr[2], limit, page, + ) + return newReplacedSql, nil +} diff --git a/database/drivers/oracle/oracle_do_insert.go b/database/drivers/oracle/oracle_do_insert.go new file mode 100644 index 0000000..9f728c1 --- /dev/null +++ b/database/drivers/oracle/oracle_do_insert.go @@ -0,0 +1,278 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package oracle + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/container/gset" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gctx" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +const ( + internalPrimaryKeyInCtx gctx.StrKey = "primary_key_field" +) + +// DoInsert inserts or updates data for given table. +// The list parameter must contain at least one record, which was previously validated. +func (d *Driver) DoInsert( + ctx context.Context, link database.Link, table string, list database.List, option database.DoInsertOption, +) (result sql.Result, err error) { + switch option.InsertOption { + case database.InsertOptionSave: + return d.doSave(ctx, link, table, list, option) + + case database.InsertOptionReplace: + // Oracle does not support REPLACE INTO syntax, use SAVE instead. + return d.doSave(ctx, link, table, list, option) + + case database.InsertOptionIgnore: + // Oracle does not support INSERT IGNORE syntax, use MERGE instead. + return d.doInsertIgnore(ctx, link, table, list, option) + + case database.InsertOptionDefault: + // For default insert, set primary key field in context to support LastInsertId. + // Only set it when the primary key is not provided in the data, for performance reason. + tableFields, err := d.GetCore().GetDB().TableFields(ctx, table) + if err == nil && len(list) > 0 { + for _, field := range tableFields { + if strings.EqualFold(field.Key, "pri") { + // Check if primary key is provided in the data. + pkProvided := false + for key := range list[0] { + if strings.EqualFold(key, field.Name) { + pkProvided = true + break + } + } + // Only use RETURNING when primary key is not provided, for performance reason. + if !pkProvided { + pkField := *field + ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField) + } + break + } + } + } + + default: + } + var ( + keys []string + values []string + params []any + ) + // Retrieve the table fields and length. + var ( + listLength = len(list) + valueHolder = make([]string, 0) + ) + for k := range list[0] { + keys = append(keys, k) + valueHolder = append(valueHolder, "?") + } + var ( + batchResult = new(database.SqlResult) + charL, charR = d.GetChars() + keyStr = charL + strings.Join(keys, charL+","+charR) + charR + valueHolderStr = strings.Join(valueHolder, ",") + ) + // Format "INSERT...INTO..." statement. + // Note: Use standard INSERT INTO syntax instead of INSERT ALL to ensure triggers fire + for i := 0; i < listLength; i++ { + for _, k := range keys { + if s, ok := list[i][k].(database.Raw); ok { + params = append(params, gconv.String(s)) + } else { + params = append(params, list[i][k]) + } + } + values = append(values, valueHolderStr) + + // Execute individual INSERT for each record to trigger row-level triggers + r, err := d.DoExec(ctx, link, fmt.Sprintf( + "INSERT INTO %s(%s) VALUES(%s)", + table, keyStr, valueHolderStr, + ), params...) + if err != nil { + return r, err + } + if n, err := r.RowsAffected(); err != nil { + return r, err + } else { + batchResult.Result = r + batchResult.Affected += n + } + params = params[:0] + } + return batchResult, nil +} + +// doSave support upsert for Oracle +func (d *Driver) doSave(ctx context.Context, + link database.Link, table string, list database.List, option database.DoInsertOption, +) (result sql.Result, err error) { + return d.doMergeInsert(ctx, link, table, list, option, true) +} + +// doInsertIgnore implements INSERT IGNORE operation using MERGE statement for Oracle database. +// It only inserts records when there's no conflict on primary/unique keys. +func (d *Driver) doInsertIgnore(ctx context.Context, + link database.Link, table string, list database.List, option database.DoInsertOption, +) (result sql.Result, err error) { + return d.doMergeInsert(ctx, link, table, list, option, false) +} + +// doMergeInsert implements MERGE-based insert operations for Oracle database. +// When withUpdate is true, it performs upsert (insert or update). +// When withUpdate is false, it performs insert ignore (insert only when no conflict). +func (d *Driver) doMergeInsert( + ctx context.Context, + link database.Link, table string, list database.List, option database.DoInsertOption, withUpdate bool, +) (result sql.Result, err error) { + // If OnConflict is not specified, automatically get the primary key of the table + conflictKeys := option.OnConflict + if len(conflictKeys) == 0 { + primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table) + if err != nil { + return nil, gerror.WrapCode( + gcode.CodeInternalError, + err, + `failed to get primary keys for table`, + ) + } + foundPrimaryKey := false + for _, primaryKey := range primaryKeys { + for dataKey := range list[0] { + if strings.EqualFold(dataKey, primaryKey) { + foundPrimaryKey = true + break + } + } + if foundPrimaryKey { + break + } + } + if !foundPrimaryKey { + return nil, gerror.NewCodef( + gcode.CodeMissingParameter, + `Replace/Save/InsertIgnore operation requires conflict detection: `+ + `either specify OnConflict() columns or ensure table '%s' has a primary key in the data`, + table, + ) + } + // TODO consider composite primary keys. + conflictKeys = primaryKeys + } + + var ( + one = list[0] + oneLen = len(one) + charL, charR = d.GetChars() + conflictKeySet = gset.NewStrSet(false) + + // queryHolders: Handle data with Holder that need to be upsert + // queryValues: Handle data that need to be upsert + // insertKeys: Handle valid keys that need to be inserted + // insertValues: Handle values that need to be inserted + // updateValues: Handle values that need to be updated + queryHolders = make([]string, oneLen) + queryValues = make([]any, oneLen) + insertKeys = make([]string, oneLen) + insertValues = make([]string, oneLen) + updateValues []string + ) + + // conflictKeys slice type conv to set type + for _, conflictKey := range conflictKeys { + conflictKeySet.Add(gstr.ToUpper(conflictKey)) + } + + index := 0 + for key, value := range one { + keyWithChar := charL + key + charR + queryHolders[index] = fmt.Sprintf("? AS %s", keyWithChar) + queryValues[index] = value + insertKeys[index] = keyWithChar + insertValues[index] = fmt.Sprintf("T2.%s", keyWithChar) + + // Build updateValues only when withUpdate is true + // Filter conflict keys and soft created fields from updateValues + if withUpdate && !(conflictKeySet.Contains(key) || d.Core.IsSoftCreatedFieldName(key)) { + updateValues = append( + updateValues, + fmt.Sprintf(`T1.%s = T2.%s`, keyWithChar, keyWithChar), + ) + } + index++ + } + + var ( + batchResult = new(database.SqlResult) + sqlStr = parseSqlForMerge(table, queryHolders, insertKeys, insertValues, updateValues, conflictKeys) + ) + r, err := d.DoExec(ctx, link, sqlStr, queryValues...) + if err != nil { + return r, err + } + if n, err := r.RowsAffected(); err != nil { + return r, err + } else { + batchResult.Result = r + batchResult.Affected += n + } + return batchResult, nil +} + +// parseSqlForMerge generates MERGE statement for Oracle database. +// When updateValues is empty, it only inserts (INSERT IGNORE behavior). +// When updateValues is provided, it performs upsert (INSERT or UPDATE). +// Examples: +// - INSERT IGNORE: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) +// - UPSERT: MERGE INTO table T1 USING (...) T2 ON (...) WHEN NOT MATCHED THEN INSERT(...) VALUES (...) WHEN MATCHED THEN UPDATE SET ... +func parseSqlForMerge(table string, + queryHolders, insertKeys, insertValues, updateValues, duplicateKey []string, +) (sqlStr string) { + var ( + queryHolderStr = strings.Join(queryHolders, ",") + insertKeyStr = strings.Join(insertKeys, ",") + insertValueStr = strings.Join(insertValues, ",") + duplicateKeyStr string + ) + + // Build ON condition + for index, keys := range duplicateKey { + if index != 0 { + duplicateKeyStr += " AND " + } + duplicateKeyStr += fmt.Sprintf("T1.%s = T2.%s", keys, keys) + } + + // Build SQL based on whether UPDATE is needed + pattern := gstr.Trim( + `MERGE INTO %s T1 USING (SELECT %s FROM DUAL) T2 ON (%s) WHEN ` + + `NOT MATCHED THEN INSERT(%s) VALUES (%s)`, + ) + if len(updateValues) > 0 { + // Upsert: INSERT or UPDATE + pattern += gstr.Trim(` WHEN MATCHED THEN UPDATE SET %s`) + return fmt.Sprintf( + pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr, + strings.Join(updateValues, ","), + ) + } + // Insert Ignore: INSERT only + return fmt.Sprintf(pattern, table, queryHolderStr, duplicateKeyStr, insertKeyStr, insertValueStr) +} diff --git a/database/drivers/oracle/oracle_open.go b/database/drivers/oracle/oracle_open.go new file mode 100644 index 0000000..8c1c68e --- /dev/null +++ b/database/drivers/oracle/oracle_open.go @@ -0,0 +1,59 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package oracle + +import ( + "database/sql" + "strings" + + gora "github.com/sijms/go-ora/v2" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/util/gconv" +) + +// Open creates and returns an underlying sql.DB object for oracle. +func (d *Driver) Open(config *database.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "oracle" + ) + + options := map[string]string{ + "CONNECTION TIMEOUT": "60", + "PREFETCH_ROWS": "25", + } + + if config.Debug { + options["TRACE FILE"] = "oracle_trace.log" + } + // [username:[password]@]host[:port][/service_name][?param1=value1&...¶mN=valueN] + if config.Extra != "" { + // fix #3226 + list := strings.Split(config.Extra, "&") + for _, v := range list { + kv := strings.Split(v, "=") + if len(kv) == 2 { + options[kv[0]] = kv[1] + } + } + } + source = gora.BuildUrl( + config.Host, gconv.Int(config.Port), config.Name, config.User, config.Pass, options, + ) + + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} diff --git a/database/drivers/oracle/oracle_order.go b/database/drivers/oracle/oracle_order.go new file mode 100644 index 0000000..60d7157 --- /dev/null +++ b/database/drivers/oracle/oracle_order.go @@ -0,0 +1,12 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package oracle + +// OrderRandomFunction returns the SQL function for random ordering. +func (d *Driver) OrderRandomFunction() string { + return "DBMS_RANDOM.VALUE()" +} diff --git a/database/drivers/oracle/oracle_result.go b/database/drivers/oracle/oracle_result.go new file mode 100644 index 0000000..a479553 --- /dev/null +++ b/database/drivers/oracle/oracle_result.go @@ -0,0 +1,24 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package oracle + +// Result implements sql.Result interface for Oracle database. +type Result struct { + lastInsertId int64 + rowsAffected int64 + lastInsertIdError error +} + +// LastInsertId returns the last insert id. +func (r *Result) LastInsertId() (int64, error) { + return r.lastInsertId, r.lastInsertIdError +} + +// RowsAffected returns the rows affected. +func (r *Result) RowsAffected() (int64, error) { + return r.rowsAffected, nil +} diff --git a/database/drivers/oracle/oracle_table_fields.go b/database/drivers/oracle/oracle_table_fields.go new file mode 100644 index 0000000..de47c57 --- /dev/null +++ b/database/drivers/oracle/oracle_table_fields.go @@ -0,0 +1,84 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package oracle + +import ( + "context" + "fmt" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/util/gutil" +) + +var ( + tableFieldsSqlTmp = ` +SELECT + c.COLUMN_NAME AS FIELD, + CASE + WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)=0) THEN 'INT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')' + WHEN (c.DATA_TYPE='NUMBER' AND NVL(c.DATA_SCALE,0)>0) THEN 'FLOAT'||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')' + WHEN c.DATA_TYPE='FLOAT' THEN c.DATA_TYPE||'('||c.DATA_PRECISION||','||c.DATA_SCALE||')' + ELSE c.DATA_TYPE||'('||c.DATA_LENGTH||')' END AS TYPE, + c.NULLABLE, + CASE WHEN pk.COLUMN_NAME IS NOT NULL THEN 'PRI' ELSE '' END AS KEY +FROM USER_TAB_COLUMNS c +LEFT JOIN ( + SELECT cols.COLUMN_NAME + FROM USER_CONSTRAINTS cons + JOIN USER_CONS_COLUMNS cols ON cons.CONSTRAINT_NAME = cols.CONSTRAINT_NAME + WHERE cons.TABLE_NAME = '%s' AND cons.CONSTRAINT_TYPE = 'P' +) pk ON c.COLUMN_NAME = pk.COLUMN_NAME +WHERE c.TABLE_NAME = '%s' +ORDER BY c.COLUMN_ID +` +) + +func init() { + var err error + tableFieldsSqlTmp, err = database.FormatMultiLineSqlToSingle(tableFieldsSqlTmp) + if err != nil { + panic(err) + } +} + +// TableFields retrieves and returns the fields' information of specified table of current schema. +// +// Also see DriverMysql.TableFields. +func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*database.TableField, err error) { + var ( + result database.Result + link database.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + upperTable = strings.ToUpper(table) + structureSql = fmt.Sprintf(tableFieldsSqlTmp, upperTable, upperTable) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, structureSql) + if err != nil { + return nil, err + } + + fields = make(map[string]*database.TableField) + for i, m := range result { + isNull := false + if m["NULLABLE"].String() == "Y" { + isNull = true + } + + fields[m["FIELD"].String()] = &database.TableField{ + Index: i, + Name: m["FIELD"].String(), + Type: m["TYPE"].String(), + Null: isNull, + Key: m["KEY"].String(), + } + } + return fields, nil +} diff --git a/database/drivers/oracle/oracle_tables.go b/database/drivers/oracle/oracle_tables.go new file mode 100644 index 0000000..51b706c --- /dev/null +++ b/database/drivers/oracle/oracle_tables.go @@ -0,0 +1,39 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package oracle + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +const ( + tablesSqlTmp = `SELECT TABLE_NAME FROM USER_TABLES ORDER BY TABLE_NAME` +) + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +// Note that it ignores the parameter `schema` in oracle database, as it is not necessary. +func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result database.Result + // DO NOT use `usedSchema` as parameter for function `SlaveLink`. + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, tablesSqlTmp) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} diff --git a/database/drivers/pgsql/pgsql.go b/database/drivers/pgsql/pgsql.go index b847918..c157781 100644 --- a/database/drivers/pgsql/pgsql.go +++ b/database/drivers/pgsql/pgsql.go @@ -4,39 +4,39 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -// Package pgsql implements database.Driver for PostgreSQL database. +// Package pgsql implements database.Driver, which supports operations for database PostgreSQL. package pgsql import ( - "database/sql" - "fmt" - _ "github.com/lib/pq" "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/os/gctx" ) -// Driver is the driver for PostgreSQL database. +// Driver is the driver for postgresql database. type Driver struct { *database.Core } const ( - quoteChar = `"` + internalPrimaryKeyInCtx gctx.StrKey = "primary_key" + defaultSchema string = "public" + quoteChar string = `"` ) func init() { - if err := database.Register("pgsql", New()); err != nil { + if err := database.Register(`pgsql`, New()); err != nil { panic(err) } } -// New creates and returns a driver that implements database.Driver for PostgreSQL. +// New create and returns a driver that implements database.Driver, which supports operations for PostgreSql. func New() database.Driver { return &Driver{} } -// New creates and returns a database object for PostgreSQL. +// New creates and returns a database object for postgresql. // It implements the interface of database.Driver for extra database driver installation. func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.DB, error) { return &Driver{ @@ -44,28 +44,7 @@ func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.D }, nil } -// GetChars returns the security char for PostgreSQL database. +// GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// Open creates and returns an underlying sql.DB object for PostgreSQL. -func (d *Driver) Open(config *database.ConfigNode) (*sql.DB, error) { - var ( - source string - username = config.User - password = config.Pass - host = config.Host - port = config.Port - dbName = config.Name - ) - - source = fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", - host, port, username, password, dbName) - - if config.Extra != "" { - source += " " + config.Extra - } - - return sql.Open("postgres", source) -} diff --git a/database/drivers/pgsql/pgsql_convert.go b/database/drivers/pgsql/pgsql_convert.go new file mode 100644 index 0000000..12153ec --- /dev/null +++ b/database/drivers/pgsql/pgsql_convert.go @@ -0,0 +1,272 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +import ( + "context" + "reflect" + "strings" + + "github.com/google/uuid" + "github.com/lib/pq" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// ConvertValueForField converts value to database acceptable value. +func (d *Driver) ConvertValueForField(ctx context.Context, fieldType string, fieldValue any) (any, error) { + if fieldValue == nil { + return d.Core.ConvertValueForField(ctx, fieldType, fieldValue) + } + + var fieldValueKind = reflect.TypeOf(fieldValue).Kind() + + if fieldValueKind == reflect.Slice { + // For bytea type, pass []byte directly without any conversion. + if _, ok := fieldValue.([]byte); ok && gstr.Contains(fieldType, "bytea") { + return d.Core.ConvertValueForField(ctx, fieldType, fieldValue) + } + // For pgsql, json or jsonb require '[]' + if !gstr.Contains(fieldType, "json") { + fieldValue = gstr.ReplaceByMap(gconv.String(fieldValue), + map[string]string{ + "[": "{", + "]": "}", + }, + ) + } + } + return d.Core.ConvertValueForField(ctx, fieldType, fieldValue) +} + +// CheckLocalTypeForField checks and returns corresponding local golang type for given db type. +// The parameter `fieldType` is in lower case, like: +// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `_float4`, `_float8`, etc. +// +// PostgreSQL type mapping: +// +// | PostgreSQL Type | Local Go Type | +// |------------------------------|---------------| +// | int2, int4 | int | +// | int8 | int64 | +// | uuid | uuid.UUID | +// | _int2, _int4 | []int32 | // Note: pq package does not provide Int16Array; int32 is used for compatibility +// | _int8 | []int64 | +// | _float4 | []float32 | +// | _float8 | []float64 | +// | _bool | []bool | +// | _varchar, _text | []string | +// | _char, _bpchar | []string | +// | _numeric, _decimal, _money | []float64 | +// | bytea | []byte | +// | _bytea | [][]byte | +// | _uuid | []uuid.UUID | +func (d *Driver) CheckLocalTypeForField(ctx context.Context, fieldType string, fieldValue any) (database.LocalType, error) { + var typeName string + match, _ := gregex.MatchString(`(.+?)\((.+)\)`, fieldType) + if len(match) == 3 { + typeName = gstr.Trim(match[1]) + } else { + typeName = fieldType + } + typeName = strings.ToLower(typeName) + switch typeName { + case "int2", "int4": + return database.LocalTypeInt, nil + + case "int8": + return database.LocalTypeInt64, nil + + case "uuid": + return database.LocalTypeUUID, nil + + case "_int2", "_int4": + return database.LocalTypeInt32Slice, nil + + case "_int8": + return database.LocalTypeInt64Slice, nil + + case "_float4": + return database.LocalTypeFloat32Slice, nil + + case "_float8": + return database.LocalTypeFloat64Slice, nil + + case "_bool": + return database.LocalTypeBoolSlice, nil + + case "_varchar", "_text", "_char", "_bpchar": + return database.LocalTypeStringSlice, nil + + case "_uuid": + return database.LocalTypeUUIDSlice, nil + + case "_numeric", "_decimal", "_money": + return database.LocalTypeFloat64Slice, nil + + case "bytea": + return database.LocalTypeBytes, nil + + case "_bytea": + return database.LocalTypeBytesSlice, nil + + default: + return d.Core.CheckLocalTypeForField(ctx, fieldType, fieldValue) + } +} + +// ConvertValueForLocal converts value to local Golang type of value according field type name from database. +// The parameter `fieldType` is in lower case, like: +// `int2`, `int4`, `int8`, `_int2`, `_int4`, `_int8`, `uuid`, `_uuid`, etc. +// +// See: https://www.postgresql.org/docs/current/datatype.html +// +// PostgreSQL type mapping: +// +// | PostgreSQL Type | SQL Type | pq Type | Go Type | +// |-----------------|--------------------------------|-----------------|-------------| +// | int2 | int2, smallint | - | int | +// | int4 | int4, integer | - | int | +// | int8 | int8, bigint, bigserial | - | int64 | +// | uuid | uuid | - | uuid.UUID | +// | _int2 | int2[], smallint[] | pq.Int32Array | []int32 | +// | _int4 | int4[], integer[] | pq.Int32Array | []int32 | +// | _int8 | int8[], bigint[] | pq.Int64Array | []int64 | +// | _float4 | float4[], real[] | pq.Float32Array | []float32 | +// | _float8 | float8[], double precision[] | pq.Float64Array | []float64 | +// | _bool | boolean[], bool[] | pq.BoolArray | []bool | +// | _varchar | varchar[], character varying[] | pq.StringArray | []string | +// | _text | text[] | pq.StringArray | []string | +// | _char, _bpchar | char[], character[] | pq.StringArray | []string | +// | _numeric | numeric[] | pq.Float64Array | []float64 | +// | _decimal | decimal[] | pq.Float64Array | []float64 | +// | _money | money[] | pq.Float64Array | []float64 | +// | bytea | bytea | - | []byte | +// | _bytea | bytea[] | pq.ByteaArray | [][]byte | +// | _uuid | uuid[] | pq.StringArray | []uuid.UUID | +// +// Note: PostgreSQL also supports these array types but they are not yet mapped: +// - _date (date[]), _timestamp (timestamp[]), _timestamptz (timestamptz[]) +// - _jsonb (jsonb[]), _json (json[]) +func (d *Driver) ConvertValueForLocal(ctx context.Context, fieldType string, fieldValue any) (any, error) { + typeName, _ := gregex.ReplaceString(`\(.+\)`, "", fieldType) + typeName = strings.ToLower(typeName) + + // Basic types are mostly handled by Core layer; handle array types and special-case bytea here. + switch typeName { + + // []byte + case "bytea": + if v, ok := fieldValue.([]byte); ok { + return v, nil + } + return fieldValue, nil + + // []int32 + case "_int2", "_int4": + var result pq.Int32Array + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []int32(result), nil + + // []int64 + case "_int8": + var result pq.Int64Array + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []int64(result), nil + + // []float32 + case "_float4": + var result pq.Float32Array + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []float32(result), nil + + // []float64 + case "_float8": + var result pq.Float64Array + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []float64(result), nil + + // []bool + case "_bool": + var result pq.BoolArray + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []bool(result), nil + + // []string + case "_varchar", "_text", "_char", "_bpchar": + var result pq.StringArray + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []string(result), nil + + // uuid.UUID + case "uuid": + var uuidStr string + switch v := fieldValue.(type) { + case []byte: + uuidStr = string(v) + case string: + uuidStr = v + default: + uuidStr = gconv.String(fieldValue) + } + result, err := uuid.Parse(uuidStr) + if err != nil { + return nil, err + } + return result, nil + + // []uuid.UUID + case "_uuid": + var strArray pq.StringArray + if err := strArray.Scan(fieldValue); err != nil { + return nil, err + } + result := make([]uuid.UUID, len(strArray)) + for i, s := range strArray { + parsed, err := uuid.Parse(s) + if err != nil { + return nil, err + } + result[i] = parsed + } + return result, nil + + // []float64 + case "_numeric", "_decimal", "_money": + var result pq.Float64Array + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return []float64(result), nil + + // [][]byte + case "_bytea": + var result pq.ByteaArray + if err := result.Scan(fieldValue); err != nil { + return nil, err + } + return [][]byte(result), nil + + default: + return d.Core.ConvertValueForLocal(ctx, fieldType, fieldValue) + } +} diff --git a/database/drivers/pgsql/pgsql_do_exec.go b/database/drivers/pgsql/pgsql_do_exec.go new file mode 100644 index 0000000..8af03cc --- /dev/null +++ b/database/drivers/pgsql/pgsql_do_exec.go @@ -0,0 +1,110 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +// DoExec commits the sql string and its arguments to underlying driver +// through given link object and returns the execution result. +func (d *Driver) DoExec(ctx context.Context, link database.Link, sql string, args ...any) (result sql.Result, err error) { + var ( + isUseCoreDoExec bool = false // Check whether the default method needs to be used + primaryKey string = "" + pkField database.TableField + ) + + // Transaction checks. + if link == nil { + if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil { + // Firstly, check and retrieve transaction link from context. + link = tx + } else if link, err = d.MasterLink(); err != nil { + // Or else it creates one from master node. + return nil, err + } + } else if !link.IsTransaction() { + // If current link is not transaction link, it checks and retrieves transaction from context. + if tx := database.TXFromCtx(ctx, d.GetGroup()); tx != nil { + link = tx + } + } + + // Check if it is an insert operation with primary key. + if value := ctx.Value(internalPrimaryKeyInCtx); value != nil { + var ok bool + pkField, ok = value.(database.TableField) + if !ok { + isUseCoreDoExec = true + } + } else { + isUseCoreDoExec = true + } + + // check if it is an insert operation. + if !isUseCoreDoExec && pkField.Name != "" && strings.Contains(sql, "INSERT INTO") { + primaryKey = pkField.Name + sql += fmt.Sprintf(` RETURNING "%s"`, primaryKey) + } else { + // use default DoExec + return d.Core.DoExec(ctx, link, sql, args...) + } + + // Only the insert operation with primary key can execute the following code + + // Sql filtering. + sql, args = d.FormatSqlBeforeExecuting(sql, args) + sql, args, err = d.DoFilter(ctx, link, sql, args) + if err != nil { + return nil, err + } + + // Link execution. + var out database.DoCommitOutput + out, err = d.DoCommit(ctx, database.DoCommitInput{ + Link: link, + Sql: sql, + Args: args, + Stmt: nil, + Type: database.SqlTypeQueryContext, + IsTransaction: link.IsTransaction(), + }) + + if err != nil { + return nil, err + } + affected := len(out.Records) + if affected > 0 { + if !strings.Contains(pkField.Type, "int") { + return Result{ + affected: int64(affected), + lastInsertId: 0, + lastInsertIdError: gerror.NewCodef( + gcode.CodeNotSupported, + "LastInsertId is not supported by primary key type: %s", pkField.Type), + }, nil + } + + if out.Records[affected-1][primaryKey] != nil { + lastInsertId := out.Records[affected-1][primaryKey].Int64() + return Result{ + affected: int64(affected), + lastInsertId: lastInsertId, + }, nil + } + } + + return Result{}, nil +} diff --git a/database/drivers/pgsql/pgsql_do_filter.go b/database/drivers/pgsql/pgsql_do_filter.go new file mode 100644 index 0000000..ac35b01 --- /dev/null +++ b/database/drivers/pgsql/pgsql_do_filter.go @@ -0,0 +1,58 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +import ( + "context" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" +) + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *Driver) DoFilter( + ctx context.Context, link database.Link, sql string, args []any, +) (newSql string, newArgs []any, err error) { + var index int + // Convert placeholder char '?' to string "$x". + newSql, err = gregex.ReplaceStringFunc(`\?`, sql, func(s string) string { + index++ + return fmt.Sprintf(`$%d`, index) + }) + if err != nil { + return "", nil, err + } + // Handle pgsql jsonb feature support, which contains place-holder char '?'. + // Refer: + // https://github.com/gogf/gf/issues/1537 + // https://www.postgresql.org/docs/12/functions-json.html + newSql, err = gregex.ReplaceStringFuncMatch( + `(::jsonb([^\w\d]*)\$\d)`, + newSql, + func(match []string) string { + return fmt.Sprintf(`::jsonb%s?`, match[2]) + }, + ) + if err != nil { + return "", nil, err + } + newSql, err = gregex.ReplaceString(` LIMIT (\d+),\s*(\d+)`, ` LIMIT $2 OFFSET $1`, newSql) + if err != nil { + return "", nil, err + } + + // Add support for pgsql INSERT OR IGNORE. + if gstr.HasPrefix(newSql, database.InsertOperationIgnore) { + newSql = "INSERT" + newSql[len(database.InsertOperationIgnore):] + " ON CONFLICT DO NOTHING" + } + + newArgs = args + + return d.Core.DoFilter(ctx, link, newSql, newArgs) +} diff --git a/database/drivers/pgsql/pgsql_do_insert.go b/database/drivers/pgsql/pgsql_do_insert.go new file mode 100644 index 0000000..2a3019d --- /dev/null +++ b/database/drivers/pgsql/pgsql_do_insert.go @@ -0,0 +1,84 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +import ( + "context" + "database/sql" + "strings" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +// DoInsert inserts or updates data for given table. +// The list parameter must contain at least one record, which was previously validated. +func (d *Driver) DoInsert( + ctx context.Context, + link database.Link, table string, list database.List, option database.DoInsertOption, +) (result sql.Result, err error) { + switch option.InsertOption { + case + database.InsertOptionSave, + database.InsertOptionReplace: + // PostgreSQL does not support REPLACE INTO syntax, use Save (ON CONFLICT ... DO UPDATE) instead. + // Automatically detect primary keys if OnConflict is not specified. + if len(option.OnConflict) == 0 { + primaryKeys, err := d.Core.GetPrimaryKeys(ctx, table) + if err != nil { + return nil, gerror.WrapCode( + gcode.CodeInternalError, + err, + `failed to get primary keys for Save/Replace operation`, + ) + } + foundPrimaryKey := false + for _, primaryKey := range primaryKeys { + for dataKey := range list[0] { + if strings.EqualFold(dataKey, primaryKey) { + foundPrimaryKey = true + break + } + } + if foundPrimaryKey { + break + } + } + if !foundPrimaryKey { + return nil, gerror.NewCodef( + gcode.CodeMissingParameter, + `Replace/Save operation requires conflict detection: `+ + `either specify OnConflict() columns or ensure table '%s' has a primary key in the data`, + table, + ) + } + // TODO consider composite primary keys. + option.OnConflict = primaryKeys + } + // Treat Replace as Save operation + option.InsertOption = database.InsertOptionSave + + // pgsql support InsertIgnore natively, so no need to set primary key in context. + case database.InsertOptionIgnore, database.InsertOptionDefault: + // Get table fields to retrieve the primary key TableField object (not just the name) + // because DoExec needs the `TableField.Type` to determine if LastInsertId is supported. + tableFields, err := d.GetCore().GetDB().TableFields(ctx, table) + if err == nil { + for _, field := range tableFields { + if strings.EqualFold(field.Key, "pri") { + pkField := *field + ctx = context.WithValue(ctx, internalPrimaryKeyInCtx, pkField) + break + } + } + } + + default: + } + return d.Core.DoInsert(ctx, link, table, list, option) +} diff --git a/database/drivers/pgsql/pgsql_format_upsert.go b/database/drivers/pgsql/pgsql_format_upsert.go new file mode 100644 index 0000000..8f693d3 --- /dev/null +++ b/database/drivers/pgsql/pgsql_format_upsert.go @@ -0,0 +1,94 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +import ( + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// FormatUpsert returns SQL clause of type upsert for PgSQL. +// For example: ON CONFLICT (id) DO UPDATE SET ... +func (d *Driver) FormatUpsert(columns []string, list database.List, option database.DoInsertOption) (string, error) { + if len(option.OnConflict) == 0 { + return "", gerror.NewCode( + gcode.CodeMissingParameter, `Please specify conflict columns`, + ) + } + + var onDuplicateStr string + if option.OnDuplicateStr != "" { + onDuplicateStr = option.OnDuplicateStr + } else if len(option.OnDuplicateMap) > 0 { + for k, v := range option.OnDuplicateMap { + if len(onDuplicateStr) > 0 { + onDuplicateStr += "," + } + switch v.(type) { + case database.Raw, *database.Raw: + onDuplicateStr += fmt.Sprintf( + "%s=%s", + d.Core.QuoteWord(k), + v, + ) + case database.Counter, *database.Counter: + var counter database.Counter + switch value := v.(type) { + case database.Counter: + counter = value + case *database.Counter: + counter = *value + } + operator, columnVal := "+", counter.Value + if columnVal < 0 { + operator, columnVal = "-", -columnVal + } + // Note: In PostgreSQL ON CONFLICT DO UPDATE, we use EXCLUDED to reference + // the value that was proposed for insertion. This differs from MySQL's + // ON DUPLICATE KEY UPDATE behavior where the column name without prefix + // references the current row's value. + onDuplicateStr += fmt.Sprintf( + "%s=EXCLUDED.%s%s%s", + d.QuoteWord(k), + d.QuoteWord(counter.Field), + operator, + gconv.String(columnVal), + ) + default: + onDuplicateStr += fmt.Sprintf( + "%s=EXCLUDED.%s", + d.Core.QuoteWord(k), + d.Core.QuoteWord(gconv.String(v)), + ) + } + } + } else { + for _, column := range columns { + // If it's SAVE operation, do not automatically update the creating time. + if d.Core.IsSoftCreatedFieldName(column) { + continue + } + if len(onDuplicateStr) > 0 { + onDuplicateStr += "," + } + onDuplicateStr += fmt.Sprintf( + "%s=EXCLUDED.%s", + d.Core.QuoteWord(column), + d.Core.QuoteWord(column), + ) + } + } + + conflictKeys := gstr.Join(option.OnConflict, ",") + + return fmt.Sprintf("ON CONFLICT (%s) DO UPDATE SET ", conflictKeys) + onDuplicateStr, nil +} diff --git a/database/drivers/pgsql/pgsql_open.go b/database/drivers/pgsql/pgsql_open.go new file mode 100644 index 0000000..54e1ad2 --- /dev/null +++ b/database/drivers/pgsql/pgsql_open.go @@ -0,0 +1,69 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +import ( + "database/sql" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gstr" +) + +// Open creates and returns an underlying sql.DB object for pgsql. +// https://pkg.go.dev/github.com/lib/pq +func (d *Driver) Open(config *database.ConfigNode) (db *sql.DB, err error) { + source, err := configNodeToSource(config) + if err != nil { + return nil, err + } + underlyingDriverName := "postgres" + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} + +func configNodeToSource(config *database.ConfigNode) (string, error) { + var source string + source = fmt.Sprintf( + "user=%s password='%s' host=%s sslmode=disable", + config.User, config.Pass, config.Host, + ) + if config.Port != "" { + source = fmt.Sprintf("%s port=%s", source, config.Port) + } + if config.Name != "" { + source = fmt.Sprintf("%s dbname=%s", source, config.Name) + } + if config.Namespace != "" { + source = fmt.Sprintf("%s search_path=%s", source, config.Namespace) + } + if config.Timezone != "" { + source = fmt.Sprintf("%s timezone=%s", source, config.Timezone) + } + if config.Extra != "" { + extraMap, err := gstr.Parse(config.Extra) + if err != nil { + return "", gerror.WrapCodef( + gcode.CodeInvalidParameter, + err, + `invalid extra configuration: %s`, config.Extra, + ) + } + for k, v := range extraMap { + source += fmt.Sprintf(` %s=%s`, k, v) + } + } + return source, nil +} diff --git a/database/drivers/pgsql/pgsql_order.go b/database/drivers/pgsql/pgsql_order.go new file mode 100644 index 0000000..6fedce1 --- /dev/null +++ b/database/drivers/pgsql/pgsql_order.go @@ -0,0 +1,12 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +// OrderRandomFunction returns the SQL function for random ordering. +func (d *Driver) OrderRandomFunction() string { + return "RANDOM()" +} diff --git a/database/drivers/pgsql/pgsql_result.go b/database/drivers/pgsql/pgsql_result.go new file mode 100644 index 0000000..2874078 --- /dev/null +++ b/database/drivers/pgsql/pgsql_result.go @@ -0,0 +1,24 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +import "database/sql" + +type Result struct { + sql.Result + affected int64 + lastInsertId int64 + lastInsertIdError error +} + +func (pgr Result) RowsAffected() (int64, error) { + return pgr.affected, nil +} + +func (pgr Result) LastInsertId() (int64, error) { + return pgr.lastInsertId, pgr.lastInsertIdError +} diff --git a/database/drivers/pgsql/pgsql_table_fields.go b/database/drivers/pgsql/pgsql_table_fields.go new file mode 100644 index 0000000..7c26570 --- /dev/null +++ b/database/drivers/pgsql/pgsql_table_fields.go @@ -0,0 +1,107 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +import ( + "context" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/util/gutil" +) + +var ( + tableFieldsSqlTmp = ` +SELECT + a.attname AS field, + t.typname AS type, + a.attnotnull AS null, + (CASE WHEN d.contype = 'p' THEN 'pri' WHEN d.contype = 'u' THEN 'uni' ELSE '' END) AS key, + ic.column_default AS default_value, + b.description AS comment, + COALESCE(character_maximum_length, numeric_precision, -1) AS length, + numeric_scale AS scale +FROM pg_attribute a + LEFT JOIN pg_class c ON a.attrelid = c.oid + LEFT JOIN pg_constraint d ON d.conrelid = c.oid AND a.attnum = d.conkey[1] + LEFT JOIN pg_description b ON a.attrelid = b.objoid AND a.attnum = b.objsubid + LEFT JOIN pg_type t ON a.atttypid = t.oid + LEFT JOIN information_schema.columns ic ON ic.column_name = a.attname AND ic.table_name = c.relname +WHERE c.oid = '%s'::regclass + AND a.attisdropped IS FALSE + AND a.attnum > 0 +ORDER BY a.attnum` +) + +func init() { + var err error + tableFieldsSqlTmp, err = database.FormatMultiLineSqlToSingle(tableFieldsSqlTmp) + if err != nil { + panic(err) + } +} + +// TableFields retrieves and returns the fields' information of specified table of current schema. +func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*database.TableField, err error) { + var ( + result database.Result + link database.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + // TODO duplicated `id` result? + structureSql = fmt.Sprintf(tableFieldsSqlTmp, table) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, structureSql) + if err != nil { + return nil, err + } + fields = make(map[string]*database.TableField) + var ( + index = 0 + name string + ok bool + existingField *database.TableField + ) + for _, m := range result { + name = m["field"].String() + // Merge duplicated fields, especially for key constraints. + // Priority: pri > uni > others + if existingField, ok = fields[name]; ok { + currentKey := m["key"].String() + // Merge key information with priority: pri > uni + if currentKey == "pri" || (currentKey == "uni" && existingField.Key != "pri") { + existingField.Key = currentKey + } + continue + } + + var ( + fieldType string + dataType = m["type"].String() + dataLength = m["length"].Int() + ) + if dataLength > 0 { + fieldType = fmt.Sprintf("%s(%d)", dataType, dataLength) + } else { + fieldType = dataType + } + + fields[name] = &database.TableField{ + Index: index, + Name: name, + Type: fieldType, + Null: !m["null"].Bool(), + Key: m["key"].String(), + Default: m["default_value"].Val(), + Comment: m["comment"].String(), + } + index++ + } + return fields, nil +} diff --git a/database/drivers/pgsql/pgsql_tables.go b/database/drivers/pgsql/pgsql_tables.go new file mode 100644 index 0000000..b845168 --- /dev/null +++ b/database/drivers/pgsql/pgsql_tables.go @@ -0,0 +1,102 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package pgsql + +import ( + "context" + "fmt" + "regexp" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/text/gregex" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gutil" +) + +var ( + tablesSqlTmp = ` +SELECT + c.relname +FROM + pg_class c +INNER JOIN pg_namespace n ON + c.relnamespace = n.oid +WHERE + n.nspname = '%s' + AND c.relkind IN ('r', 'p') + %s +ORDER BY + c.relname +` + + versionRegex = regexp.MustCompile(`PostgreSQL (\d+\.\d+)`) +) + +func init() { + var err error + tablesSqlTmp, err = database.FormatMultiLineSqlToSingle(tablesSqlTmp) + if err != nil { + panic(err) + } +} + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var ( + result database.Result + usedSchema = gutil.GetOrDefaultStr(d.GetConfig().Namespace, schema...) + ) + if usedSchema == "" { + usedSchema = defaultSchema + } + // DO NOT use `usedSchema` as parameter for function `SlaveLink`. + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + + useRelpartbound := "" + if gstr.CompareVersion(d.version(ctx, link), "10") >= 0 { + useRelpartbound = "AND c.relpartbound IS NULL" + } + + var query = fmt.Sprintf( + tablesSqlTmp, + usedSchema, + useRelpartbound, + ) + + query, _ = gregex.ReplaceString(`[\n\r\s]+`, " ", gstr.Trim(query)) + result, err = d.DoSelect(ctx, link, query) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} + +// version checks and returns the database version. +func (d *Driver) version(ctx context.Context, link database.Link) string { + result, err := d.DoSelect(ctx, link, "SELECT version();") + if err != nil { + return "" + } + if len(result) > 0 { + if v, ok := result[0]["version"]; ok { + matches := versionRegex.FindStringSubmatch(v.String()) + if len(matches) >= 2 { + return matches[1] + } + } + } + return "" +} diff --git a/database/drivers/pgsql/testdata/issues/issue3632.sql b/database/drivers/pgsql/testdata/issues/issue3632.sql new file mode 100644 index 0000000..9489794 --- /dev/null +++ b/database/drivers/pgsql/testdata/issues/issue3632.sql @@ -0,0 +1,4 @@ +CREATE TABLE "public"."%s" ( + "one" int8[] NOT NULL, + "two" text[][] NOT NULL +); diff --git a/database/drivers/pgsql/testdata/issues/issue3668.sql b/database/drivers/pgsql/testdata/issues/issue3668.sql new file mode 100644 index 0000000..968231a --- /dev/null +++ b/database/drivers/pgsql/testdata/issues/issue3668.sql @@ -0,0 +1,4 @@ +CREATE TABLE "public"."%s" ( + "text" varchar(255) COLLATE "pg_catalog"."default", + "number" int4 +); diff --git a/database/drivers/pgsql/testdata/issues/issue3671.sql b/database/drivers/pgsql/testdata/issues/issue3671.sql new file mode 100644 index 0000000..57d78d7 --- /dev/null +++ b/database/drivers/pgsql/testdata/issues/issue3671.sql @@ -0,0 +1,8 @@ +CREATE TABLE "public"."%s" +( + "one" int8[] NOT NULL, + "two" text[][] NOT NULL, + "three" jsonb, + "four" json, + "five" jsonb +); diff --git a/database/drivers/pgsql/testdata/issues/issue4033.sql b/database/drivers/pgsql/testdata/issues/issue4033.sql new file mode 100644 index 0000000..57f9725 --- /dev/null +++ b/database/drivers/pgsql/testdata/issues/issue4033.sql @@ -0,0 +1,5 @@ +CREATE TABLE test_enum ( + id int8 NOT NULL, + status int2 DEFAULT 0 NOT NULL, + CONSTRAINT test_enum_pk PRIMARY KEY (id) +); \ No newline at end of file diff --git a/database/drivers/pgsql/testdata/table_with_prefix.sql b/database/drivers/pgsql/testdata/table_with_prefix.sql new file mode 100644 index 0000000..f7dfe21 --- /dev/null +++ b/database/drivers/pgsql/testdata/table_with_prefix.sql @@ -0,0 +1,6 @@ +DROP TABLE IF EXISTS instance; +CREATE TABLE instance ( + f_id SERIAL NOT NULL PRIMARY KEY, + name varchar(255) DEFAULT '' +); +INSERT INTO instance VALUES (1, 'john'); diff --git a/database/drivers/pgsql/testdata/with_multiple_depends.sql b/database/drivers/pgsql/testdata/with_multiple_depends.sql new file mode 100644 index 0000000..15d0af8 --- /dev/null +++ b/database/drivers/pgsql/testdata/with_multiple_depends.sql @@ -0,0 +1,30 @@ + +CREATE TABLE table_a ( + id SERIAL PRIMARY KEY, + alias varchar(255) DEFAULT '' +); + +INSERT INTO table_a VALUES (1, 'table_a_test1'); +INSERT INTO table_a VALUES (2, 'table_a_test2'); + +CREATE TABLE table_b ( + id SERIAL PRIMARY KEY, + table_a_id integer NOT NULL, + alias varchar(255) DEFAULT '' +); + +INSERT INTO table_b VALUES (10, 1, 'table_b_test1'); +INSERT INTO table_b VALUES (20, 2, 'table_b_test2'); +INSERT INTO table_b VALUES (30, 1, 'table_b_test3'); +INSERT INTO table_b VALUES (40, 2, 'table_b_test4'); + +CREATE TABLE table_c ( + id SERIAL PRIMARY KEY, + table_b_id integer NOT NULL, + alias varchar(255) DEFAULT '' +); + +INSERT INTO table_c VALUES (100, 10, 'table_c_test1'); +INSERT INTO table_c VALUES (200, 10, 'table_c_test2'); +INSERT INTO table_c VALUES (300, 20, 'table_c_test3'); +INSERT INTO table_c VALUES (400, 30, 'table_c_test4'); diff --git a/database/drivers/pgsql/testdata/with_tpl_user.sql b/database/drivers/pgsql/testdata/with_tpl_user.sql new file mode 100644 index 0000000..8cf3c93 --- /dev/null +++ b/database/drivers/pgsql/testdata/with_tpl_user.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS %s ( + id SERIAL PRIMARY KEY, + name varchar(45) NOT NULL +); diff --git a/database/drivers/pgsql/testdata/with_tpl_user_detail.sql b/database/drivers/pgsql/testdata/with_tpl_user_detail.sql new file mode 100644 index 0000000..b14e785 --- /dev/null +++ b/database/drivers/pgsql/testdata/with_tpl_user_detail.sql @@ -0,0 +1,4 @@ +CREATE TABLE IF NOT EXISTS %s ( + uid SERIAL PRIMARY KEY, + address varchar(45) NOT NULL +); diff --git a/database/drivers/pgsql/testdata/with_tpl_user_scores.sql b/database/drivers/pgsql/testdata/with_tpl_user_scores.sql new file mode 100644 index 0000000..4584f79 --- /dev/null +++ b/database/drivers/pgsql/testdata/with_tpl_user_scores.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS %s ( + id SERIAL PRIMARY KEY, + uid integer NOT NULL, + score integer NOT NULL +); diff --git a/database/drivers/sqlite/sqlite.go b/database/drivers/sqlite/sqlite.go index 9479b30..d1e05da 100644 --- a/database/drivers/sqlite/sqlite.go +++ b/database/drivers/sqlite/sqlite.go @@ -4,18 +4,16 @@ // If a copy of the MIT was not distributed with this file, // You can obtain one at https://github.com/gogf/gf. -// Package sqlite implements database.Driver for SQLite database. +// Package sqlite implements database.Driver, which supports operations for database SQLite. package sqlite import ( - "database/sql" - - _ "modernc.org/sqlite" + _ "github.com/glebarez/go-sqlite" "git.magicany.cc/black1552/gin-base/database" ) -// Driver is the driver for SQLite database. +// Driver is the driver for sqlite database. type Driver struct { *database.Core } @@ -25,34 +23,25 @@ const ( ) func init() { - if err := database.Register("sqlite", New()); err != nil { + if err := database.Register(`sqlite`, New()); err != nil { panic(err) } } -// New creates and returns a driver that implements database.Driver for SQLite. +// New create and returns a driver that implements database.Driver, which supports operations for SQLite. func New() database.Driver { return &Driver{} } -// New creates and returns a database object for SQLite. +// New creates and returns a database object for sqlite. +// It implements the interface of database.Driver for extra database driver installation. func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.DB, error) { return &Driver{ Core: core, }, nil } -// GetChars returns the security char for SQLite. +// GetChars returns the security char for this type of database. func (d *Driver) GetChars() (charLeft string, charRight string) { return quoteChar, quoteChar } - -// Open creates and returns an underlying sql.DB object for SQLite. -func (d *Driver) Open(config *database.ConfigNode) (*sql.DB, error) { - // For SQLite, use the Name field as the database file path - dbName := config.Name - if dbName == "" { - dbName = ":memory:" - } - return sql.Open("sqlite", dbName) -} diff --git a/database/drivers/sqlite/sqlite_do_filter.go b/database/drivers/sqlite/sqlite_do_filter.go new file mode 100644 index 0000000..906f6be --- /dev/null +++ b/database/drivers/sqlite/sqlite_do_filter.go @@ -0,0 +1,29 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/text/gstr" +) + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *Driver) DoFilter( + ctx context.Context, link database.Link, sql string, args []any, +) (newSql string, newArgs []any, err error) { + // Special insert/ignore operation for sqlite. + switch { + case gstr.HasPrefix(sql, database.InsertOperationIgnore): + sql = "INSERT OR IGNORE" + sql[len(database.InsertOperationIgnore):] + + case gstr.HasPrefix(sql, database.InsertOperationReplace): + sql = "INSERT OR REPLACE" + sql[len(database.InsertOperationReplace):] + } + return d.Core.DoFilter(ctx, link, sql, args) +} diff --git a/database/drivers/sqlite/sqlite_format_upsert.go b/database/drivers/sqlite/sqlite_format_upsert.go new file mode 100644 index 0000000..c3822f8 --- /dev/null +++ b/database/drivers/sqlite/sqlite_format_upsert.go @@ -0,0 +1,90 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +import ( + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// FormatUpsert returns SQL clause of type upsert for SQLite. +// For example: ON CONFLICT (id) DO UPDATE SET ... +func (d *Driver) FormatUpsert(columns []string, list database.List, option database.DoInsertOption) (string, error) { + if len(option.OnConflict) == 0 { + return "", gerror.NewCode( + gcode.CodeMissingParameter, `Please specify conflict columns`, + ) + } + + var onDuplicateStr string + if option.OnDuplicateStr != "" { + onDuplicateStr = option.OnDuplicateStr + } else if len(option.OnDuplicateMap) > 0 { + for k, v := range option.OnDuplicateMap { + if len(onDuplicateStr) > 0 { + onDuplicateStr += "," + } + switch v.(type) { + case database.Raw, *database.Raw: + onDuplicateStr += fmt.Sprintf( + "%s=%s", + d.Core.QuoteWord(k), + v, + ) + case database.Counter, *database.Counter: + var counter database.Counter + switch value := v.(type) { + case database.Counter: + counter = value + case *database.Counter: + counter = *value + } + operator, columnVal := "+", counter.Value + if columnVal < 0 { + operator, columnVal = "-", -columnVal + } + onDuplicateStr += fmt.Sprintf( + "%s=EXCLUDED.%s%s%s", + d.QuoteWord(k), + d.QuoteWord(counter.Field), + operator, + gconv.String(columnVal), + ) + default: + onDuplicateStr += fmt.Sprintf( + "%s=EXCLUDED.%s", + d.Core.QuoteWord(k), + d.Core.QuoteWord(gconv.String(v)), + ) + } + } + } else { + for _, column := range columns { + // If it's SAVE operation, do not automatically update the creating time. + if d.Core.IsSoftCreatedFieldName(column) { + continue + } + if len(onDuplicateStr) > 0 { + onDuplicateStr += "," + } + onDuplicateStr += fmt.Sprintf( + "%s=EXCLUDED.%s", + d.Core.QuoteWord(column), + d.Core.QuoteWord(column), + ) + } + } + + conflictKeys := gstr.Join(option.OnConflict, ",") + + return fmt.Sprintf("ON CONFLICT (%s) DO UPDATE SET ", conflictKeys) + onDuplicateStr, nil +} diff --git a/database/drivers/sqlite/sqlite_open.go b/database/drivers/sqlite/sqlite_open.go new file mode 100644 index 0000000..1ae1f71 --- /dev/null +++ b/database/drivers/sqlite/sqlite_open.go @@ -0,0 +1,63 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +import ( + "database/sql" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/encoding/gurl" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// Open creates and returns an underlying sql.DB object for sqlite. +// https://github.com/glebarez/go-sqlite +func (d *Driver) Open(config *database.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "sqlite" + ) + source = config.Name + // It searches the source file to locate its absolute path.. + if absolutePath, _ := gfile.Search(source); absolutePath != "" { + source = absolutePath + } + // Multiple PRAGMAs can be specified, e.g.: + // path/to/some.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL) + if config.Extra != "" { + var ( + options string + extraMap map[string]any + ) + if extraMap, err = gstr.Parse(config.Extra); err != nil { + return nil, err + } + for k, v := range extraMap { + if options != "" { + options += "&" + } + options += fmt.Sprintf(`_pragma=%s(%s)`, k, gurl.Encode(gconv.String(v))) + } + if len(options) > 1 { + source += "?" + options + } + } + + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} diff --git a/database/drivers/sqlite/sqlite_order.go b/database/drivers/sqlite/sqlite_order.go new file mode 100644 index 0000000..075e67e --- /dev/null +++ b/database/drivers/sqlite/sqlite_order.go @@ -0,0 +1,12 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +// OrderRandomFunction returns the SQL function for random ordering. +func (d *Driver) OrderRandomFunction() string { + return "RANDOM()" +} diff --git a/database/drivers/sqlite/sqlite_table_fields.go b/database/drivers/sqlite/sqlite_table_fields.go new file mode 100644 index 0000000..f7deb6c --- /dev/null +++ b/database/drivers/sqlite/sqlite_table_fields.go @@ -0,0 +1,49 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +import ( + "context" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/util/gutil" +) + +// TableFields retrieves and returns the fields' information of specified table of current schema. +// +// Also see DriverMysql.TableFields. +func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*database.TableField, err error) { + var ( + result database.Result + link database.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, d.QuoteWord(table))) + if err != nil { + return nil, err + } + fields = make(map[string]*database.TableField) + for i, m := range result { + mKey := "" + if m["pk"].Bool() { + mKey = "pri" + } + fields[m["name"].String()] = &database.TableField{ + Index: i, + Name: m["name"].String(), + Type: m["type"].String(), + Key: mKey, + Default: m["dflt_value"].Val(), + Null: !m["notnull"].Bool(), + } + } + return fields, nil +} diff --git a/database/drivers/sqlite/sqlite_tables.go b/database/drivers/sqlite/sqlite_tables.go new file mode 100644 index 0000000..486b7e5 --- /dev/null +++ b/database/drivers/sqlite/sqlite_tables.go @@ -0,0 +1,38 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlite + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +const ( + tablesSqlTmp = `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME` +) + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result database.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + + result, err = d.DoSelect(ctx, link, tablesSqlTmp) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} diff --git a/database/drivers/sqlitecgo/sqlitecgo.go b/database/drivers/sqlitecgo/sqlitecgo.go new file mode 100644 index 0000000..39d456f --- /dev/null +++ b/database/drivers/sqlitecgo/sqlitecgo.go @@ -0,0 +1,52 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +// Package sqlitecgo implements database.Driver, which supports operations for database SQLite. +// +// Note: +// 1. Using sqlitecgo is for building a 32-bit Windows operating system +// 2. You need to set the environment variable CGO_ENABLED=1 and make sure that GCC is installed +// on your path. windows gcc: https://jmeubank.github.io/tdm-gcc/ +package sqlitecgo + +import ( + _ "github.com/mattn/go-sqlite3" + + "git.magicany.cc/black1552/gin-base/database" +) + +// Driver is the driver for sqlite database. +type Driver struct { + *database.Core +} + +const ( + quoteChar = "`" +) + +func init() { + if err := database.Register(`sqlite`, New()); err != nil { + panic(err) + } +} + +// New create and returns a driver that implements database.Driver, which supports operations for SQLite. +func New() database.Driver { + return &Driver{} +} + +// New creates and returns a database object for sqlite. +// It implements the interface of database.Driver for extra database driver installation. +func (d *Driver) New(core *database.Core, node *database.ConfigNode) (database.DB, error) { + return &Driver{ + Core: core, + }, nil +} + +// GetChars returns the security char for this type of database. +func (d *Driver) GetChars() (charLeft string, charRight string) { + return quoteChar, quoteChar +} diff --git a/database/drivers/sqlitecgo/sqlitecgo_do_filter.go b/database/drivers/sqlitecgo/sqlitecgo_do_filter.go new file mode 100644 index 0000000..7920ca6 --- /dev/null +++ b/database/drivers/sqlitecgo/sqlitecgo_do_filter.go @@ -0,0 +1,29 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlitecgo + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/text/gstr" +) + +// DoFilter deals with the sql string before commits it to underlying sql driver. +func (d *Driver) DoFilter( + ctx context.Context, link database.Link, sql string, args []any, +) (newSql string, newArgs []any, err error) { + // Special insert/ignore operation for sqlite. + switch { + case gstr.HasPrefix(sql, database.InsertOperationIgnore): + sql = "INSERT OR IGNORE" + sql[len(database.InsertOperationIgnore):] + + case gstr.HasPrefix(sql, database.InsertOperationReplace): + sql = "INSERT OR REPLACE" + sql[len(database.InsertOperationReplace):] + } + return d.Core.DoFilter(ctx, link, sql, args) +} diff --git a/database/drivers/sqlitecgo/sqlitecgo_format_upsert.go b/database/drivers/sqlitecgo/sqlitecgo_format_upsert.go new file mode 100644 index 0000000..72c5b4e --- /dev/null +++ b/database/drivers/sqlitecgo/sqlitecgo_format_upsert.go @@ -0,0 +1,90 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlitecgo + +import ( + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// FormatUpsert returns SQL clause of type upsert for SQLite. +// For example: ON CONFLICT (id) DO UPDATE SET ... +func (d *Driver) FormatUpsert(columns []string, list database.List, option database.DoInsertOption) (string, error) { + if len(option.OnConflict) == 0 { + return "", gerror.NewCode( + gcode.CodeMissingParameter, `Please specify conflict columns`, + ) + } + + var onDuplicateStr string + if option.OnDuplicateStr != "" { + onDuplicateStr = option.OnDuplicateStr + } else if len(option.OnDuplicateMap) > 0 { + for k, v := range option.OnDuplicateMap { + if len(onDuplicateStr) > 0 { + onDuplicateStr += "," + } + switch v.(type) { + case database.Raw, *database.Raw: + onDuplicateStr += fmt.Sprintf( + "%s=%s", + d.Core.QuoteWord(k), + v, + ) + case database.Counter, *database.Counter: + var counter database.Counter + switch value := v.(type) { + case database.Counter: + counter = value + case *database.Counter: + counter = *value + } + operator, columnVal := "+", counter.Value + if columnVal < 0 { + operator, columnVal = "-", -columnVal + } + onDuplicateStr += fmt.Sprintf( + "%s=EXCLUDED.%s%s%s", + d.QuoteWord(k), + d.QuoteWord(counter.Field), + operator, + gconv.String(columnVal), + ) + default: + onDuplicateStr += fmt.Sprintf( + "%s=EXCLUDED.%s", + d.Core.QuoteWord(k), + d.Core.QuoteWord(gconv.String(v)), + ) + } + } + } else { + for _, column := range columns { + // If it's SAVE operation, do not automatically update the creating time. + if d.Core.IsSoftCreatedFieldName(column) { + continue + } + if len(onDuplicateStr) > 0 { + onDuplicateStr += "," + } + onDuplicateStr += fmt.Sprintf( + "%s=EXCLUDED.%s", + d.Core.QuoteWord(column), + d.Core.QuoteWord(column), + ) + } + } + + conflictKeys := gstr.Join(option.OnConflict, ",") + + return fmt.Sprintf("ON CONFLICT (%s) DO UPDATE SET ", conflictKeys) + onDuplicateStr, nil +} diff --git a/database/drivers/sqlitecgo/sqlitecgo_open.go b/database/drivers/sqlitecgo/sqlitecgo_open.go new file mode 100644 index 0000000..0e81c0b --- /dev/null +++ b/database/drivers/sqlitecgo/sqlitecgo_open.go @@ -0,0 +1,72 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +// Package sqlitecgo implements database.Driver, which supports operations for database SQLite. +// +// Note: +// 1. Using sqlitecgo is for building a 32-bit Windows operating system +// 2. You need to set the environment variable CGO_ENABLED=1 and make sure that GCC is installed +// on your path. windows gcc: https://jmeubank.github.io/tdm-gcc/ +package sqlitecgo + +import ( + "database/sql" + "fmt" + + _ "github.com/mattn/go-sqlite3" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/encoding/gurl" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/os/gfile" + "github.com/gogf/gf/v2/text/gstr" + "github.com/gogf/gf/v2/util/gconv" +) + +// Open creates and returns an underlying sql.DB object for sqlite. +// https://github.com/mattn/go-sglite3 +func (d *Driver) Open(config *database.ConfigNode) (db *sql.DB, err error) { + var ( + source string + underlyingDriverName = "sqlite3" + ) + source = config.Name + // It searches the source file to locate its absolute path.. + if absolutePath, _ := gfile.Search(source); absolutePath != "" { + source = absolutePath + } + + // Multiple PRAGMAs can be specified, e.g.: + // path/to/some.db?_pragma=busy_timeout(5000)&_pragma=journal_mode(WAL) + if config.Extra != "" { + var ( + options string + extraMap map[string]any + ) + if extraMap, err = gstr.Parse(config.Extra); err != nil { + return nil, err + } + for k, v := range extraMap { + if options != "" { + options += "&" + } + options += fmt.Sprintf(`_pragma=%s(%s)`, k, gurl.Encode(gconv.String(v))) + } + if len(options) > 1 { + source += "?" + options + } + } + + if db, err = sql.Open(underlyingDriverName, source); err != nil { + err = gerror.WrapCodef( + gcode.CodeDbOperationError, err, + `sql.Open failed for driver "%s" by source "%s"`, underlyingDriverName, source, + ) + return nil, err + } + return +} diff --git a/database/drivers/sqlitecgo/sqlitecgo_table_fields.go b/database/drivers/sqlitecgo/sqlitecgo_table_fields.go new file mode 100644 index 0000000..4f889ed --- /dev/null +++ b/database/drivers/sqlitecgo/sqlitecgo_table_fields.go @@ -0,0 +1,55 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +// Package sqlitecgo implements database.Driver, which supports operations for database SQLite. +// +// Note: +// 1. Using sqlitecgo is for building a 32-bit Windows operating system +// 2. You need to set the environment variable CGO_ENABLED=1 and make sure that GCC is installed +// on your path. windows gcc: https://jmeubank.github.io/tdm-gcc/ +package sqlitecgo + +import ( + "context" + "fmt" + + "git.magicany.cc/black1552/gin-base/database" + "github.com/gogf/gf/v2/util/gutil" +) + +// TableFields retrieves and returns the fields' information of specified table of current schema. +// +// Also see DriverMysql.TableFields. +func (d *Driver) TableFields(ctx context.Context, table string, schema ...string) (fields map[string]*database.TableField, err error) { + var ( + result database.Result + link database.Link + usedSchema = gutil.GetOrDefaultStr(d.GetSchema(), schema...) + ) + if link, err = d.SlaveLink(usedSchema); err != nil { + return nil, err + } + result, err = d.DoSelect(ctx, link, fmt.Sprintf(`PRAGMA TABLE_INFO(%s)`, d.QuoteWord(table))) + if err != nil { + return nil, err + } + fields = make(map[string]*database.TableField) + for i, m := range result { + mKey := "" + if m["pk"].Bool() { + mKey = "pri" + } + fields[m["name"].String()] = &database.TableField{ + Index: i, + Name: m["name"].String(), + Type: m["type"].String(), + Key: mKey, + Default: m["dflt_value"].Val(), + Null: !m["notnull"].Bool(), + } + } + return fields, nil +} diff --git a/database/drivers/sqlitecgo/sqlitecgo_tables.go b/database/drivers/sqlitecgo/sqlitecgo_tables.go new file mode 100644 index 0000000..25b9f21 --- /dev/null +++ b/database/drivers/sqlitecgo/sqlitecgo_tables.go @@ -0,0 +1,38 @@ +// Copyright GoFrame Author(https://goframe.org). All Rights Reserved. +// +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, +// You can obtain one at https://github.com/gogf/gf. + +package sqlitecgo + +import ( + "context" + + "git.magicany.cc/black1552/gin-base/database" +) + +const ( + tablesSqlTmp = `SELECT NAME FROM SQLITE_MASTER WHERE TYPE='table' ORDER BY NAME` +) + +// Tables retrieves and returns the tables of current schema. +// It's mainly used in cli tool chain for automatically generating the models. +func (d *Driver) Tables(ctx context.Context, schema ...string) (tables []string, err error) { + var result database.Result + link, err := d.SlaveLink(schema...) + if err != nil { + return nil, err + } + + result, err = d.DoSelect(ctx, link, tablesSqlTmp) + if err != nil { + return + } + for _, m := range result { + for _, v := range m { + tables = append(tables, v.String()) + } + } + return +} diff --git a/database/gdb.go b/database/gdb.go index 0d0a568..534b28b 100644 --- a/database/gdb.go +++ b/database/gdb.go @@ -687,6 +687,7 @@ type ( Result []Record // Map is alias of map[string]any, which is the most common usage map type. + Map = map[string]any // List is type of map array. diff --git a/go.sum b/go.sum index 7296957..df69159 100644 --- a/go.sum +++ b/go.sum @@ -6,14 +6,21 @@ git.magicany.cc/black1552/gf-common v1.0.1018/go.mod h1:ln6bd5oXxPNsktr8xI3itmsq gitee.com/chunanyong/dm v1.8.12 h1:WupbFZL0MRNIIiCPaLDHgFi5jkdkjzjPReuWPaInGwk= gitee.com/chunanyong/dm v1.8.12/go.mod h1:EPRJnuPFgbyOFgJ0TRYCTGzhq+ZT4wdyaj/GW/LLcNg= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.5.1/go.mod h1:h8hyGFDsU5HMivxiS2iYFZsgDbU9OnnJ163x5UGVKYo= github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.2.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/ClickHouse/clickhouse-go v1.5.4/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI= github.com/ClickHouse/clickhouse-go/v2 v2.0.15 h1:lLAZliqrZEygkxosLaW1qHyeTb4Ho7fVCZ0WKCpLocU= github.com/ClickHouse/clickhouse-go/v2 v2.0.15/go.mod h1:Z21o82zD8FFqefOQDg93c0XITlxGbTsWQuRm588Azkk= github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= +github.com/UNO-SOFT/zlog v0.8.1/go.mod h1:yqFOjn3OhvJ4j7ArJqQNA+9V+u6t9zSAyIZdWdMweWc= github.com/VictoriaMetrics/easyproto v0.1.4 h1:r8cNvo8o6sR4QShBXQd1bKw/VVLSQma/V2KhTBPf+Sc= github.com/VictoriaMetrics/easyproto v0.1.4/go.mod h1:QlGlzaJnDfFd8Lk6Ci/fuLxfTo3/GThPs2KH23mv710= github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4= @@ -29,17 +36,21 @@ github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyM github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs= +github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos= github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/duke-git/lancet/v2 v2.3.7/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc= 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/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= @@ -109,6 +120,9 @@ github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0 h1:39+jbTenm7KBj4hO2C8ANAxVH github.com/gogf/gf/contrib/drivers/pgsql/v2 v2.10.0/go.mod h1:B0s0fVzn0W220E8UTpSGzrrGKsop5KcB90twBeLCiz0= github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.0 h1:OyAH7Ls2c9Un7CJiAq7G6eY1jWIICRkN8C5SyM94rnY= github.com/gogf/gf/contrib/drivers/sqlite/v2 v2.10.0/go.mod h1:fwhAMG0qZpeHbbP2JE78rJRfV7eBbu9jXkxTMM1lwyo= +github.com/gogf/gf/contrib/registry/etcd/v2 v2.10.0/go.mod h1:ezkHf1r7YxkFYis7Y1807D1tr+3nXolfL8IsY816Cuw= +github.com/gogf/gf/contrib/registry/file/v2 v2.10.0/go.mod h1:ZzGWiTbQ9nAmDynrmzaRJlqYHD/F3MeMwCvHrkv/LP4= +github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.10.0/go.mod h1:zGZdjS08IqiSGhKfy69/qgcYKUYUL/VsOx5fuT++TVE= github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs= github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -121,6 +135,7 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2V github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -140,19 +155,28 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 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/ibmdb/go_ibm_db v0.5.4 h1:cveEOt1J2PoQivQdxIQB0f8ugDJYKaSmh7RUKAaJyAE= github.com/ibmdb/go_ibm_db v0.5.4/go.mod h1:BA12Alfe+h5BMGZGE+b0pqP4leILZkpoxe5qr/iMoHw= github.com/ibmruntimes/go-recordio/v2 v2.0.0-20240416213906-ae0ad556db70 h1:muF5XqVkHnMdbMDXusPdKtuT8qWzefBgSuLH1JVHcC4= github.com/ibmruntimes/go-recordio/v2 v2.0.0-20240416213906-ae0ad556db70/go.mod h1:NSpUK0x9IyEoM1EjTp2/S8ErxZfRHoA2DfwiYobFSkc= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks= +github.com/jordanlewis/gcassert v0.0.0-20250430164644-389ef753e22e/go.mod h1:ZybsQk6DWyN5t7An1MuPm1gtSZ1xDaTXS9ZjIOxvQrk= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= @@ -163,6 +187,7 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= @@ -192,6 +217,8 @@ github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8 github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 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/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/oklog/ulid/v2 v2.0.2/go.mod h1:mtBL0Qe/0HAx6/a4Z30qxVIAL1eQDweXq5lxOEiwQ68= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc= github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= @@ -200,6 +227,7 @@ github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8= github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I= github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY= +github.com/olekukonko/ts v0.0.0-20171002115256-78ecb04241c0/go.mod h1:F/7q8/HZz+TXjlsoZQQKVYvXTZaFH4QRa3y+j1p7MS0= github.com/paulmach/orb v0.7.1 h1:Zha++Z5OX/l168sqHK3k4z18LDvr+YAO/VjK0ReQ9rU= github.com/paulmach/orb v0.7.1/go.mod h1:FWRlTgl88VI1RBx/MkrwWDRhQ96ctqMCh8boXhmqB/A= github.com/paulmach/protoscan v0.2.1/go.mod h1:SpcSwydNLrxUGSDvXvO0P7g7AuhJ7lcKfDlhJCDw2gY= @@ -209,6 +237,9 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.0/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= @@ -217,6 +248,9 @@ github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SA github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= 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/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= +github.com/richardlehane/msoleps v1.0.4/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= @@ -228,6 +262,7 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sijms/go-ora/v2 v2.7.10 h1:GSLdj0PYYgSndhsnm7b6p32OqgnwnUZSkFb3j+htfhI= github.com/sijms/go-ora/v2 v2.7.10/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= @@ -252,6 +287,7 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tiendc/go-deepcopy v1.6.0/go.mod h1:toXoeQoUqXOOS/X4sKuiAoSk6elIdqc0pN7MTgOOo2I= github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= @@ -264,12 +300,18 @@ github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= +github.com/xuri/excelize/v2 v2.9.1/go.mod h1:x7L6pKz2dvo9ejrRuD8Lnl98z4JLt0TGAwjhW+EiP8s= +github.com/xuri/nfp v0.0.1/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.etcd.io/etcd/api/v3 v3.5.17/go.mod h1:d1hvkRuXkts6PmaYk2Vrgqbv7H4ADfAKhyJqHNLJCB4= +go.etcd.io/etcd/client/pkg/v3 v3.5.17/go.mod h1:4DqK1TKacp/86nJk4FLQqo6Mn2vvQFBmruW3pP14H/w= +go.etcd.io/etcd/client/v3 v3.5.17/go.mod h1:j2d4eXTHWkT2ClBgnnEPm/Wuu7jsqku41v9DZ3OtjQo= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= @@ -288,10 +330,13 @@ go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= @@ -342,8 +387,10 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -359,10 +406,16 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= @@ -381,10 +434,14 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg= gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo= +gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw= modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= @@ -411,3 +468,4 @@ 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= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=