Compare commits

...

10 Commits

Author SHA1 Message Date
black 7e54a96454 ```
fix(database): 修复SQLite迁移中自增主键的定义问题

- 修复了自增列同时标记为主键时重复添加PRIMARY KEY约束的问题
- 确保自增主键列只添加一次PRIMARY KEY约束
- 优化了复合主键约束的添加逻辑,避免与自增约束冲突
- 修正了自增主键的类型定义为INTEGER以符合SQLite规范
```
2026-04-14 09:54:48 +08:00
black 33faa6f722 feat(database): 添加 SQLite 数据库目录自动创建功能
- 为 SQLite 数据库添加数据库文件目录自动创建逻辑
- 实现 ensureSQLiteDirectory 方法解析数据库路径并创建必要目录
- 支持从 config.Name 或 config.Link 中提取 SQLite 文件路径
- 添加对 SQLite 链接格式 sqlite::@file(path) 的路径解析支持
- 在数据库表迁移前确保 SQLite 数据库文件目录存在
- 新增 sqlitecgo 驱动包实现 SQLite 数据库驱动支持
2026-04-14 09:47:19 +08:00
black 284f9380ed feat(database): 添加ClickHouse数据库驱动支持
- 实现了ClickHouse数据库驱动程序,支持基本的数据库操作
- 添加了ClickHouse特定的迁移功能,包括表、列、索引的创建和管理
- 集成了ClickHouse的语法特性,如MergeTree引擎和Nullable类型
- 实现了数据库连接池管理和SQL执行接口
- 添加了对系统表查询的支持,用于检查表和列的存在性
2026-04-13 16:07:54 +08:00
maguodong a083b74f9b chore(gendao): 更新版权信息并修改工具名称标识
- 移除 consts.go 文件中的版权注释
- 将模板文件中的 GoFrame CLI 工具标识替换为 Gin-base CLI 工具
- 更新 DO、Entity、Table 等模板中的自动生成声明
- 移除 mlog 和 utils 包中的版权注释
2026-04-08 17:55:21 +08:00
maguodong 8475859a70 feat(gendao): 添加代码生成工具用于自动生成DAO层代码
- 实现了完整的gendao命令行工具功能
- 添加了模板文件用于生成DAO、DO、Entity等代码结构
- 集成了数据库表字段映射和类型转换功能
- 支持分片表模式匹配和自定义配置选项
- 实现了代码清理和格式化功能
- 提供了灵活的表名过滤和前缀处理机制
2026-04-08 16:53:01 +08:00
maguodong 398e732301 feat(database): 添加 ClickHouse 数据库驱动支持
- 实现了完整的 ClickHouse 驱动,包括连接、查询、插入、更新、删除等基本操作
- 添加了 ClickHouse 特有的数据类型转换和 SQL 过滤功能
- 实现了表结构查询和字段信息获取功能
- 添加了 Ping 检查和错误处理机制
- 增加了对 UPDATE 和 DELETE 语句的语法转换以适配 ClickHouse
- 添加了批量插入操作的支持
- 新增了 14 个测试用例文件用于验证数据库相关功能
2026-04-08 15:34:51 +08:00
maguodong 70ad831041 refactor(cmd): 修改模块名称为gen
- 将模块名从 gin-base/cmd 更改为 gin-base/cmd/gen
- 更新 go.mod 文件中的模块声明
- 保持 Go 版本要求为 1.25.0 不变
2026-04-08 14:22:07 +08:00
maguodong c88ac8f871 移动main.go 2026-04-08 14:16:20 +08:00
maguodong fd1a6686b1 feat(generator): 添加代码生成模板常量定义
- 新增 consts.go 定义 DO NOT EDIT 标记常量
- 添加控制器生成模板相关常量定义
- 添加 SDK 客户端生成模板常量定义
- 添加数据访问对象(DAO)生成模板常量定义
- 添加枚举、协议缓冲区实体和服务生成模板常量定义
- 添加数据库表字段定义模板常量定义
2026-04-08 14:11:46 +08:00
maguodong d01b2973df feat(database): 添加ClickHouse数据库驱动支持
- 新增ClickHouse数据库驱动实现
- 更新数据库驱动注册包,添加对MySQL、PostgreSQL、SQL Server、Oracle、SQLite和ClickHouse的支持
- 修改Database函数返回类型从gdb.DB到DB
- 添加go.mod和go.sum依赖管理文件
2026-04-08 11:02:33 +08:00
138 changed files with 10989 additions and 139 deletions

69
cmd/README.md Normal file
View File

@ -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`

View File

@ -0,0 +1,138 @@
// Copyright GoFrame gf 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 consts
const TemplateGenDaoIndexContent = `
// =================================================================================
// This file is auto-generated by the Gin-base CLI tool. You may modify it as needed.
// =================================================================================
package {{.TplPackageName}}
import (
"{{.TplImportPrefix}}/internal"
"git.magicany.cc/black1552/gin-base/database"
)
// {{.TplTableNameCamelLowerCase}}Dao is the data access object for the table {{.TplTableName}}.
// You can define custom methods on it to extend its functionality as needed.
type {{.TplTableNameCamelLowerCase}}Dao struct {
*internal.{{.TplTableNameCamelCase}}Dao
}
var (
// {{.TplTableNameCamelCase}} is a globally accessible object for table {{.TplTableName}} operations.
{{.TplTableNameCamelCase}} = {{.TplTableNameCamelLowerCase}}Dao{
{{- if .TplTableSharding -}}
internal.New{{.TplTableNameCamelCase}}Dao({{.TplTableNameCamelLowerCase}}ShardingHandler),
{{- else -}}
internal.New{{.TplTableNameCamelCase}}Dao(),
{{- end -}}
}
)
{{if .TplTableSharding -}}
// {{.TplTableNameCamelLowerCase}}ShardingHandler is the handler for sharding operations.
// You can fill this sharding handler with your custom implementation.
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: database.ShardingSchemaConfig{},
})
return m
}
{{- end}}
// Add your custom methods and functionality below.
`
const TemplateGenDaoInternalContent = `
// ==========================================================================
// Code generated and maintained by Gin-base CLI tool. DO NOT EDIT. {{.TplCreatedAtDatetimeStr}}
// ==========================================================================
package internal
import (
"context"
"git.magicany.cc/black1552/gin-base/database"
)
// {{.TplTableNameCamelCase}}Dao is the data access object for the table {{.TplTableName}}.
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 []database.ModelHandler // handlers for customized model modification.
}
// {{.TplTableNameCamelCase}}Columns defines and stores column names for the table {{.TplTableName}}.
type {{.TplTableNameCamelCase}}Columns struct {
{{.TplColumnDefine}}
}
// {{.TplTableNameCamelLowerCase}}Columns holds the columns for the table {{.TplTableName}}.
var {{.TplTableNameCamelLowerCase}}Columns = {{.TplTableNameCamelCase}}Columns{
{{.TplColumnNames}}
}
// New{{.TplTableNameCamelCase}}Dao creates and returns a new DAO object for table data access.
func New{{.TplTableNameCamelCase}}Dao(handlers ...database.ModelHandler) *{{.TplTableNameCamelCase}}Dao {
return &{{.TplTableNameCamelCase}}Dao{
group: "{{.TplGroupName}}",
table: "{{.TplTableName}}",
columns: {{.TplTableNameCamelLowerCase}}Columns,
handlers: handlers,
}
}
// DB retrieves and returns the underlying raw database management object of the current DAO.
func (dao *{{.TplTableNameCamelCase}}Dao) DB() database.DB {
return database.Database(dao.group)
}
// Table returns the table name of the current DAO.
func (dao *{{.TplTableNameCamelCase}}Dao) Table() string {
return dao.table
}
// Columns returns all column names of the current DAO.
func (dao *{{.TplTableNameCamelCase}}Dao) Columns() {{.TplTableNameCamelCase}}Columns {
return dao.columns
}
// Group returns the database configuration group name of the current DAO.
func (dao *{{.TplTableNameCamelCase}}Dao) Group() string {
return dao.group
}
// 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) *database.Model {
model := dao.DB().Model(dao.table)
for _, handler := range dao.handlers {
model = handler(model)
}
return model.Safe().Ctx(ctx)
}
// Transaction wraps the transaction logic using function f.
// It rolls back the transaction and returns the error if function f returns a non-nil error.
// It commits the transaction and returns nil if function f returns nil.
//
// 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 database.TX) error) (err error) {
return dao.Ctx(ctx).Transaction(ctx, f)
}
`

View File

@ -0,0 +1,20 @@
// Copyright GoFrame gf 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 consts
const TemplateGenDaoDoContent = `
// =================================================================================
// Code generated and maintained by Gin-base CLI tool. DO NOT EDIT. {{.TplCreatedAtDatetimeStr}}
// =================================================================================
package {{.TplPackageName}}
{{.TplPackageImports}}
// {{.TplTableNameCamelCase}} is the golang structure of table {{.TplTableName}} for DAO operations like Where/Data.
{{.TplStructDefine}}
`

View File

@ -0,0 +1,20 @@
// Copyright GoFrame gf 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 consts
const TemplateGenDaoEntityContent = `
// =================================================================================
// Code generated and maintained by Gin-base CLI tool. DO NOT EDIT. {{.TplCreatedAtDatetimeStr}}
// =================================================================================
package {{.TplPackageName}}
{{.TplPackageImports}}
// {{.TplTableNameCamelCase}} is the golang structure for table {{.TplTableName}}.
{{.TplStructDefine}}
`

View File

@ -0,0 +1,34 @@
// Copyright GoFrame gf 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 consts
const TemplateGenTableContent = `
// =================================================================================
// This file is auto-generated by the Gin-base CLI tool. You may modify it as needed.
// =================================================================================
package {{.TplPackageName}}
import (
"context"
"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]*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 database.DB, schema ...string) error {
return db.GetCore().SetTableFields(ctx, "{{.TplTableName}}", {{.TplTableNameCamelCase}}, schema...)
}
`

69
cmd/gendao/README.md Normal file
View File

@ -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`

491
cmd/gendao/gendao.go Normal file
View File

@ -0,0 +1,491 @@
// Copyright GoFrame gf 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 gendao
import (
"context"
"fmt"
"sort"
"strings"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
"golang.org/x/mod/modfile"
"git.magicany.cc/black1552/gin-base/database"
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/container/gset"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gproc"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/os/gview"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/mlog"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/utils"
)
type (
CGenDao struct{}
CGenDaoInput struct {
g.Meta `name:"dao" config:"{CGenDaoConfig}" usage:"{CGenDaoUsage}" brief:"{CGenDaoBrief}" eg:"{CGenDaoEg}" ad:"{CGenDaoAd}"`
Path string `name:"path" short:"p" brief:"{CGenDaoBriefPath}" d:"internal"`
Link string `name:"link" short:"l" brief:"{CGenDaoBriefLink}"`
Tables string `name:"tables" short:"t" brief:"{CGenDaoBriefTables}"`
TablesEx string `name:"tablesEx" short:"x" brief:"{CGenDaoBriefTablesEx}"`
ShardingPattern []string `name:"shardingPattern" short:"sp" brief:"{CGenDaoBriefShardingPattern}"`
Group string `name:"group" short:"g" brief:"{CGenDaoBriefGroup}" d:"default"`
Prefix string `name:"prefix" short:"f" brief:"{CGenDaoBriefPrefix}"`
RemovePrefix string `name:"removePrefix" short:"r" brief:"{CGenDaoBriefRemovePrefix}"`
RemoveFieldPrefix string `name:"removeFieldPrefix" short:"rf" brief:"{CGenDaoBriefRemoveFieldPrefix}"`
JsonCase string `name:"jsonCase" short:"j" brief:"{CGenDaoBriefJsonCase}" d:"CamelLower"`
ImportPrefix string `name:"importPrefix" short:"i" brief:"{CGenDaoBriefImportPrefix}"`
DaoPath string `name:"daoPath" short:"d" brief:"{CGenDaoBriefDaoPath}" d:"dao"`
TablePath string `name:"tablePath" short:"tp" brief:"{CGenDaoBriefTablePath}" d:"table"`
DoPath string `name:"doPath" short:"o" brief:"{CGenDaoBriefDoPath}" d:"model/do"`
EntityPath string `name:"entityPath" short:"e" brief:"{CGenDaoBriefEntityPath}" d:"model/entity"`
TplDaoTablePath string `name:"tplDaoTablePath" short:"t0" brief:"{CGenDaoBriefTplDaoTablePath}"`
TplDaoIndexPath string `name:"tplDaoIndexPath" short:"t1" brief:"{CGenDaoBriefTplDaoIndexPath}"`
TplDaoInternalPath string `name:"tplDaoInternalPath" short:"t2" brief:"{CGenDaoBriefTplDaoInternalPath}"`
TplDaoDoPath string `name:"tplDaoDoPath" short:"t3" brief:"{CGenDaoBriefTplDaoDoPathPath}"`
TplDaoEntityPath string `name:"tplDaoEntityPath" short:"t4" brief:"{CGenDaoBriefTplDaoEntityPath}"`
StdTime bool `name:"stdTime" short:"s" brief:"{CGenDaoBriefStdTime}" orphan:"true"`
WithTime bool `name:"withTime" short:"w" brief:"{CGenDaoBriefWithTime}" orphan:"true"`
GJsonSupport bool `name:"gJsonSupport" short:"n" brief:"{CGenDaoBriefGJsonSupport}" orphan:"true"`
OverwriteDao bool `name:"overwriteDao" short:"v" brief:"{CGenDaoBriefOverwriteDao}" orphan:"true"`
DescriptionTag bool `name:"descriptionTag" short:"c" brief:"{CGenDaoBriefDescriptionTag}" orphan:"true"`
NoJsonTag bool `name:"noJsonTag" short:"k" brief:"{CGenDaoBriefNoJsonTag}" orphan:"true"`
NoModelComment bool `name:"noModelComment" short:"m" brief:"{CGenDaoBriefNoModelComment}" orphan:"true"`
Clear bool `name:"clear" short:"a" brief:"{CGenDaoBriefClear}" orphan:"true"`
GenTable bool `name:"genTable" short:"gt" brief:"{CGenDaoBriefGenTable}" orphan:"true"`
TypeMapping map[DBFieldTypeName]CustomAttributeType `name:"typeMapping" short:"y" brief:"{CGenDaoBriefTypeMapping}" orphan:"true"`
FieldMapping map[DBTableFieldName]CustomAttributeType `name:"fieldMapping" short:"fm" brief:"{CGenDaoBriefFieldMapping}" orphan:"true"`
// internal usage purpose.
genItems *CGenDaoInternalGenItems
}
CGenDaoOutput struct{}
CGenDaoInternalInput struct {
CGenDaoInput
DB database.DB
TableNames []string
NewTableNames []string
ShardingTableSet *gset.StrSet
}
DBTableFieldName = string
DBFieldTypeName = string
CustomAttributeType struct {
Type string `brief:"custom attribute type name"`
Import string `brief:"custom import for this type"`
}
)
var (
createdAt = gtime.Now()
tplView = gview.New()
defaultTypeMapping = map[DBFieldTypeName]CustomAttributeType{
"decimal": {
Type: "float64",
},
"money": {
Type: "float64",
},
"numeric": {
Type: "float64",
},
"smallmoney": {
Type: "float64",
},
"uuid": {
Type: "uuid.UUID",
Import: "github.com/google/uuid",
},
}
// tablewriter Options
twRenderer = tablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{
Borders: tw.Border{Top: tw.Off, Bottom: tw.Off, Left: tw.Off, Right: tw.Off},
Settings: tw.Settings{
Separators: tw.Separators{BetweenRows: tw.Off, BetweenColumns: tw.Off},
},
Symbols: tw.NewSymbols(tw.StyleASCII),
}))
twConfig = tablewriter.WithConfig(tablewriter.Config{
Row: tw.CellConfig{
Formatting: tw.CellFormatting{AutoWrap: tw.WrapNone},
},
})
)
func (c CGenDao) Dao(ctx context.Context, in CGenDaoInput) (out *CGenDaoOutput, err error) {
in.genItems = newCGenDaoInternalGenItems()
if in.Link != "" {
doGenDaoForArray(ctx, -1, in)
} else if g.Cfg().Available(ctx) {
v := g.Cfg().MustGet(ctx, CGenDaoConfig)
if v.IsSlice() {
for i := 0; i < len(v.Interfaces()); i++ {
doGenDaoForArray(ctx, i, in)
}
} else {
doGenDaoForArray(ctx, -1, in)
}
} else {
doGenDaoForArray(ctx, -1, in)
}
doClear(in.genItems)
mlog.Print("done!")
return
}
// doGenDaoForArray implements the "gen dao" command for configuration array.
func doGenDaoForArray(ctx context.Context, index int, in CGenDaoInput) {
var (
err error
db database.DB
)
if index >= 0 {
err = g.Cfg().MustGet(
ctx,
fmt.Sprintf(`%s.%d`, CGenDaoConfig, index),
).Scan(&in)
if err != nil {
mlog.Fatalf(`invalid configuration of "%s": %+v`, CGenDaoConfig, err)
}
}
if dirRealPath := gfile.RealPath(in.Path); dirRealPath == "" {
mlog.Fatalf(`path "%s" does not exist`, in.Path)
}
removePrefixArray := gstr.SplitAndTrim(in.RemovePrefix, ",")
// It uses user passed database configuration.
if in.Link != "" {
var tempGroup = gtime.TimestampNanoStr()
err = database.AddConfigNode(tempGroup, database.ConfigNode{
Link: in.Link,
})
if err != nil {
mlog.Fatalf(`database configuration failed: %+v`, err)
}
if db, err = database.Instance(tempGroup); err != nil {
mlog.Fatalf(`database initialization failed: %+v`, err)
}
} else {
db = database.Database(in.Group)
}
if db == nil {
mlog.Fatal(`database initialization failed, may be invalid database configuration`)
}
var tableNames []string
if in.Tables != "" {
inputTables := gstr.SplitAndTrim(in.Tables, ",")
// Check if any table pattern contains wildcard characters.
// https://github.com/gogf/gf/issues/4629
var hasPattern bool
for _, t := range inputTables {
if containsWildcard(t) {
hasPattern = true
break
}
}
if hasPattern {
// Fetch all tables first, then filter by patterns.
allTables, err := db.Tables(context.TODO())
if err != nil {
mlog.Fatalf("fetching tables failed: %+v", err)
}
tableNames = filterTablesByPatterns(allTables, inputTables)
} else {
// Use exact table names as before.
tableNames = inputTables
}
} else {
tableNames, err = db.Tables(context.TODO())
if err != nil {
mlog.Fatalf("fetching tables failed: %+v", err)
}
}
// Table excluding.
if in.TablesEx != "" {
array := garray.NewStrArrayFrom(tableNames)
for _, p := range gstr.SplitAndTrim(in.TablesEx, ",") {
if containsWildcard(p) {
// Use exact match with ^ and $ anchors for consistency with tables pattern.
regPattern := "^" + patternToRegex(p) + "$"
for _, v := range array.Clone().Slice() {
if gregex.IsMatchString(regPattern, v) {
array.RemoveValue(v)
}
}
} else {
array.RemoveValue(p)
}
}
tableNames = array.Slice()
}
// merge default typeMapping to input typeMapping.
if in.TypeMapping == nil {
in.TypeMapping = defaultTypeMapping
} else {
for key, typeMapping := range defaultTypeMapping {
if _, ok := in.TypeMapping[key]; !ok {
in.TypeMapping[key] = typeMapping
}
}
}
// Generating dao & model go files one by one according to given table name.
var (
newTableNames = make([]string, len(tableNames))
shardingNewTableSet = gset.NewStrSet()
)
// Sort sharding patterns by length descending, so that longer (more specific) patterns
// are matched first. This prevents shorter patterns like "a_?" from incorrectly matching
// tables that should match longer patterns like "a_b_?" or "a_c_?".
// https://github.com/gogf/gf/issues/4603
sortedShardingPatterns := make([]string, len(in.ShardingPattern))
copy(sortedShardingPatterns, in.ShardingPattern)
sort.Slice(sortedShardingPatterns, func(i, j int) bool {
return len(sortedShardingPatterns[i]) > len(sortedShardingPatterns[j])
})
for i, tableName := range tableNames {
newTableName := tableName
for _, v := range removePrefixArray {
newTableName = gstr.TrimLeftStr(newTableName, v, 1)
}
if len(sortedShardingPatterns) > 0 {
for _, pattern := range sortedShardingPatterns {
var (
match []string
regPattern = gstr.Replace(pattern, "?", `(.+)`)
)
match, err = gregex.MatchString(regPattern, newTableName)
if err != nil {
mlog.Fatalf(`invalid sharding pattern "%s": %+v`, pattern, err)
}
if len(match) < 2 {
continue
}
newTableName = gstr.Replace(pattern, "?", "")
newTableName = gstr.Trim(newTableName, `_.-`)
if shardingNewTableSet.Contains(newTableName) {
tableNames[i] = ""
break
}
// Add prefix to sharding table name, if not, the isSharding check would not match.
shardingNewTableSet.Add(in.Prefix + newTableName)
break
}
}
newTableName = in.Prefix + newTableName
if tableNames[i] != "" {
// If shardingNewTableSet contains newTableName (tableName is empty), it should not be added to tableNames, make it empty and filter later.
newTableNames[i] = newTableName
}
}
tableNames = garray.NewStrArrayFrom(tableNames).FilterEmpty().Slice()
newTableNames = garray.NewStrArrayFrom(newTableNames).FilterEmpty().Slice() // Filter empty table names. make sure that newTableNames and tableNames have the same length.
in.genItems.Scale()
// Dao: index and internal.
generateDao(ctx, CGenDaoInternalInput{
CGenDaoInput: in,
DB: db,
TableNames: tableNames,
NewTableNames: newTableNames,
ShardingTableSet: shardingNewTableSet,
})
// Table: table fields.
generateTable(ctx, CGenDaoInternalInput{
CGenDaoInput: in,
DB: db,
TableNames: tableNames,
NewTableNames: newTableNames,
ShardingTableSet: shardingNewTableSet,
})
// Do.
generateDo(ctx, CGenDaoInternalInput{
CGenDaoInput: in,
DB: db,
TableNames: tableNames,
NewTableNames: newTableNames,
})
// Entity.
generateEntity(ctx, CGenDaoInternalInput{
CGenDaoInput: in,
DB: db,
TableNames: tableNames,
NewTableNames: newTableNames,
})
in.genItems.SetClear(in.Clear)
}
func getImportPartContent(ctx context.Context, source string, isDo bool, appendImports []string) string {
var packageImportsArray = garray.NewStrArray()
if isDo {
packageImportsArray.Append(`"github.com/gogf/gf/v2/frame/g"`)
}
// Time package recognition.
if strings.Contains(source, "gtime.Time") {
packageImportsArray.Append(`"github.com/gogf/gf/v2/os/gtime"`)
} else if strings.Contains(source, "time.Time") {
packageImportsArray.Append(`"time"`)
}
// Json type.
if strings.Contains(source, "gjson.Json") {
packageImportsArray.Append(`"github.com/gogf/gf/v2/encoding/gjson"`)
}
// Check and update imports in go.mod
if len(appendImports) > 0 {
goModPath := utils.GetModPath()
if goModPath == "" {
mlog.Fatal("go.mod not found in current project")
}
mod, err := modfile.Parse(goModPath, gfile.GetBytes(goModPath), nil)
if err != nil {
mlog.Fatalf("parse go.mod failed: %+v", err)
}
for _, appendImport := range appendImports {
found := false
for _, require := range mod.Require {
if gstr.Contains(appendImport, require.Mod.Path) {
found = true
break
}
}
if !found {
if err = gproc.ShellRun(ctx, `go get `+appendImport); err != nil {
mlog.Fatalf(`%+v`, err)
}
}
packageImportsArray.Append(fmt.Sprintf(`"%s"`, appendImport))
}
}
// Generate and write content to golang file.
packageImportsStr := ""
if packageImportsArray.Len() > 0 {
packageImportsStr = fmt.Sprintf("import(\n%s\n)", packageImportsArray.Join("\n"))
}
return packageImportsStr
}
func assignDefaultVar(view *gview.View, in CGenDaoInternalInput) {
var (
tplCreatedAtDatetimeStr string
tplDatetimeStr = createdAt.String()
)
if in.WithTime {
tplCreatedAtDatetimeStr = fmt.Sprintf(`Created at %s`, tplDatetimeStr)
}
view.Assigns(g.Map{
tplVarDatetimeStr: tplDatetimeStr,
tplVarCreatedAtDatetimeStr: tplCreatedAtDatetimeStr,
})
}
func sortFieldKeyForDao(fieldMap map[string]*database.TableField) []string {
names := make(map[int]string)
for _, field := range fieldMap {
names[field.Index] = field.Name
}
var (
i = 0
j = 0
result = make([]string, len(names))
)
for {
if len(names) == 0 {
break
}
if val, ok := names[i]; ok {
result[j] = val
j++
delete(names, i)
}
i++
}
return result
}
func getTemplateFromPathOrDefault(filePath string, def string) string {
if filePath != "" {
if contents := gfile.GetContents(filePath); contents != "" {
return contents
}
}
return def
}
// containsWildcard checks if the pattern contains wildcard characters (* or ?).
func containsWildcard(pattern string) bool {
return gstr.Contains(pattern, "*") || gstr.Contains(pattern, "?")
}
// patternToRegex converts a wildcard pattern to a regex pattern.
// Wildcard characters: * matches any characters, ? matches single character.
func patternToRegex(pattern string) string {
pattern = gstr.ReplaceByMap(pattern, map[string]string{
"\r": "",
"\n": "",
})
pattern = gstr.ReplaceByMap(pattern, map[string]string{
"*": "\r",
"?": "\n",
})
pattern = gregex.Quote(pattern)
pattern = gstr.ReplaceByMap(pattern, map[string]string{
"\r": ".*",
"\n": ".",
})
return pattern
}
// filterTablesByPatterns filters tables by given patterns.
// Patterns support wildcard characters: * matches any characters, ? matches single character.
// https://github.com/gogf/gf/issues/4629
func filterTablesByPatterns(allTables []string, patterns []string) []string {
var result []string
matched := make(map[string]bool)
allTablesSet := make(map[string]bool)
for _, t := range allTables {
allTablesSet[t] = true
}
for _, p := range patterns {
if containsWildcard(p) {
regPattern := "^" + patternToRegex(p) + "$"
for _, table := range allTables {
if !matched[table] && gregex.IsMatchString(regPattern, table) {
result = append(result, table)
matched[table] = true
}
}
} else {
// Exact table name, use direct string comparison.
if !allTablesSet[p] {
mlog.Printf(`table "%s" does not exist, skipped`, p)
continue
}
if !matched[p] {
result = append(result, p)
matched[p] = true
}
}
}
return result
}

View File

@ -0,0 +1,48 @@
// Copyright GoFrame gf 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 gendao
import (
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/text/gstr"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/mlog"
)
func doClear(items *CGenDaoInternalGenItems) {
var allGeneratedFilePaths = make([]string, 0)
for _, item := range items.Items {
allGeneratedFilePaths = append(allGeneratedFilePaths, item.GeneratedFilePaths...)
}
for i, v := range allGeneratedFilePaths {
allGeneratedFilePaths[i] = gfile.RealPath(v)
}
for _, item := range items.Items {
if !item.Clear {
continue
}
doClearItem(item, allGeneratedFilePaths)
}
}
func doClearItem(item CGenDaoInternalGenItem, allGeneratedFilePaths []string) {
var generatedFilePaths = make([]string, 0)
for _, dirPath := range item.StorageDirPaths {
filePaths, err := gfile.ScanDirFile(dirPath, "*.go", true)
if err != nil {
mlog.Fatal(err)
}
generatedFilePaths = append(generatedFilePaths, filePaths...)
}
for _, filePath := range generatedFilePaths {
if !gstr.InArray(allGeneratedFilePaths, filePath) {
if err := gfile.RemoveFile(filePath); err != nil {
mlog.Print(err)
}
}
}
}

260
cmd/gendao/gendao_dao.go Normal file
View File

@ -0,0 +1,260 @@
// Copyright GoFrame gf 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 gendao
import (
"bytes"
"context"
"fmt"
"path/filepath"
"strings"
"git.magicany.cc/black1552/gin-base/cmd/consts"
"github.com/olekukonko/tablewriter"
"git.magicany.cc/black1552/gin-base/database"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gview"
"github.com/gogf/gf/v2/text/gstr"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/mlog"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/utils"
)
func generateDao(ctx context.Context, in CGenDaoInternalInput) {
var (
dirPathDao = gfile.Join(in.Path, in.DaoPath)
dirPathDaoInternal = gfile.Join(dirPathDao, "internal")
)
in.genItems.AppendDirPath(dirPathDao)
for i := 0; i < len(in.TableNames); i++ {
var (
realTableName = in.TableNames[i]
newTableName = in.NewTableNames[i]
)
generateDaoSingle(ctx, generateDaoSingleInput{
CGenDaoInternalInput: in,
TableName: realTableName,
NewTableName: newTableName,
DirPathDao: dirPathDao,
DirPathDaoInternal: dirPathDaoInternal,
IsSharding: in.ShardingTableSet.Contains(newTableName),
})
}
}
type generateDaoSingleInput struct {
CGenDaoInternalInput
// TableName specifies the table name of the table.
TableName string
// NewTableName specifies the prefix-stripped or custom edited name of the table.
NewTableName string
DirPathDao string
DirPathDaoInternal string
IsSharding bool
}
// generateDaoSingle generates the dao and model content of given table.
func generateDaoSingle(ctx context.Context, in generateDaoSingleInput) {
// Generating table data preparing.
fieldMap, err := in.DB.TableFields(ctx, in.TableName)
if err != nil {
mlog.Fatalf(`fetching tables fields failed for table "%s": %+v`, in.TableName, err)
}
var (
tableNameCamelCase = formatFieldName(in.NewTableName, FieldNameCaseCamel)
tableNameCamelLowerCase = formatFieldName(in.NewTableName, FieldNameCaseCamelLower)
tableNameSnakeCase = gstr.CaseSnake(in.NewTableName)
importPrefix = in.ImportPrefix
)
if importPrefix == "" {
importPrefix = utils.GetImportPath(gfile.Join(in.Path, in.DaoPath))
} else {
importPrefix = gstr.Join(g.SliceStr{importPrefix, in.DaoPath}, "/")
}
fileName := gstr.Trim(tableNameSnakeCase, "-_.")
if len(fileName) > 5 && fileName[len(fileName)-5:] == "_test" {
// Add suffix to avoid the table name which contains "_test",
// which would make the go file a testing file.
fileName += "_table"
}
// dao - index
generateDaoIndex(generateDaoIndexInput{
generateDaoSingleInput: in,
TableNameCamelCase: tableNameCamelCase,
TableNameCamelLowerCase: tableNameCamelLowerCase,
ImportPrefix: importPrefix,
FileName: fileName,
})
// dao - internal
generateDaoInternal(generateDaoInternalInput{
generateDaoSingleInput: in,
TableNameCamelCase: tableNameCamelCase,
TableNameCamelLowerCase: tableNameCamelLowerCase,
ImportPrefix: importPrefix,
FileName: fileName,
FieldMap: fieldMap,
})
}
type generateDaoIndexInput struct {
generateDaoSingleInput
TableNameCamelCase string
TableNameCamelLowerCase string
ImportPrefix string
FileName string
}
func generateDaoIndex(in generateDaoIndexInput) {
path := filepath.FromSlash(gfile.Join(in.DirPathDao, in.FileName+".go"))
// It should add path to result slice whenever it would generate the path file or not.
in.genItems.AppendGeneratedFilePath(path)
if in.OverwriteDao || !gfile.Exists(path) {
var (
ctx = context.Background()
tplContent = getTemplateFromPathOrDefault(
in.TplDaoIndexPath, consts.TemplateGenDaoIndexContent,
)
)
tplView.ClearAssigns()
tplView.Assigns(gview.Params{
tplVarTableSharding: in.IsSharding,
tplVarTableShardingPrefix: in.NewTableName + "_",
tplVarImportPrefix: in.ImportPrefix,
tplVarTableName: in.TableName,
tplVarTableNameCamelCase: in.TableNameCamelCase,
tplVarTableNameCamelLowerCase: in.TableNameCamelLowerCase,
tplVarPackageName: filepath.Base(in.DaoPath),
})
indexContent, err := tplView.ParseContent(ctx, tplContent)
if err != nil {
mlog.Fatalf("parsing template content failed: %v", err)
}
if err = gfile.PutContents(path, strings.TrimSpace(indexContent)); err != nil {
mlog.Fatalf("writing content to '%s' failed: %v", path, err)
} else {
utils.GoFmt(path)
mlog.Print("generated:", gfile.RealPath(path))
}
}
}
type generateDaoInternalInput struct {
generateDaoSingleInput
TableNameCamelCase string
TableNameCamelLowerCase string
ImportPrefix string
FileName string
FieldMap map[string]*database.TableField
}
func generateDaoInternal(in generateDaoInternalInput) {
var (
ctx = context.Background()
removeFieldPrefixArray = gstr.SplitAndTrim(in.RemoveFieldPrefix, ",")
tplContent = getTemplateFromPathOrDefault(
in.TplDaoInternalPath, consts.TemplateGenDaoInternalContent,
)
)
tplView.ClearAssigns()
tplView.Assigns(gview.Params{
tplVarImportPrefix: in.ImportPrefix,
tplVarTableName: in.TableName,
tplVarGroupName: in.Group,
tplVarTableNameCamelCase: in.TableNameCamelCase,
tplVarTableNameCamelLowerCase: in.TableNameCamelLowerCase,
tplVarColumnDefine: gstr.Trim(generateColumnDefinitionForDao(in.FieldMap, removeFieldPrefixArray)),
tplVarColumnNames: gstr.Trim(generateColumnNamesForDao(in.FieldMap, removeFieldPrefixArray)),
})
assignDefaultVar(tplView, in.CGenDaoInternalInput)
modelContent, err := tplView.ParseContent(ctx, tplContent)
if err != nil {
mlog.Fatalf("parsing template content failed: %v", err)
}
path := filepath.FromSlash(gfile.Join(in.DirPathDaoInternal, in.FileName+".go"))
in.genItems.AppendGeneratedFilePath(path)
if err := gfile.PutContents(path, strings.TrimSpace(modelContent)); err != nil {
mlog.Fatalf("writing content to '%s' failed: %v", path, err)
} else {
utils.GoFmt(path)
mlog.Print("generated:", gfile.RealPath(path))
}
}
// generateColumnNamesForDao generates and returns the column names assignment content of column struct
// for specified table.
func generateColumnNamesForDao(fieldMap map[string]*database.TableField, removeFieldPrefixArray []string) string {
var (
buffer = bytes.NewBuffer(nil)
array = make([][]string, len(fieldMap))
names = sortFieldKeyForDao(fieldMap)
)
for index, name := range names {
field := fieldMap[name]
newFiledName := field.Name
for _, v := range removeFieldPrefixArray {
newFiledName = gstr.TrimLeftStr(newFiledName, v, 1)
}
array[index] = []string{
" #" + formatFieldName(newFiledName, FieldNameCaseCamel) + ":",
fmt.Sprintf(` #"%s",`, field.Name),
}
}
table := tablewriter.NewTable(buffer, twRenderer, twConfig)
table.Bulk(array)
table.Render()
namesContent := buffer.String()
// Let's do this hack of table writer for indent!
namesContent = gstr.Replace(namesContent, " #", "")
buffer.Reset()
buffer.WriteString(namesContent)
return buffer.String()
}
// generateColumnDefinitionForDao generates and returns the column names definition for specified table.
func generateColumnDefinitionForDao(fieldMap map[string]*database.TableField, removeFieldPrefixArray []string) string {
var (
buffer = bytes.NewBuffer(nil)
array = make([][]string, len(fieldMap))
names = sortFieldKeyForDao(fieldMap)
)
for index, name := range names {
var (
field = fieldMap[name]
comment = gstr.Trim(gstr.ReplaceByArray(field.Comment, g.SliceStr{
"\n", " ",
"\r", " ",
}))
)
newFiledName := field.Name
for _, v := range removeFieldPrefixArray {
newFiledName = gstr.TrimLeftStr(newFiledName, v, 1)
}
array[index] = []string{
" #" + formatFieldName(newFiledName, FieldNameCaseCamel),
" # " + "string",
" #" + fmt.Sprintf(`// %s`, comment),
}
}
table := tablewriter.NewTable(buffer, twRenderer, twConfig)
table.Bulk(array)
table.Render()
defineContent := buffer.String()
// Let's do this hack of table writer for indent!
defineContent = gstr.Replace(defineContent, " #", "")
buffer.Reset()
buffer.WriteString(defineContent)
return buffer.String()
}

100
cmd/gendao/gendao_do.go Normal file
View File

@ -0,0 +1,100 @@
// Copyright GoFrame gf 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 gendao
import (
"context"
"fmt"
"path/filepath"
"strings"
"git.magicany.cc/black1552/gin-base/cmd/consts"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gview"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/mlog"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/utils"
)
func generateDo(ctx context.Context, in CGenDaoInternalInput) {
var dirPathDo = filepath.FromSlash(gfile.Join(in.Path, in.DoPath))
in.genItems.AppendDirPath(dirPathDo)
in.NoJsonTag = true
in.DescriptionTag = false
in.NoModelComment = false
// Model content.
for i, tableName := range in.TableNames {
fieldMap, err := in.DB.TableFields(ctx, tableName)
if err != nil {
mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", tableName, err)
}
var (
newTableName = in.NewTableNames[i]
doFilePath = gfile.Join(dirPathDo, gstr.CaseSnake(newTableName)+".go")
structDefinition, _ = generateStructDefinition(ctx, generateStructDefinitionInput{
CGenDaoInternalInput: in,
TableName: tableName,
StructName: formatFieldName(newTableName, FieldNameCaseCamel),
FieldMap: fieldMap,
IsDo: true,
})
)
// replace all types to any.
structDefinition, _ = gregex.ReplaceStringFuncMatch(
"([A-Z]\\w*?)\\s+([\\w\\*\\.]+?)\\s+(//)",
structDefinition,
func(match []string) string {
// If the type is already a pointer/slice/map, it does nothing.
if !gstr.HasPrefix(match[2], "*") && !gstr.HasPrefix(match[2], "[]") && !gstr.HasPrefix(match[2], "map") {
return fmt.Sprintf(`%s any %s`, match[1], match[3])
}
return match[0]
},
)
modelContent := generateDoContent(
ctx,
in,
tableName,
formatFieldName(newTableName, FieldNameCaseCamel),
structDefinition,
)
in.genItems.AppendGeneratedFilePath(doFilePath)
err = gfile.PutContents(doFilePath, strings.TrimSpace(modelContent))
if err != nil {
mlog.Fatalf(`writing content to "%s" failed: %v`, doFilePath, err)
} else {
utils.GoFmt(doFilePath)
mlog.Print("generated:", gfile.RealPath(doFilePath))
}
}
}
func generateDoContent(
ctx context.Context, in CGenDaoInternalInput, tableName, tableNameCamelCase, structDefine string,
) string {
var (
tplContent = getTemplateFromPathOrDefault(
in.TplDaoDoPath, consts.TemplateGenDaoDoContent,
)
)
tplView.ClearAssigns()
tplView.Assigns(gview.Params{
tplVarTableName: tableName,
tplVarPackageImports: getImportPartContent(ctx, structDefine, true, nil),
tplVarTableNameCamelCase: tableNameCamelCase,
tplVarStructDefine: structDefine,
tplVarPackageName: filepath.Base(in.DoPath),
})
assignDefaultVar(tplView, in)
doContent, err := tplView.ParseContent(ctx, tplContent)
if err != nil {
mlog.Fatalf("parsing template content failed: %v", err)
}
return doContent
}

View File

@ -0,0 +1,85 @@
// Copyright GoFrame gf 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 gendao
import (
"context"
"path/filepath"
"strings"
"git.magicany.cc/black1552/gin-base/cmd/consts"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gview"
"github.com/gogf/gf/v2/text/gstr"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/mlog"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/utils"
)
func generateEntity(ctx context.Context, in CGenDaoInternalInput) {
var dirPathEntity = gfile.Join(in.Path, in.EntityPath)
in.genItems.AppendDirPath(dirPathEntity)
// Model content.
for i, tableName := range in.TableNames {
fieldMap, err := in.DB.TableFields(ctx, tableName)
if err != nil {
mlog.Fatalf("fetching tables fields failed for table '%s':\n%v", tableName, err)
}
var (
newTableName = in.NewTableNames[i]
entityFilePath = filepath.FromSlash(gfile.Join(dirPathEntity, gstr.CaseSnake(newTableName)+".go"))
structDefinition, appendImports = generateStructDefinition(ctx, generateStructDefinitionInput{
CGenDaoInternalInput: in,
TableName: tableName,
StructName: formatFieldName(newTableName, FieldNameCaseCamel),
FieldMap: fieldMap,
IsDo: false,
})
entityContent = generateEntityContent(
ctx,
in,
newTableName,
formatFieldName(newTableName, FieldNameCaseCamel),
structDefinition,
appendImports,
)
)
in.genItems.AppendGeneratedFilePath(entityFilePath)
err = gfile.PutContents(entityFilePath, strings.TrimSpace(entityContent))
if err != nil {
mlog.Fatalf("writing content to '%s' failed: %v", entityFilePath, err)
} else {
utils.GoFmt(entityFilePath)
mlog.Print("generated:", gfile.RealPath(entityFilePath))
}
}
}
func generateEntityContent(
ctx context.Context, in CGenDaoInternalInput, tableName, tableNameCamelCase, structDefine string, appendImports []string,
) string {
var (
tplContent = getTemplateFromPathOrDefault(
in.TplDaoEntityPath, consts.TemplateGenDaoEntityContent,
)
)
tplView.ClearAssigns()
tplView.Assigns(gview.Params{
tplVarTableName: tableName,
tplVarPackageImports: getImportPartContent(ctx, structDefine, false, appendImports),
tplVarTableNameCamelCase: tableNameCamelCase,
tplVarStructDefine: structDefine,
tplVarPackageName: filepath.Base(in.EntityPath),
})
assignDefaultVar(tplView, in)
entityContent, err := tplView.ParseContent(ctx, tplContent)
if err != nil {
mlog.Fatalf("parsing template content failed: %v", err)
}
return entityContent
}

View File

@ -0,0 +1,53 @@
// Copyright GoFrame gf 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 gendao
type (
CGenDaoInternalGenItems struct {
index int
Items []CGenDaoInternalGenItem
}
CGenDaoInternalGenItem struct {
Clear bool
StorageDirPaths []string
GeneratedFilePaths []string
}
)
func newCGenDaoInternalGenItems() *CGenDaoInternalGenItems {
return &CGenDaoInternalGenItems{
index: -1,
Items: make([]CGenDaoInternalGenItem, 0),
}
}
func (i *CGenDaoInternalGenItems) Scale() {
i.Items = append(i.Items, CGenDaoInternalGenItem{
StorageDirPaths: make([]string, 0),
GeneratedFilePaths: make([]string, 0),
Clear: false,
})
i.index++
}
func (i *CGenDaoInternalGenItems) SetClear(clear bool) {
i.Items[i.index].Clear = clear
}
func (i *CGenDaoInternalGenItems) AppendDirPath(storageDirPath string) {
i.Items[i.index].StorageDirPaths = append(
i.Items[i.index].StorageDirPaths,
storageDirPath,
)
}
func (i *CGenDaoInternalGenItems) AppendGeneratedFilePath(generatedFilePath string) {
i.Items[i.index].GeneratedFilePaths = append(
i.Items[i.index].GeneratedFilePaths,
generatedFilePath,
)
}

View File

@ -0,0 +1,230 @@
// Copyright GoFrame gf 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 gendao
import (
"bytes"
"context"
"fmt"
"strings"
"github.com/olekukonko/tablewriter"
"git.magicany.cc/black1552/gin-base/database"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
)
type generateStructDefinitionInput struct {
CGenDaoInternalInput
TableName string // Table name.
StructName string // Struct name.
FieldMap map[string]*database.TableField // Table field map.
IsDo bool // Is generating DTO struct.
}
func generateStructDefinition(ctx context.Context, in generateStructDefinitionInput) (string, []string) {
var appendImports []string
buffer := bytes.NewBuffer(nil)
array := make([][]string, len(in.FieldMap))
names := sortFieldKeyForDao(in.FieldMap)
for index, name := range names {
var imports string
field := in.FieldMap[name]
array[index], imports = generateStructFieldDefinition(ctx, field, in)
if imports != "" {
appendImports = append(appendImports, imports)
}
}
table := tablewriter.NewTable(buffer, twRenderer, twConfig)
table.Bulk(array)
table.Render()
stContent := buffer.String()
// Let's do this hack of table writer for indent!
stContent = gstr.Replace(stContent, " #", "")
stContent = gstr.Replace(stContent, "` ", "`")
stContent = gstr.Replace(stContent, "``", "")
buffer.Reset()
fmt.Fprintf(buffer, "type %s struct {\n", in.StructName)
if in.IsDo {
fmt.Fprintf(buffer, "g.Meta `orm:\"table:%s, do:true\"`\n", in.TableName)
}
buffer.WriteString(stContent)
buffer.WriteString("}")
return buffer.String(), appendImports
}
func getTypeMappingInfo(
ctx context.Context, fieldType string, inTypeMapping map[DBFieldTypeName]CustomAttributeType,
) (typeNameStr, importStr string) {
if typeMapping, ok := inTypeMapping[strings.ToLower(fieldType)]; ok {
typeNameStr = typeMapping.Type
importStr = typeMapping.Import
return
}
tryTypeMatch, _ := gregex.MatchString(`(.+?)\(([^\(\)]+)\)([\s\)]*)`, fieldType)
var (
tryTypeName string
moreTry bool
)
if len(tryTypeMatch) == 4 {
tryTypeMatch3, _ := gregex.ReplaceString(`\s+`, "", tryTypeMatch[3])
tryTypeName = gstr.Trim(tryTypeMatch[1]) + tryTypeMatch3
moreTry = tryTypeMatch3 != ""
} else {
tryTypeName = gstr.Split(fieldType, " ")[0]
}
if tryTypeName != "" {
if typeMapping, ok := inTypeMapping[strings.ToLower(tryTypeName)]; ok {
typeNameStr = typeMapping.Type
importStr = typeMapping.Import
} else if moreTry {
typeNameStr, importStr = getTypeMappingInfo(ctx, tryTypeName, inTypeMapping)
}
}
return
}
// generateStructFieldDefinition generates and returns the attribute definition for specified field.
func generateStructFieldDefinition(
ctx context.Context, field *database.TableField, in generateStructDefinitionInput,
) (attrLines []string, appendImport string) {
var (
err error
localTypeName database.LocalType
localTypeNameStr string
)
if in.TypeMapping != nil && len(in.TypeMapping) > 0 {
localTypeNameStr, appendImport = getTypeMappingInfo(ctx, field.Type, in.TypeMapping)
}
if localTypeNameStr == "" {
localTypeName, err = in.DB.CheckLocalTypeForField(ctx, field.Type, nil)
if err != nil {
panic(err)
}
localTypeNameStr = string(localTypeName)
switch localTypeName {
case database.LocalTypeDate, database.LocalTypeTime, database.LocalTypeDatetime:
if in.StdTime {
localTypeNameStr = "time.Time"
} else {
localTypeNameStr = "*gtime.Time"
}
case database.LocalTypeInt64Bytes:
localTypeNameStr = "int64"
case database.LocalTypeUint64Bytes:
localTypeNameStr = "uint64"
// Special type handle.
case database.LocalTypeJson, database.LocalTypeJsonb:
if in.GJsonSupport {
localTypeNameStr = "*gjson.Json"
} else {
localTypeNameStr = "string"
}
}
}
var (
tagKey = "`"
descriptionTag = gstr.Replace(formatComment(field.Comment), `"`, `\"`)
)
removeFieldPrefixArray := gstr.SplitAndTrim(in.RemoveFieldPrefix, ",")
newFiledName := field.Name
for _, v := range removeFieldPrefixArray {
newFiledName = gstr.TrimLeftStr(newFiledName, v, 1)
}
if in.FieldMapping != nil && len(in.FieldMapping) > 0 {
if typeMapping, ok := in.FieldMapping[fmt.Sprintf("%s.%s", in.TableName, newFiledName)]; ok {
localTypeNameStr = typeMapping.Type
appendImport = typeMapping.Import
}
}
attrLines = []string{
" #" + formatFieldName(newFiledName, FieldNameCaseCamel),
" #" + localTypeNameStr,
}
jsonTag := gstr.CaseConvert(newFiledName, gstr.CaseTypeMatch(in.JsonCase))
attrLines = append(attrLines, fmt.Sprintf(` #%sjson:"%s"`, tagKey, jsonTag))
// orm tag
if !in.IsDo {
// entity
attrLines = append(attrLines, fmt.Sprintf(` #orm:"%s"`, field.Name))
}
attrLines = append(attrLines, fmt.Sprintf(` #description:"%s"%s`, descriptionTag, tagKey))
attrLines = append(attrLines, fmt.Sprintf(` #// %s`, formatComment(field.Comment)))
for k, v := range attrLines {
if in.NoJsonTag {
v, _ = gregex.ReplaceString(`json:".+"`, ``, v)
}
if !in.DescriptionTag {
v, _ = gregex.ReplaceString(`description:".*"`, ``, v)
}
if in.NoModelComment {
v, _ = gregex.ReplaceString(`//.+`, ``, v)
}
attrLines[k] = v
}
return attrLines, appendImport
}
type FieldNameCase string
const (
FieldNameCaseCamel FieldNameCase = "CaseCamel"
FieldNameCaseCamelLower FieldNameCase = "CaseCamelLower"
)
// formatFieldName formats and returns a new field name that is used for golang codes generating.
func formatFieldName(fieldName string, nameCase FieldNameCase) string {
// For normal databases like mysql, pgsql, sqlite,
// field/table names of that are in normal case.
var newFieldName = fieldName
if isAllUpper(fieldName) {
// For special databases like dm, oracle,
// field/table names of that are in upper case.
newFieldName = strings.ToLower(fieldName)
}
switch nameCase {
case FieldNameCaseCamel:
return gstr.CaseCamel(newFieldName)
case FieldNameCaseCamelLower:
return gstr.CaseCamelLower(newFieldName)
default:
return ""
}
}
// isAllUpper checks and returns whether given `fieldName` all letters are upper case.
func isAllUpper(fieldName string) bool {
for _, b := range fieldName {
if b >= 'a' && b <= 'z' {
return false
}
}
return true
}
// formatComment formats the comment string to fit the golang code without any lines.
func formatComment(comment string) string {
comment = gstr.ReplaceByArray(comment, g.SliceStr{
"\n", " ",
"\r", " ",
})
comment = gstr.Replace(comment, `\n`, " ")
comment = gstr.Trim(comment)
return comment
}

147
cmd/gendao/gendao_table.go Normal file
View File

@ -0,0 +1,147 @@
// Copyright GoFrame gf 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 gendao
import (
"bytes"
"context"
"path/filepath"
"sort"
"strconv"
"strings"
"git.magicany.cc/black1552/gin-base/cmd/consts"
"git.magicany.cc/black1552/gin-base/database"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gview"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/mlog"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/utils"
)
// generateTable generates dao files for given tables.
func generateTable(ctx context.Context, in CGenDaoInternalInput) {
dirPathTable := gfile.Join(in.Path, in.TablePath)
if !in.GenTable {
if gfile.Exists(dirPathTable) {
in.genItems.AppendDirPath(dirPathTable)
}
return
}
in.genItems.AppendDirPath(dirPathTable)
for i := 0; i < len(in.TableNames); i++ {
var (
realTableName = in.TableNames[i]
newTableName = in.NewTableNames[i]
)
generateTableSingle(ctx, generateTableSingleInput{
CGenDaoInternalInput: in,
TableName: realTableName,
NewTableName: newTableName,
DirPathTable: dirPathTable,
})
}
}
// generateTableSingleInput is the input parameter for generateTableSingle.
type generateTableSingleInput struct {
CGenDaoInternalInput
// TableName specifies the table name of the table.
TableName string
// NewTableName specifies the prefix-stripped or custom edited name of the table.
NewTableName string
DirPathTable string
}
// generateTableSingle generates dao files for a single table.
func generateTableSingle(ctx context.Context, in generateTableSingleInput) {
// Generating table data preparing.
fieldMap, err := in.DB.TableFields(ctx, in.TableName)
if err != nil {
mlog.Fatalf(`fetching tables fields failed for table "%s": %+v`, in.TableName, err)
}
tableNameSnakeCase := gstr.CaseSnake(in.NewTableName)
fileName := gstr.Trim(tableNameSnakeCase, "-_.")
if len(fileName) > 5 && fileName[len(fileName)-5:] == "_test" {
// Add suffix to avoid the table name which contains "_test",
// which would make the go file a testing file.
fileName += "_table"
}
path := filepath.FromSlash(gfile.Join(in.DirPathTable, fileName+".go"))
in.genItems.AppendGeneratedFilePath(path)
if in.OverwriteDao || !gfile.Exists(path) {
var (
ctx = context.Background()
tplContent = getTemplateFromPathOrDefault(
in.TplDaoTablePath, consts.TemplateGenTableContent,
)
)
tplView.ClearAssigns()
tplView.Assigns(gview.Params{
tplVarGroupName: in.Group,
tplVarTableName: in.TableName,
tplVarTableNameCamelCase: formatFieldName(in.NewTableName, FieldNameCaseCamel),
tplVarPackageName: filepath.Base(in.TablePath),
tplVarTableFields: generateTableFields(fieldMap),
})
indexContent, err := tplView.ParseContent(ctx, tplContent)
if err != nil {
mlog.Fatalf("parsing template content failed: %v", err)
}
if err = gfile.PutContents(path, strings.TrimSpace(indexContent)); err != nil {
mlog.Fatalf("writing content to '%s' failed: %v", path, err)
} else {
utils.GoFmt(path)
mlog.Print("generated:", gfile.RealPath(path))
}
}
}
// generateTableFields generates and returns the field definition content for specified table.
func generateTableFields(fields map[string]*database.TableField) string {
var buf bytes.Buffer
fieldNames := make([]string, 0, len(fields))
for fieldName := range fields {
fieldNames = append(fieldNames, fieldName)
}
sort.Slice(fieldNames, func(i, j int) bool {
return fields[fieldNames[i]].Index < fields[fieldNames[j]].Index // asc
})
for index, fieldName := range fieldNames {
field := fields[fieldName]
buf.WriteString(" " + strconv.Quote(field.Name) + ": {\n")
buf.WriteString(" Index: " + gconv.String(field.Index) + ",\n")
buf.WriteString(" Name: " + strconv.Quote(field.Name) + ",\n")
buf.WriteString(" Type: " + strconv.Quote(field.Type) + ",\n")
buf.WriteString(" Null: " + gconv.String(field.Null) + ",\n")
buf.WriteString(" Key: " + strconv.Quote(field.Key) + ",\n")
buf.WriteString(" Default: " + generateDefaultValue(field.Default) + ",\n")
buf.WriteString(" Extra: " + strconv.Quote(field.Extra) + ",\n")
buf.WriteString(" Comment: " + strconv.Quote(field.Comment) + ",\n")
buf.WriteString(" },")
if index != len(fieldNames)-1 {
buf.WriteString("\n")
}
}
return buf.String()
}
// generateDefaultValue generates and returns the default value definition for specified field.
func generateDefaultValue(value interface{}) string {
if value == nil {
return "nil"
}
switch v := value.(type) {
case string:
return strconv.Quote(v)
default:
return gconv.String(v)
}
}

155
cmd/gendao/gendao_tag.go Normal file
View File

@ -0,0 +1,155 @@
// Copyright GoFrame gf 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 gendao
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gtag"
)
const (
CGenDaoConfig = `gfcli.gen.dao`
CGenDaoUsage = `gf gen dao [OPTION]`
CGenDaoBrief = `automatically generate go files for dao/do/entity`
CGenDaoEg = `
gf gen dao
gf gen dao -l "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
gf gen dao -p ./model -g user-center -t user,user_detail,user_login
gf gen dao -r user_
`
CGenDaoAd = `
CONFIGURATION SUPPORT
Options are also supported by configuration file.
It's suggested using configuration file instead of command line arguments making producing.
The configuration node name is "gfcli.gen.dao", which also supports multiple databases, for example(config.yaml):
gfcli:
gen:
dao:
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/test"
tables: "order,products"
jsonCase: "CamelLower"
- link: "mysql:root:12345678@tcp(127.0.0.1:3306)/primary"
path: "./my-app"
prefix: "primary_"
tables: "user, userDetail"
typeMapping:
decimal:
type: decimal.Decimal
import: github.com/shopspring/decimal
numeric:
type: string
fieldMapping:
table_name.field_name:
type: decimal.Decimal
import: github.com/shopspring/decimal
`
CGenDaoBriefPath = `directory path for generated files`
CGenDaoBriefLink = `database configuration, the same as the ORM configuration of GoFrame`
CGenDaoBriefTables = `generate models only for given tables, multiple table names separated with ','`
CGenDaoBriefTablesEx = `generate models excluding given tables, multiple table names separated with ','`
CGenDaoBriefPrefix = `add prefix for all table of specified link/database tables`
CGenDaoBriefRemovePrefix = `remove specified prefix of the table, multiple prefix separated with ','`
CGenDaoBriefRemoveFieldPrefix = `remove specified prefix of the field, multiple prefix separated with ','`
CGenDaoBriefStdTime = `use time.Time from stdlib instead of gtime.Time for generated time/date fields of tables`
CGenDaoBriefWithTime = `add created time for auto produced go files`
CGenDaoBriefGJsonSupport = `use gJsonSupport to use *gjson.Json instead of string for generated json fields of tables`
CGenDaoBriefImportPrefix = `custom import prefix for generated go files`
CGenDaoBriefDaoPath = `directory path for storing generated dao files under path`
CGenDaoBriefTablePath = `directory path for storing generated table files under path`
CGenDaoBriefDoPath = `directory path for storing generated do files under path`
CGenDaoBriefEntityPath = `directory path for storing generated entity files under path`
CGenDaoBriefOverwriteDao = `overwrite all dao files both inside/outside internal folder`
CGenDaoBriefModelFile = `custom file name for storing generated model content`
CGenDaoBriefModelFileForDao = `custom file name generating model for DAO operations like Where/Data. It's empty in default`
CGenDaoBriefDescriptionTag = `add comment to description tag for each field`
CGenDaoBriefNoJsonTag = `no json tag will be added for each field`
CGenDaoBriefNoModelComment = `no model comment will be added for each field`
CGenDaoBriefClear = `delete all generated go files that do not exist in database`
CGenDaoBriefGenTable = `generate table files`
CGenDaoBriefTypeMapping = `custom local type mapping for generated struct attributes relevant to fields of table`
CGenDaoBriefFieldMapping = `custom local type mapping for generated struct attributes relevant to specific fields of table`
CGenDaoBriefShardingPattern = `sharding pattern for table name, e.g. "users_?" will be replace tables "users_001,users_002,..." to "users" dao`
CGenDaoBriefGroup = `
specifying the configuration group name of database for generated ORM instance,
it's not necessary and the default value is "default"
`
CGenDaoBriefJsonCase = `
generated json tag case for model struct, cases are as follows:
| Case | Example |
|---------------- |--------------------|
| Camel | AnyKindOfString |
| CamelLower | anyKindOfString | default
| Snake | any_kind_of_string |
| SnakeScreaming | ANY_KIND_OF_STRING |
| SnakeFirstUpper | rgb_code_md5 |
| Kebab | any-kind-of-string |
| KebabScreaming | ANY-KIND-OF-STRING |
`
CGenDaoBriefTplDaoIndexPath = `template file path for dao index file`
CGenDaoBriefTplDaoInternalPath = `template file path for dao internal file`
CGenDaoBriefTplDaoDoPathPath = `template file path for dao do file`
CGenDaoBriefTplDaoEntityPath = `template file path for dao entity file`
tplVarTableName = `TplTableName`
tplVarTableNameCamelCase = `TplTableNameCamelCase`
tplVarTableNameCamelLowerCase = `TplTableNameCamelLowerCase`
tplVarTableSharding = `TplTableSharding`
tplVarTableShardingPrefix = `TplTableShardingPrefix`
tplVarTableFields = `TplTableFields`
tplVarPackageImports = `TplPackageImports`
tplVarImportPrefix = `TplImportPrefix`
tplVarStructDefine = `TplStructDefine`
tplVarColumnDefine = `TplColumnDefine`
tplVarColumnNames = `TplColumnNames`
tplVarGroupName = `TplGroupName`
tplVarDatetimeStr = `TplDatetimeStr`
tplVarCreatedAtDatetimeStr = `TplCreatedAtDatetimeStr`
tplVarPackageName = `TplPackageName`
)
func init() {
gtag.Sets(g.MapStrStr{
`CGenDaoConfig`: CGenDaoConfig,
`CGenDaoUsage`: CGenDaoUsage,
`CGenDaoBrief`: CGenDaoBrief,
`CGenDaoEg`: CGenDaoEg,
`CGenDaoAd`: CGenDaoAd,
`CGenDaoBriefPath`: CGenDaoBriefPath,
`CGenDaoBriefLink`: CGenDaoBriefLink,
`CGenDaoBriefTables`: CGenDaoBriefTables,
`CGenDaoBriefTablesEx`: CGenDaoBriefTablesEx,
`CGenDaoBriefPrefix`: CGenDaoBriefPrefix,
`CGenDaoBriefRemovePrefix`: CGenDaoBriefRemovePrefix,
`CGenDaoBriefRemoveFieldPrefix`: CGenDaoBriefRemoveFieldPrefix,
`CGenDaoBriefStdTime`: CGenDaoBriefStdTime,
`CGenDaoBriefWithTime`: CGenDaoBriefWithTime,
`CGenDaoBriefDaoPath`: CGenDaoBriefDaoPath,
`CGenDaoBriefTablePath`: CGenDaoBriefTablePath,
`CGenDaoBriefDoPath`: CGenDaoBriefDoPath,
`CGenDaoBriefEntityPath`: CGenDaoBriefEntityPath,
`CGenDaoBriefGJsonSupport`: CGenDaoBriefGJsonSupport,
`CGenDaoBriefImportPrefix`: CGenDaoBriefImportPrefix,
`CGenDaoBriefOverwriteDao`: CGenDaoBriefOverwriteDao,
`CGenDaoBriefModelFile`: CGenDaoBriefModelFile,
`CGenDaoBriefModelFileForDao`: CGenDaoBriefModelFileForDao,
`CGenDaoBriefDescriptionTag`: CGenDaoBriefDescriptionTag,
`CGenDaoBriefNoJsonTag`: CGenDaoBriefNoJsonTag,
`CGenDaoBriefNoModelComment`: CGenDaoBriefNoModelComment,
`CGenDaoBriefClear`: CGenDaoBriefClear,
`CGenDaoBriefGenTable`: CGenDaoBriefGenTable,
`CGenDaoBriefTypeMapping`: CGenDaoBriefTypeMapping,
`CGenDaoBriefFieldMapping`: CGenDaoBriefFieldMapping,
`CGenDaoBriefShardingPattern`: CGenDaoBriefShardingPattern,
`CGenDaoBriefGroup`: CGenDaoBriefGroup,
`CGenDaoBriefJsonCase`: CGenDaoBriefJsonCase,
`CGenDaoBriefTplDaoIndexPath`: CGenDaoBriefTplDaoIndexPath,
`CGenDaoBriefTplDaoInternalPath`: CGenDaoBriefTplDaoInternalPath,
`CGenDaoBriefTplDaoDoPathPath`: CGenDaoBriefTplDaoDoPathPath,
`CGenDaoBriefTplDaoEntityPath`: CGenDaoBriefTplDaoEntityPath,
})
}

View File

@ -0,0 +1,7 @@
package consts
const (
// DoNotEditKey is used in generated files,
// which marks the files will be overwritten by CLI tool.
DoNotEditKey = `DO NOT EDIT`
)

View File

@ -0,0 +1,70 @@
package mlog
import (
"context"
"github.com/gogf/gf/v2/os/gcmd"
"github.com/gogf/gf/v2/os/genv"
"github.com/gogf/gf/v2/os/glog"
)
const (
headerPrintEnvName = "GF_CLI_MLOG_HEADER"
)
var (
ctx = context.TODO()
logger = glog.New()
)
func init() {
if genv.Get(headerPrintEnvName).String() == "1" {
logger.SetHeaderPrint(true)
} else {
logger.SetHeaderPrint(false)
}
if gcmd.GetOpt("debug") != nil || gcmd.GetOpt("gf.debug") != nil {
logger.SetHeaderPrint(true)
logger.SetStackSkip(4)
logger.SetFlags(logger.GetFlags() | glog.F_FILE_LONG)
logger.SetDebug(true)
} else {
logger.SetStack(false)
logger.SetDebug(false)
}
}
// SetHeaderPrint enables/disables header printing to stdout.
func SetHeaderPrint(enabled bool) {
logger.SetHeaderPrint(enabled)
if enabled {
_ = genv.Set(headerPrintEnvName, "1")
} else {
_ = genv.Set(headerPrintEnvName, "0")
}
}
func Print(v ...any) {
logger.Print(ctx, v...)
}
func Printf(format string, v ...any) {
logger.Printf(ctx, format, v...)
}
func Fatal(v ...any) {
logger.Fatal(ctx, v...)
}
func Fatalf(format string, v ...any) {
logger.Fatalf(ctx, format, v...)
}
func Debug(v ...any) {
logger.Debug(ctx, v...)
}
func Debugf(format string, v ...any) {
logger.Debugf(ctx, format, v...)
}

View File

@ -0,0 +1,129 @@
package utils
import (
"context"
"fmt"
"golang.org/x/tools/imports"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gproc"
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/consts"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/mlog"
)
// GoFmt formats the source file and adds or removes import statements as necessary.
func GoFmt(path string) {
replaceFunc := func(path, content string) string {
res, err := imports.Process(path, []byte(content), nil)
if err != nil {
mlog.Printf(`error format "%s" go files: %v`, path, err)
return content
}
return string(res)
}
var err error
if gfile.IsFile(path) {
// File format.
if gfile.ExtName(path) != "go" {
return
}
err = gfile.ReplaceFileFunc(replaceFunc, path)
} else {
// Folder format.
err = gfile.ReplaceDirFunc(replaceFunc, path, "*.go", true)
}
if err != nil {
mlog.Printf(`error format "%s" go files: %v`, path, err)
}
}
// GoModTidy executes `go mod tidy` at specified directory `dirPath`.
func GoModTidy(ctx context.Context, dirPath string) error {
command := fmt.Sprintf(`cd %s && go mod tidy`, dirPath)
err := gproc.ShellRun(ctx, command)
return err
}
// IsFileDoNotEdit checks and returns whether file contains `do not edit` key.
func IsFileDoNotEdit(filePath string) bool {
if !gfile.Exists(filePath) {
return true
}
return gstr.Contains(gfile.GetContents(filePath), consts.DoNotEditKey)
}
// ReplaceGeneratedContentGFV2 replaces generated go content from goframe v1 to v2.
func ReplaceGeneratedContentGFV2(folderPath string) (err error) {
return gfile.ReplaceDirFunc(func(path, content string) string {
if gstr.Contains(content, `"github.com/gogf/gf`) && !gstr.Contains(content, `"github.com/gogf/gf/v2`) {
content = gstr.Replace(content, `"github.com/gogf/gf"`, `"github.com/gogf/gf/v2"`)
content = gstr.Replace(content, `"github.com/gogf/gf/`, `"github.com/gogf/gf/v2/`)
content = gstr.Replace(content, `"github.com/gogf/gf/v2/contrib/`, `"github.com/gogf/gf/contrib/`)
return content
}
return content
}, folderPath, "*.go", true)
}
// GetImportPath calculates and returns the golang import path for given `dirPath`.
// Note that it needs a `go.mod` in current working directory or parent directories to detect the path.
func GetImportPath(dirPath string) string {
// If `filePath` does not exist, create it firstly to find the import path.
var realPath = gfile.RealPath(dirPath)
if realPath == "" {
_ = gfile.Mkdir(dirPath)
realPath = gfile.RealPath(dirPath)
}
var (
newDir = gfile.Dir(realPath)
oldDir string
suffix = gfile.Basename(dirPath)
goModName = "go.mod"
goModPath string
importPath string
)
for {
goModPath = gfile.Join(newDir, goModName)
if gfile.Exists(goModPath) {
match, _ := gregex.MatchString(`^module\s+(.+)\s*`, gfile.GetContents(goModPath))
importPath = gstr.Trim(match[1]) + "/" + suffix
importPath = gstr.Replace(importPath, `\`, `/`)
importPath = gstr.TrimRight(importPath, `/`)
return importPath
}
oldDir = newDir
newDir = gfile.Dir(oldDir)
if newDir == oldDir {
return ""
}
suffix = gfile.Basename(oldDir) + "/" + suffix
}
}
// GetModPath retrieves and returns the file path of go.mod for current project.
func GetModPath() string {
var (
oldDir = gfile.Pwd()
newDir = oldDir
goModName = "go.mod"
goModPath string
)
for {
goModPath = gfile.Join(newDir, goModName)
if gfile.Exists(goModPath) {
return goModPath
}
newDir = gfile.Dir(oldDir)
if newDir == oldDir {
break
}
oldDir = newDir
}
return ""
}

View File

@ -0,0 +1,55 @@
// Copyright GoFrame gf 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 utils
import (
"io"
"net/http"
"os"
"time"
"github.com/schollz/progressbar/v3"
"github.com/gogf/gf/v2/errors/gerror"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/mlog"
)
// HTTPDownloadFileWithPercent downloads target url file to local path with percent process printing.
func HTTPDownloadFileWithPercent(url string, localSaveFilePath string) error {
start := time.Now()
out, err := os.Create(localSaveFilePath)
if err != nil {
return gerror.Wrapf(err, `download "%s" to "%s" failed`, url, localSaveFilePath)
}
defer out.Close()
headResp, err := http.Head(url)
if err != nil {
return gerror.Wrapf(err, `download "%s" to "%s" failed`, url, localSaveFilePath)
}
defer headResp.Body.Close()
resp, err := http.Get(url)
if err != nil {
return gerror.Wrapf(err, `download "%s" to "%s" failed`, url, localSaveFilePath)
}
defer resp.Body.Close()
bar := progressbar.NewOptions(int(resp.ContentLength), progressbar.OptionShowBytes(true), progressbar.OptionShowCount())
writer := io.MultiWriter(out, bar)
_, err = io.Copy(writer, resp.Body)
elapsed := time.Since(start)
if elapsed > time.Minute {
mlog.Printf(`download completed in %.0fm`, float64(elapsed)/float64(time.Minute))
} else {
mlog.Printf(`download completed in %.0fs`, elapsed.Seconds())
}
return nil
}

View File

@ -0,0 +1,23 @@
// Copyright GoFrame gf 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 utils_test
import (
"fmt"
"testing"
"github.com/gogf/gf/v2/test/gtest"
"git.magicany.cc/black1552/gin-base/cmd/gendao/internal/utility/utils"
)
func Test_GetModPath(t *testing.T) {
gtest.C(t, func(t *gtest.T) {
goModPath := utils.GetModPath()
fmt.Println(goModPath)
})
}

159
cmd/main.go Normal file
View File

@ -0,0 +1,159 @@
package main
import (
"context"
"fmt"
"os"
"git.magicany.cc/black1552/gin-base/cmd/gendao"
"git.magicany.cc/black1552/gin-base/config"
_ "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")
user := getStringValue(defaultDbConfig, "user", "root")
pass := getStringValue(defaultDbConfig, "pass", "")
dbType := getStringValue(defaultDbConfig, "type", "mysql")
link := getStringValue(defaultDbConfig, "link", "")
fmt.Println("=== Gin-Base DAO 代码生成工具 ===")
fmt.Printf("🔧 类型: %s\n", dbType)
// 构建数据库连接字符串
var connectionInfo string
if link == "" {
// 如果没有配置 link则根据数据库类型构建
switch dbType {
case "sqlite":
// SQLite 使用配置文件中的 link 或默认路径
connectionInfo = fmt.Sprintf("📁 数据库文件: %s", link)
case "mysql", "mariadb":
link = fmt.Sprintf("mysql:%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true&loc=Local",
user, pass, host, port, name,
)
connectionInfo = fmt.Sprintf("📊 数据库: %s\n🌐 主机: %s:%s", name, host, port)
case "pgsql", "postgresql":
link = fmt.Sprintf("pgsql:%s:%s@tcp(%s:%s)/%s?sslmode=disable",
user, pass, host, port, name,
)
connectionInfo = fmt.Sprintf("📊 数据库: %s\n🌐 主机: %s:%s", name, host, port)
case "mssql":
link = fmt.Sprintf("mssql:%s:%s@tcp(%s:%s)/%s",
user, pass, host, port, name,
)
connectionInfo = fmt.Sprintf("📊 数据库: %s\n🌐 主机: %s:%s", name, host, port)
case "oracle":
link = fmt.Sprintf("oracle:%s:%s@%s:%s/%s",
user, pass, host, port, name,
)
connectionInfo = fmt.Sprintf("📊 数据库: %s\n🌐 主机: %s:%s", name, host, port)
case "clickhouse":
link = fmt.Sprintf("clickhouse:%s:%s@tcp(%s:%s)/%s",
user, pass, host, port, name,
)
connectionInfo = fmt.Sprintf("📊 数据库: %s\n🌐 主机: %s:%s", name, host, port)
default:
fmt.Printf("⚠️ 警告: 未知的数据库类型 %s尝试使用配置的 link\n", dbType)
if link == "" {
fmt.Println("❌ 错误: 未配置数据库连接信息")
os.Exit(1)
}
connectionInfo = "🔗 使用自定义连接"
}
} else {
// 使用配置文件中直接提供的 link
connectionInfo = fmt.Sprintf("🔗 连接: %s", link)
}
fmt.Println(connectionInfo)
fmt.Println()
// 准备表名参数
tablesArg := ""
if len(os.Args) > 1 {
tablesArg = joinStrings(os.Args[1:], ",")
fmt.Printf("📋 指定生成表: %s\n\n", tablesArg)
} else {
fmt.Println("💡 提示: 可以通过命令行参数指定要生成的表")
fmt.Println(" 例如: gin-dao-gen users orders\n")
}
// 调用 gendao 的 Dao 函数生成代码
genDao := gendao.CGenDao{}
input := gendao.CGenDaoInput{
Link: link,
Tables: tablesArg,
Path: "./",
DaoPath: "dao",
DoPath: "model/do",
EntityPath: "model/entity",
TablePath: "model/table",
Group: "default",
JsonCase: "CamelLower",
DescriptionTag: true,
GenTable: true,
}
fmt.Println("🔨 开始生成代码...")
_, err := genDao.Dao(ctx, input)
if err != nil {
fmt.Printf("\n❌ 代码生成失败: %v\n", err)
os.Exit(1)
}
fmt.Println("\n🎉 代码生成完成!")
fmt.Println("📁 生成的文件位于:")
fmt.Println(" - ./dao/")
fmt.Println(" - ./model/do/")
fmt.Println(" - ./model/entity/")
fmt.Println(" - ./model/table/")
}
// 辅助函数:从 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
}
// 辅助函数:连接字符串数组
func joinStrings(strs []string, sep string) string {
if len(strs) == 0 {
return ""
}
result := strs[0]
for i := 1; i < len(strs); i++ {
result += sep + strs[i]
}
return result
}

60
database/consts/consts.go Normal file
View File

@ -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.
)

View File

@ -0,0 +1,73 @@
// 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 implements database.Driver, which supports operations for database ClickHouse.
package clickhouse
import (
"context"
"errors"
"git.magicany.cc/black1552/gin-base/database"
"github.com/gogf/gf/v2/os/gctx"
)
// 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 (
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 {
panic(err)
}
}
// 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.
// 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
}
func (d *Driver) injectNeedParsedSql(ctx context.Context) context.Context {
if ctx.Value(needParsedSqlInCtx) != nil {
return ctx
}
return context.WithValue(ctx, needParsedSqlInCtx, true)
}
// Migration returns a Migration instance for ClickHouse database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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...)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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...)
}

View File

@ -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
}

View File

@ -0,0 +1,321 @@
// 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"
)
// Migration implements database migration operations for ClickHouse.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new ClickHouse Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
if opts.IfNotExists {
sql.WriteString("IF NOT EXISTS ")
}
sql.WriteString(database.QuoteIdentifier(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
for name, def := range columns {
colDef := m.buildColumnDefinition(name, def)
colDefs = append(colDefs, " "+colDef)
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
// Add engine specification (required for ClickHouse)
sql.WriteString(" ENGINE = MergeTree()")
if opts.Comment != "" {
sql.WriteString(fmt.Sprintf(" COMMENT '%s'", escapeString(opts.Comment)))
}
return m.ExecuteSQL(ctx, sql.String())
}
// buildColumnDefinition builds column definition for ClickHouse.
func (m *Migration) buildColumnDefinition(name string, def *database.ColumnDefinition) string {
var parts []string
parts = append(parts, database.QuoteIdentifier(name))
// Handle ClickHouse-specific types
dbType := def.Type
parts = append(parts, dbType)
if !def.Null {
// ClickHouse uses Nullable type wrapper
if !strings.HasPrefix(dbType, "Nullable(") {
// Type is already non-nullable by default in ClickHouse
}
} else {
// Make type nullable if needed
if !strings.HasPrefix(dbType, "Nullable(") {
parts[len(parts)-1] = fmt.Sprintf("Nullable(%s)", dbType)
}
}
if def.Default != nil {
defaultValue := formatDefaultValue(def.Default)
parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue))
}
if def.Comment != "" {
parts = append(parts, fmt.Sprintf("COMMENT '%s'", escapeString(def.Comment)))
}
return strings.Join(parts, " ")
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
sql := "DROP TABLE "
if len(ifExists) > 0 && ifExists[0] {
sql += "IF EXISTS "
}
sql += database.QuoteIdentifier(table)
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "currentDatabase()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM system.tables WHERE database = %s AND name = '%s'",
schema,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"RENAME TABLE %s TO %s",
database.QuoteIdentifier(oldName),
database.QuoteIdentifier(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME COLUMN %s TO %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(oldName),
database.QuoteIdentifier(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
// Note: ClickHouse has limited index support compared to traditional databases.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
// ClickHouse uses different indexing mechanisms
// For now, we'll use ALTER TABLE ADD INDEX syntax
colList := m.BuildIndexColumns(columns)
sql := fmt.Sprintf(
"ALTER TABLE %s ADD INDEX %s (%s) TYPE minmax GRANULARITY 1",
database.QuoteIdentifier(table),
database.QuoteIdentifier(index),
colList,
)
return m.ExecuteSQL(ctx, sql)
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP INDEX %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(index),
)
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "currentDatabase()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM system.data_skipping_indices WHERE database = %s AND table = '%s' AND name = '%s'",
schema,
table,
index,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey is not supported in ClickHouse.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
// ClickHouse does not support foreign keys
return fmt.Errorf("ClickHouse does not support foreign key constraints")
}
// DropForeignKey is not supported in ClickHouse.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
// ClickHouse does not support foreign keys
return fmt.Errorf("ClickHouse does not support foreign key constraints")
}
// HasForeignKey always returns false as ClickHouse doesn't support foreign keys.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
return false, nil
}
// CreateSchema creates a new database schema.
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
sql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema.
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := fmt.Sprintf("DROP DATABASE IF EXISTS %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM system.databases WHERE name = '%s'",
schema,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// formatDefaultValue formats the default value for SQL.
func formatDefaultValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
case float32, float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "1"
}
return "0"
case nil:
return "NULL"
default:
return fmt.Sprintf("'%v'", v)
}
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "\\'")
return s
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -4,15 +4,27 @@
// If a copy of the MIT was not distributed with this file, // If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf. // You can obtain one at https://github.com/gogf/gf.
// Package drivers provides database drivers registration for GF ORM. // Package drivers provides database drivers registration for our custom ORM.
// This package automatically registers common database drivers when imported. // This package automatically registers all supported database drivers when imported.
// Supported databases: MySQL, MariaDB, PostgreSQL, SQL Server, Oracle, SQLite, ClickHouse
package drivers package drivers
import ( import (
_ "github.com/gogf/gf/contrib/drivers/clickhouse/v2" // MySQL & MariaDB driver
_ "github.com/gogf/gf/contrib/drivers/mssql/v2" _ "git.magicany.cc/black1552/gin-base/database/drivers/mysql"
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
_ "github.com/gogf/gf/contrib/drivers/oracle/v2" // PostgreSQL driver
_ "github.com/gogf/gf/contrib/drivers/pgsql/v2" _ "git.magicany.cc/black1552/gin-base/database/drivers/pgsql"
_ "github.com/gogf/gf/contrib/drivers/sqlite/v2"
// SQL Server driver
_ "git.magicany.cc/black1552/gin-base/database/drivers/mssql"
// Oracle driver
_ "git.magicany.cc/black1552/gin-base/database/drivers/oracle"
// SQLite driver (pure Go implementation, no CGO required)
_ "git.magicany.cc/black1552/gin-base/database/drivers/sqlite"
// ClickHouse driver
_ "git.magicany.cc/black1552/gin-base/database/drivers/clickhouse"
) )

View File

@ -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 mssql implements database.Driver, which supports operations for MSSQL.
package mssql
import (
_ "github.com/microsoft/go-mssqldb"
"git.magicany.cc/black1552/gin-base/database"
)
// Driver is the driver for SQL server database.
type Driver struct {
*database.Core
}
const (
rowNumberAliasForSelect = `ROW_NUMBER__`
quoteChar = `"`
)
func init() {
if err := database.Register(`mssql`, New()); err != nil {
panic(err)
}
}
// 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.
// 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
}
// Migration returns a Migration instance for MSSQL database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}

View File

@ -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
}

View File

@ -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(?)
}

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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)
}

View File

@ -0,0 +1,340 @@
// 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"
"strings"
"git.magicany.cc/black1552/gin-base/database"
)
// Migration implements database migration operations for SQL Server.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new SQL Server Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
sql.WriteString(database.QuoteIdentifier(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
var primaryKeys []string
for name, def := range columns {
colDef := m.buildColumnDefinition(name, def)
if def.PrimaryKey {
primaryKeys = append(primaryKeys, database.QuoteIdentifier(name))
}
colDefs = append(colDefs, " "+colDef)
}
// Add primary key constraint if needed
if len(primaryKeys) > 0 {
colDefs = append(colDefs, fmt.Sprintf(" PRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")))
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
return m.ExecuteSQL(ctx, sql.String())
}
// buildColumnDefinition builds column definition for SQL Server.
func (m *Migration) buildColumnDefinition(name string, def *database.ColumnDefinition) string {
var parts []string
parts = append(parts, database.QuoteIdentifier(name))
// Handle SQL Server-specific types
dbType := def.Type
if def.AutoIncrement {
if dbType == "INT" || dbType == "INTEGER" {
dbType = "INT IDENTITY(1,1)"
} else if dbType == "BIGINT" {
dbType = "BIGINT IDENTITY(1,1)"
}
}
parts = append(parts, dbType)
if !def.Null {
parts = append(parts, "NOT NULL")
} else {
parts = append(parts, "NULL")
}
if def.Default != nil {
defaultValue := formatDefaultValue(def.Default)
parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue))
}
return strings.Join(parts, " ")
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
if len(ifExists) > 0 && ifExists[0] {
sql := fmt.Sprintf(
"IF OBJECT_ID('%s', 'U') IS NOT NULL DROP TABLE %s",
table,
database.QuoteIdentifier(table),
)
return m.ExecuteSQL(ctx, sql)
}
sql := fmt.Sprintf("DROP TABLE %s", database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '%s'",
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"EXEC sp_rename '%s', '%s'",
oldName,
newName,
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
sql := fmt.Sprintf(
"EXEC sp_rename '%s.%s', '%s', 'COLUMN'",
table,
oldName,
newName,
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ALTER COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE ")
if opts.Unique {
sql.WriteString("UNIQUE ")
}
sql.WriteString("INDEX ")
sql.WriteString(database.QuoteIdentifier(index))
sql.WriteString(" ON ")
sql.WriteString(database.QuoteIdentifier(table))
colList := m.BuildIndexColumns(columns)
sql.WriteString(fmt.Sprintf(" (%s)", colList))
return m.ExecuteSQL(ctx, sql.String())
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf("DROP INDEX %s ON %s", database.QuoteIdentifier(index), database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM sys.indexes WHERE name = '%s' AND object_id = OBJECT_ID('%s')",
index,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey creates a foreign key constraint.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
var opts database.ForeignKeyOptions
for _, opt := range options {
opt(&opts)
}
sql := fmt.Sprintf(
"ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY %s REFERENCES %s %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(constraint),
m.BuildForeignKeyColumns(columns),
database.QuoteIdentifier(refTable),
m.BuildForeignKeyColumns(refColumns),
)
if opts.OnDelete != "" {
sql += fmt.Sprintf(" ON DELETE %s", opts.OnDelete)
}
if opts.OnUpdate != "" {
sql += fmt.Sprintf(" ON UPDATE %s", opts.OnUpdate)
}
return m.ExecuteSQL(ctx, sql)
}
// DropForeignKey drops a foreign key constraint.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP CONSTRAINT %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(constraint),
)
return m.ExecuteSQL(ctx, sql)
}
// HasForeignKey checks if a foreign key constraint exists.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE CONSTRAINT_NAME = '%s' AND TABLE_NAME = '%s' AND CONSTRAINT_TYPE = 'FOREIGN KEY'",
constraint,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateSchema creates a new database schema.
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
sql := fmt.Sprintf("CREATE SCHEMA %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema.
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := fmt.Sprintf("DROP SCHEMA %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = '%s'",
schema,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// formatDefaultValue formats the default value for SQL.
func formatDefaultValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
case float32, float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "1"
}
return "0"
case nil:
return "NULL"
default:
return fmt.Sprintf("'%v'", v)
}
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "''")
return s
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -0,0 +1,64 @@
// 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 implements database.Driver, which supports operations for database MySQL.
package mysql
import (
"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.
type Driver struct {
*database.Core
}
const (
quoteChar = "`"
)
func init() {
var (
err error
driverObj = New()
driverNames = consts.SliceStr{"mysql", "mariadb", "tidb"} // TODO remove mariadb and tidb in future versions.
)
for _, driverName := range driverNames {
if err = database.Register(driverName, driverObj); err != nil {
panic(err)
}
}
}
// 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.
// 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
}
// Migration returns a Migration instance for MySQL database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}

View File

@ -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)
}

View File

@ -0,0 +1,365 @@
// 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"
)
// Migration implements database migration operations for MySQL.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new MySQL Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
if opts.IfNotExists {
sql.WriteString("IF NOT EXISTS ")
}
sql.WriteString(database.QuoteIdentifier(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
var primaryKeys []string
for name, def := range columns {
colDef := m.BuildColumnDefinition(name, def)
if def.PrimaryKey {
primaryKeys = append(primaryKeys, database.QuoteIdentifier(name))
}
colDefs = append(colDefs, " "+colDef)
}
// Add primary key constraint if needed
if len(primaryKeys) > 0 {
colDefs = append(colDefs, fmt.Sprintf(" PRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")))
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
// Add table options
if opts.Engine != "" {
sql.WriteString(fmt.Sprintf(" ENGINE=%s", opts.Engine))
}
if opts.Charset != "" {
sql.WriteString(fmt.Sprintf(" DEFAULT CHARSET=%s", opts.Charset))
}
if opts.Collation != "" {
sql.WriteString(fmt.Sprintf(" COLLATE=%s", opts.Collation))
}
if opts.Comment != "" {
sql.WriteString(fmt.Sprintf(" COMMENT='%s'", escapeString(opts.Comment)))
}
return m.ExecuteSQL(ctx, sql.String())
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
sql := "DROP TABLE "
if len(ifExists) > 0 && ifExists[0] {
sql += "IF EXISTS "
}
sql += database.QuoteIdentifier(table)
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "DATABASE()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = '%s'",
schema,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"RENAME TABLE %s TO %s",
database.QuoteIdentifier(oldName),
database.QuoteIdentifier(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifier(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.BuildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
// MySQL requires the full column definition when renaming
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return err
}
field, ok := fields[oldName]
if !ok {
return fmt.Errorf("column %s does not exist in table %s", oldName, table)
}
def := &database.ColumnDefinition{
Type: field.Type,
Null: field.Null,
Default: field.Default,
Comment: field.Comment,
AutoIncrement: strings.Contains(field.Extra, "auto_increment"),
}
colDef := m.BuildColumnDefinition(newName, def)
sql := fmt.Sprintf(
"ALTER TABLE %s CHANGE COLUMN %s %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(oldName),
colDef,
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.BuildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s MODIFY COLUMN %s", database.QuoteIdentifier(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE ")
if opts.Unique {
sql.WriteString("UNIQUE ")
}
if opts.FullText {
sql.WriteString("FULLTEXT ")
}
if opts.Spatial {
sql.WriteString("SPATIAL ")
}
sql.WriteString("INDEX ")
sql.WriteString(database.QuoteIdentifier(index))
sql.WriteString(" ON ")
sql.WriteString(database.QuoteIdentifier(table))
colList := m.BuildIndexColumns(columns)
sql.WriteString(fmt.Sprintf(" (%s)", colList))
if opts.Using != "" {
sql.WriteString(fmt.Sprintf(" USING %s", opts.Using))
}
return m.ExecuteSQL(ctx, sql.String())
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf(
"DROP INDEX %s ON %s",
database.QuoteIdentifier(index),
database.QuoteIdentifier(table),
)
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "DATABASE()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = %s AND table_name = '%s' AND index_name = '%s'",
schema,
table,
index,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey creates a foreign key constraint.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
var opts database.ForeignKeyOptions
for _, opt := range options {
opt(&opts)
}
sql := fmt.Sprintf(
"ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY %s REFERENCES %s %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(constraint),
m.BuildForeignKeyColumns(columns),
database.QuoteIdentifier(refTable),
m.BuildForeignKeyColumns(refColumns),
)
if opts.OnDelete != "" {
sql += fmt.Sprintf(" ON DELETE %s", opts.OnDelete)
}
if opts.OnUpdate != "" {
sql += fmt.Sprintf(" ON UPDATE %s", opts.OnUpdate)
}
return m.ExecuteSQL(ctx, sql)
}
// DropForeignKey drops a foreign key constraint.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP FOREIGN KEY %s",
database.QuoteIdentifier(table),
database.QuoteIdentifier(constraint),
)
return m.ExecuteSQL(ctx, sql)
}
// HasForeignKey checks if a foreign key constraint exists.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "DATABASE()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.table_constraints WHERE constraint_schema = %s AND table_name = '%s' AND constraint_name = '%s' AND constraint_type = 'FOREIGN KEY'",
schema,
table,
constraint,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateSchema creates a new database schema.
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
sql := fmt.Sprintf("CREATE DATABASE IF NOT EXISTS %s", database.QuoteIdentifier(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema.
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := "DROP DATABASE "
if len(cascade) > 0 && cascade[0] {
sql += "IF EXISTS "
}
sql += database.QuoteIdentifier(schema)
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '%s'",
schema,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "''")
s = strings.ReplaceAll(s, "\\", "\\\\")
return s
}

View File

@ -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&...&paramN=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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

@ -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}]', '<p>礼品详情</p>', 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);

View File

@ -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);

View File

@ -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);

View File

@ -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\": \"是\"}]');

View File

@ -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);

View File

@ -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');

View File

@ -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

View File

@ -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;

View File

@ -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');

View File

@ -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;

View File

@ -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;

View File

@ -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);

View File

@ -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
);

View File

@ -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);

View File

@ -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;

View File

@ -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');

View File

@ -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');

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,56 @@
// 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 implements database.Driver, which supports operations for database Oracle.
package oracle
import (
"git.magicany.cc/black1552/gin-base/database"
)
// Driver is the driver for oracle database.
type Driver struct {
*database.Core
}
const (
rowNumberAliasForSelect = `ROW_NUMBER__`
quoteChar = `"`
)
func init() {
if err := database.Register(`oracle`, New()); err != nil {
panic(err)
}
}
// 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.
// 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
}
// Migration returns a Migration instance for Oracle database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -0,0 +1,351 @@
// 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"
)
// Migration implements database migration operations for Oracle.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new Oracle Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
sql.WriteString(database.QuoteIdentifierDouble(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
var primaryKeys []string
for name, def := range columns {
colDef := m.buildColumnDefinition(name, def)
if def.PrimaryKey {
primaryKeys = append(primaryKeys, database.QuoteIdentifierDouble(name))
}
colDefs = append(colDefs, " "+colDef)
}
// Add primary key constraint if needed
if len(primaryKeys) > 0 {
colDefs = append(colDefs, fmt.Sprintf(" PRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")))
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
return m.ExecuteSQL(ctx, sql.String())
}
// buildColumnDefinition builds column definition for Oracle.
func (m *Migration) buildColumnDefinition(name string, def *database.ColumnDefinition) string {
var parts []string
parts = append(parts, database.QuoteIdentifierDouble(name))
// Handle Oracle-specific types
dbType := def.Type
parts = append(parts, dbType)
if !def.Null {
parts = append(parts, "NOT NULL")
}
if def.Default != nil {
defaultValue := formatDefaultValue(def.Default)
parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue))
}
return strings.Join(parts, " ")
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
if len(ifExists) > 0 && ifExists[0] {
sql := fmt.Sprintf(
"BEGIN EXECUTE IMMEDIATE 'DROP TABLE %s'; EXCEPTION WHEN OTHERS THEN IF SQLCODE != -942 THEN RAISE; END IF; END;",
database.QuoteIdentifierDouble(table),
)
return m.ExecuteSQL(ctx, sql)
}
sql := fmt.Sprintf("DROP TABLE %s", database.QuoteIdentifierDouble(table))
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM USER_TABLES WHERE TABLE_NAME = '%s'",
strings.ToUpper(table),
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME TO %s",
database.QuoteIdentifierDouble(oldName),
database.QuoteIdentifierDouble(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifierDouble(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD %s", database.QuoteIdentifierDouble(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME COLUMN %s TO %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(oldName),
database.QuoteIdentifierDouble(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s MODIFY %s", database.QuoteIdentifierDouble(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE ")
if opts.Unique {
sql.WriteString("UNIQUE ")
}
sql.WriteString("INDEX ")
sql.WriteString(database.QuoteIdentifierDouble(index))
sql.WriteString(" ON ")
sql.WriteString(database.QuoteIdentifierDouble(table))
colList := m.BuildIndexColumnsWithDouble(columns)
sql.WriteString(fmt.Sprintf(" (%s)", colList))
return m.ExecuteSQL(ctx, sql.String())
}
// BuildIndexColumnsWithDouble builds the column list for index creation using double quotes.
func (m *Migration) BuildIndexColumnsWithDouble(columns []string) string {
quoted := make([]string, len(columns))
for i, col := range columns {
quoted[i] = database.QuoteIdentifierDouble(col)
}
return strings.Join(quoted, ", ")
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf("DROP INDEX %s", database.QuoteIdentifierDouble(index))
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM USER_INDEXES WHERE INDEX_NAME = '%s' AND TABLE_NAME = '%s'",
strings.ToUpper(index),
strings.ToUpper(table),
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey creates a foreign key constraint.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
var opts database.ForeignKeyOptions
for _, opt := range options {
opt(&opts)
}
sql := fmt.Sprintf(
"ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY %s REFERENCES %s %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(constraint),
m.BuildForeignKeyColumnsWithDouble(columns),
database.QuoteIdentifierDouble(refTable),
m.BuildForeignKeyColumnsWithDouble(refColumns),
)
if opts.OnDelete != "" {
sql += fmt.Sprintf(" ON DELETE %s", opts.OnDelete)
}
return m.ExecuteSQL(ctx, sql)
}
// BuildForeignKeyColumnsWithDouble builds the column list for foreign key using double quotes.
func (m *Migration) BuildForeignKeyColumnsWithDouble(columns []string) string {
quoted := make([]string, len(columns))
for i, col := range columns {
quoted[i] = database.QuoteIdentifierDouble(col)
}
return "(" + strings.Join(quoted, ", ") + ")"
}
// DropForeignKey drops a foreign key constraint.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP CONSTRAINT %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(constraint),
)
return m.ExecuteSQL(ctx, sql)
}
// HasForeignKey checks if a foreign key constraint exists.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM USER_CONSTRAINTS WHERE CONSTRAINT_NAME = '%s' AND TABLE_NAME = '%s' AND CONSTRAINT_TYPE = 'R'",
strings.ToUpper(constraint),
strings.ToUpper(table),
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateSchema creates a new database schema (user in Oracle).
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
// In Oracle, schema is equivalent to user
sql := fmt.Sprintf("CREATE USER %s IDENTIFIED BY password", database.QuoteIdentifierDouble(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema (user in Oracle).
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := "DROP USER "
if len(cascade) > 0 && cascade[0] {
sql += database.QuoteIdentifierDouble(schema) + " CASCADE"
} else {
sql += database.QuoteIdentifierDouble(schema)
}
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM ALL_USERS WHERE USERNAME = '%s'",
strings.ToUpper(schema),
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// formatDefaultValue formats the default value for SQL.
func formatDefaultValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
case float32, float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "1"
}
return "0"
case nil:
return "NULL"
default:
return fmt.Sprintf("'%v'", v)
}
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "''")
return s
}

View File

@ -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&...&paramN=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
}

View File

@ -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()"
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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 pgsql implements database.Driver, which supports operations for database PostgreSQL.
package pgsql
import (
_ "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.
type Driver struct {
*database.Core
}
const (
internalPrimaryKeyInCtx gctx.StrKey = "primary_key"
defaultSchema string = "public"
quoteChar string = `"`
)
func init() {
if err := database.Register(`pgsql`, New()); err != nil {
panic(err)
}
}
// 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.
// 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
}
// Migration returns a Migration instance for PostgreSQL database operations.
func (d *Driver) Migration() *Migration {
return NewMigration(d)
}
// GetMigration returns a Migration instance implementing the Migration interface.
func (d *Driver) GetMigration() database.Migration {
return d.Migration()
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -0,0 +1,481 @@
// 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"
"strings"
"git.magicany.cc/black1552/gin-base/database"
)
// Migration implements database migration operations for PostgreSQL.
type Migration struct {
*database.MigrationCore
*database.AutoMigrateCore
}
// NewMigration creates a new PostgreSQL Migration instance.
func NewMigration(db database.DB) *Migration {
return &Migration{
MigrationCore: database.NewMigrationCore(db),
AutoMigrateCore: database.NewAutoMigrateCore(db),
}
}
// CreateTable creates a new table with the given name and column definitions.
func (m *Migration) CreateTable(ctx context.Context, table string, columns map[string]*database.ColumnDefinition, options ...database.TableOption) error {
if len(columns) == 0 {
return fmt.Errorf("cannot create table without columns")
}
var opts database.TableOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE TABLE ")
if opts.IfNotExists {
sql.WriteString("IF NOT EXISTS ")
}
sql.WriteString(database.QuoteIdentifierDouble(table))
sql.WriteString(" (\n")
// Add columns
var colDefs []string
var primaryKeys []string
for name, def := range columns {
colDef := m.buildColumnDefinition(name, def)
if def.PrimaryKey {
primaryKeys = append(primaryKeys, database.QuoteIdentifierDouble(name))
}
colDefs = append(colDefs, " "+colDef)
}
// Add primary key constraint if needed
if len(primaryKeys) > 0 {
colDefs = append(colDefs, fmt.Sprintf(" PRIMARY KEY (%s)", strings.Join(primaryKeys, ", ")))
}
sql.WriteString(strings.Join(colDefs, ",\n"))
sql.WriteString("\n)")
// Add table comment if provided
if opts.Comment != "" {
commentSQL := fmt.Sprintf(
"COMMENT ON TABLE %s IS '%s'",
database.QuoteIdentifierDouble(table),
escapeString(opts.Comment),
)
if err := m.ExecuteSQL(ctx, commentSQL); err != nil {
return err
}
}
return m.ExecuteSQL(ctx, sql.String())
}
// buildColumnDefinition builds column definition for PostgreSQL.
func (m *Migration) buildColumnDefinition(name string, def *database.ColumnDefinition) string {
var parts []string
parts = append(parts, database.QuoteIdentifierDouble(name))
// Handle PostgreSQL-specific types
dbType := def.Type
if def.AutoIncrement {
if dbType == "INT" || dbType == "INTEGER" {
dbType = "SERIAL"
} else if dbType == "BIGINT" {
dbType = "BIGSERIAL"
}
}
parts = append(parts, dbType)
if def.PrimaryKey {
// Primary key is handled separately
} else {
if !def.Null {
parts = append(parts, "NOT NULL")
}
if def.Unique {
parts = append(parts, "UNIQUE")
}
if def.Default != nil {
defaultValue := formatDefaultValue(def.Default)
parts = append(parts, fmt.Sprintf("DEFAULT %s", defaultValue))
}
}
return strings.Join(parts, " ")
}
// DropTable drops an existing table from the database.
func (m *Migration) DropTable(ctx context.Context, table string, ifExists ...bool) error {
sql := "DROP TABLE "
if len(ifExists) > 0 && ifExists[0] {
sql += "IF EXISTS "
}
sql += database.QuoteIdentifierDouble(table)
return m.ExecuteSQL(ctx, sql)
}
// HasTable checks if a table exists in the database.
func (m *Migration) HasTable(ctx context.Context, table string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "current_schema()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = %s AND table_name = '%s'",
schema,
table,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// RenameTable renames an existing table from oldName to newName.
func (m *Migration) RenameTable(ctx context.Context, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME TO %s",
database.QuoteIdentifierDouble(oldName),
database.QuoteIdentifierDouble(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// TruncateTable removes all records from a table but keeps the table structure.
func (m *Migration) TruncateTable(ctx context.Context, table string) error {
sql := fmt.Sprintf("TRUNCATE TABLE %s", database.QuoteIdentifierDouble(table))
return m.ExecuteSQL(ctx, sql)
}
// AddColumn adds a new column to an existing table.
func (m *Migration) AddColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
colDef := m.buildColumnDefinition(column, definition)
sql := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s", database.QuoteIdentifierDouble(table), colDef)
return m.ExecuteSQL(ctx, sql)
}
// DropColumn removes a column from an existing table.
func (m *Migration) DropColumn(ctx context.Context, table, column string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP COLUMN %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
)
return m.ExecuteSQL(ctx, sql)
}
// RenameColumn renames a column in an existing table.
func (m *Migration) RenameColumn(ctx context.Context, table, oldName, newName string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s RENAME COLUMN %s TO %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(oldName),
database.QuoteIdentifierDouble(newName),
)
return m.ExecuteSQL(ctx, sql)
}
// ModifyColumn modifies an existing column's definition.
func (m *Migration) ModifyColumn(ctx context.Context, table, column string, definition *database.ColumnDefinition) error {
// PostgreSQL requires multiple ALTER statements for different modifications
var statements []string
if definition.Type != "" {
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s TYPE %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
definition.Type,
))
}
if definition.Default != nil {
defaultValue := formatDefaultValue(definition.Default)
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s SET DEFAULT %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
defaultValue,
))
} else {
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s DROP DEFAULT",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
))
}
if !definition.Null {
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s SET NOT NULL",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
))
} else {
statements = append(statements, fmt.Sprintf(
"ALTER TABLE %s ALTER COLUMN %s DROP NOT NULL",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(column),
))
}
// Execute all statements
for _, stmt := range statements {
if err := m.ExecuteSQL(ctx, stmt); err != nil {
return err
}
}
return nil
}
// HasColumn checks if a column exists in a table.
func (m *Migration) HasColumn(ctx context.Context, table, column string) (bool, error) {
fields, err := m.GetDB().TableFields(ctx, table)
if err != nil {
return false, err
}
_, exists := fields[column]
return exists, nil
}
// CreateIndex creates a new index on the specified table and columns.
func (m *Migration) CreateIndex(ctx context.Context, table, index string, columns []string, options ...database.IndexOption) error {
var opts database.IndexOptions
for _, opt := range options {
opt(&opts)
}
var sql strings.Builder
sql.WriteString("CREATE ")
if opts.Unique {
sql.WriteString("UNIQUE ")
}
sql.WriteString("INDEX ")
sql.WriteString(database.QuoteIdentifierDouble(index))
sql.WriteString(" ON ")
sql.WriteString(database.QuoteIdentifierDouble(table))
colList := m.BuildIndexColumnsWithDouble(columns)
sql.WriteString(fmt.Sprintf(" (%s)", colList))
if opts.Using != "" {
sql.WriteString(fmt.Sprintf(" USING %s", opts.Using))
}
if opts.Comment != "" {
// Comment will be added after index creation
}
err := m.ExecuteSQL(ctx, sql.String())
if err != nil {
return err
}
// Add comment if provided
if opts.Comment != "" {
commentSQL := fmt.Sprintf(
"COMMENT ON INDEX %s IS '%s'",
database.QuoteIdentifierDouble(index),
escapeString(opts.Comment),
)
return m.ExecuteSQL(ctx, commentSQL)
}
return nil
}
// BuildIndexColumnsWithDouble builds the column list for index creation using double quotes.
func (m *Migration) BuildIndexColumnsWithDouble(columns []string) string {
quoted := make([]string, len(columns))
for i, col := range columns {
quoted[i] = database.QuoteIdentifierDouble(col)
}
return strings.Join(quoted, ", ")
}
// DropIndex drops an existing index from a table.
func (m *Migration) DropIndex(ctx context.Context, table, index string) error {
sql := fmt.Sprintf("DROP INDEX %s", database.QuoteIdentifierDouble(index))
return m.ExecuteSQL(ctx, sql)
}
// HasIndex checks if an index exists on a table.
func (m *Migration) HasIndex(ctx context.Context, table, index string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "current_schema()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM pg_indexes WHERE schemaname = %s AND tablename = '%s' AND indexname = '%s'",
schema,
table,
index,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateForeignKey creates a foreign key constraint.
func (m *Migration) CreateForeignKey(ctx context.Context, table, constraint string, columns []string, refTable string, refColumns []string, options ...database.ForeignKeyOption) error {
var opts database.ForeignKeyOptions
for _, opt := range options {
opt(&opts)
}
sql := fmt.Sprintf(
"ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY %s REFERENCES %s %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(constraint),
m.BuildForeignKeyColumnsWithDouble(columns),
database.QuoteIdentifierDouble(refTable),
m.BuildForeignKeyColumnsWithDouble(refColumns),
)
if opts.OnDelete != "" {
sql += fmt.Sprintf(" ON DELETE %s", opts.OnDelete)
}
if opts.OnUpdate != "" {
sql += fmt.Sprintf(" ON UPDATE %s", opts.OnUpdate)
}
if opts.Deferrable {
sql += " DEFERRABLE"
if opts.InitiallyDeferred {
sql += " INITIALLY DEFERRED"
}
}
return m.ExecuteSQL(ctx, sql)
}
// BuildForeignKeyColumnsWithDouble builds the column list for foreign key using double quotes.
func (m *Migration) BuildForeignKeyColumnsWithDouble(columns []string) string {
quoted := make([]string, len(columns))
for i, col := range columns {
quoted[i] = database.QuoteIdentifierDouble(col)
}
return "(" + strings.Join(quoted, ", ") + ")"
}
// DropForeignKey drops a foreign key constraint.
func (m *Migration) DropForeignKey(ctx context.Context, table, constraint string) error {
sql := fmt.Sprintf(
"ALTER TABLE %s DROP CONSTRAINT %s",
database.QuoteIdentifierDouble(table),
database.QuoteIdentifierDouble(constraint),
)
return m.ExecuteSQL(ctx, sql)
}
// HasForeignKey checks if a foreign key constraint exists.
func (m *Migration) HasForeignKey(ctx context.Context, table, constraint string) (bool, error) {
schema := m.GetDB().GetSchema()
if schema == "" {
schema = "current_schema()"
} else {
schema = fmt.Sprintf("'%s'", schema)
}
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.table_constraints WHERE constraint_schema = %s AND table_name = '%s' AND constraint_name = '%s' AND constraint_type = 'FOREIGN KEY'",
schema,
table,
constraint,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// CreateSchema creates a new database schema.
func (m *Migration) CreateSchema(ctx context.Context, schema string) error {
sql := fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", database.QuoteIdentifierDouble(schema))
return m.ExecuteSQL(ctx, sql)
}
// DropSchema drops an existing database schema.
func (m *Migration) DropSchema(ctx context.Context, schema string, cascade ...bool) error {
sql := "DROP SCHEMA "
if len(cascade) > 0 && cascade[0] {
sql += "IF EXISTS "
sql += database.QuoteIdentifierDouble(schema) + " CASCADE"
} else {
sql += "IF EXISTS " + database.QuoteIdentifierDouble(schema)
}
return m.ExecuteSQL(ctx, sql)
}
// HasSchema checks if a schema exists.
func (m *Migration) HasSchema(ctx context.Context, schema string) (bool, error) {
query := fmt.Sprintf(
"SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = '%s'",
schema,
)
value, err := m.GetDB().GetValue(ctx, query)
if err != nil {
return false, err
}
return value.Int() > 0, nil
}
// formatDefaultValue formats the default value for SQL.
func formatDefaultValue(value any) string {
switch v := value.(type) {
case string:
return fmt.Sprintf("'%s'", escapeString(v))
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64:
return fmt.Sprintf("%d", v)
case float32, float64:
return fmt.Sprintf("%f", v)
case bool:
if v {
return "TRUE"
}
return "FALSE"
case nil:
return "NULL"
default:
return fmt.Sprintf("'%v'", v)
}
}
// escapeString escapes special characters in strings for SQL.
func escapeString(s string) string {
s = strings.ReplaceAll(s, "'", "''")
return s
}

View File

@ -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
}

View File

@ -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()"
}

View File

@ -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
}

View File

@ -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
}

Some files were not shown because too many files have changed in this diff Show More